create modal for add ingredient
This commit is contained in:
parent
af49388593
commit
4fbc8003a0
@ -91,15 +91,25 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const { code, name, categoryId, subcategoryId, quantity, unit, unitPrice, vat, supplierId } = body;
|
||||
const { name, categoryId, subcategoryId, quantity, unit, unitPrice, vat, supplierId } = body;
|
||||
|
||||
if (!code || !name || !categoryId || !subcategoryId || !quantity || !unit || unitPrice == null || vat == null || !supplierId) {
|
||||
if (!name || !categoryId || !subcategoryId || !quantity || !unit || unitPrice == null || vat == null || !supplierId) {
|
||||
return NextResponse.json({ error: 'All fields are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// Auto-generate code
|
||||
const last = await db.collection('ingredients')
|
||||
.find({}, { projection: { code: 1 } })
|
||||
.sort({ code: -1 })
|
||||
.limit(1)
|
||||
.next();
|
||||
const lastNum = last?.code ? parseInt(last.code.replace('ING-', '')) : 0;
|
||||
const code = `ING-${String(lastNum + 1).padStart(3, '0')}`;
|
||||
|
||||
const now = new Date();
|
||||
const doc = {
|
||||
const doc: Record<string, unknown> = {
|
||||
code,
|
||||
name,
|
||||
categoryId: new ObjectId(categoryId),
|
||||
@ -113,6 +123,19 @@ export async function POST(request: Request) {
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
// Optional discount fields
|
||||
if (body.discountType) {
|
||||
doc.discountType = body.discountType;
|
||||
doc.discountValue = body.discountValue ?? 0;
|
||||
doc.applyDiscountToNet = body.applyDiscountToNet ?? false;
|
||||
}
|
||||
|
||||
// Optional advanced fields
|
||||
if (body.minStockLevel != null) doc.minStockLevel = body.minStockLevel;
|
||||
if (body.storageInstructions) doc.storageInstructions = body.storageInstructions;
|
||||
if (body.shelfLifeDays != null) doc.shelfLifeDays = body.shelfLifeDays;
|
||||
if (body.notes) doc.notes = body.notes;
|
||||
|
||||
const result = await db.collection('ingredients').insertOne(doc);
|
||||
|
||||
return NextResponse.json({ _id: result.insertedId, ...doc }, { status: 201 });
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
import IngredientTable, { Ingredient } from '@/components/IngredientTable';
|
||||
import AddIngredientModal from '@/components/AddIngredientModal';
|
||||
|
||||
interface Category {
|
||||
_id: string;
|
||||
@ -34,6 +35,7 @@ export default function IngredientsPage() {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [allSubcategories, setAllSubcategories] = useState<Subcategory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
|
||||
const limit = 10;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
@ -141,7 +143,10 @@ export default function IngredientsPage() {
|
||||
<button className="flex-1 sm:flex-none px-4 sm:px-5 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-700 font-medium hover:bg-gray-50 transition-colors text-sm sm:text-base">
|
||||
Import
|
||||
</button>
|
||||
<button className="flex-1 sm:flex-none px-4 sm:px-5 py-2.5 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 transition-colors text-sm sm:text-base whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
className="flex-1 sm:flex-none px-4 sm:px-5 py-2.5 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 transition-colors text-sm sm:text-base whitespace-nowrap"
|
||||
>
|
||||
+ Add Ingredient
|
||||
</button>
|
||||
</div>
|
||||
@ -301,6 +306,12 @@ export default function IngredientsPage() {
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<AddIngredientModal
|
||||
open={isAddModalOpen}
|
||||
onClose={() => setIsAddModalOpen(false)}
|
||||
onSaved={fetchIngredients}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
618
components/AddIngredientModal.tsx
Normal file
618
components/AddIngredientModal.tsx
Normal file
@ -0,0 +1,618 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
|
||||
interface Category {
|
||||
_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Subcategory {
|
||||
_id: string;
|
||||
name: string;
|
||||
categoryId: string;
|
||||
}
|
||||
|
||||
interface Supplier {
|
||||
_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AddIngredientModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
}
|
||||
|
||||
const CREATE_NEW = '__create_new__';
|
||||
|
||||
export default function AddIngredientModal({ open, onClose, onSaved }: AddIngredientModalProps) {
|
||||
// Reference data
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [subcategories, setSubcategories] = useState<Subcategory[]>([]);
|
||||
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
|
||||
|
||||
// Form fields
|
||||
const [name, setName] = useState('');
|
||||
const [categoryId, setCategoryId] = useState('');
|
||||
const [subcategoryId, setSubcategoryId] = useState('');
|
||||
const [quantity, setQuantity] = useState('');
|
||||
const [unit, setUnit] = useState('');
|
||||
const [grossPrice, setGrossPrice] = useState('');
|
||||
const [vat, setVat] = useState('9');
|
||||
const [supplierId, setSupplierId] = useState('');
|
||||
|
||||
// Discount
|
||||
const [discountType, setDiscountType] = useState<'value' | 'percent'>('value');
|
||||
const [discountValue, setDiscountValue] = useState('');
|
||||
const [applyDiscountToNet, setApplyDiscountToNet] = useState(false);
|
||||
|
||||
// Advanced
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [minStockLevel, setMinStockLevel] = useState('');
|
||||
const [storageInstructions, setStorageInstructions] = useState('');
|
||||
const [shelfLifeDays, setShelfLifeDays] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
|
||||
// Create-new inline states
|
||||
const [newCategoryName, setNewCategoryName] = useState('');
|
||||
const [newSubcategoryName, setNewSubcategoryName] = useState('');
|
||||
const [newSupplierName, setNewSupplierName] = useState('');
|
||||
const [creatingCategory, setCreatingCategory] = useState(false);
|
||||
const [creatingSubcategory, setCreatingSubcategory] = useState(false);
|
||||
const [creatingSupplier, setCreatingSupplier] = useState(false);
|
||||
|
||||
// Save state
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Fetch reference data
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
Promise.all([
|
||||
fetch('/api/categories').then(r => r.json()),
|
||||
fetch('/api/subcategories').then(r => r.json()),
|
||||
fetch('/api/suppliers').then(r => r.json()),
|
||||
]).then(([cats, subs, supps]) => {
|
||||
setCategories(cats);
|
||||
setSubcategories(subs);
|
||||
setSuppliers(supps);
|
||||
});
|
||||
}, [open]);
|
||||
|
||||
// Reset form when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setName('');
|
||||
setCategoryId('');
|
||||
setSubcategoryId('');
|
||||
setQuantity('');
|
||||
setUnit('');
|
||||
setGrossPrice('');
|
||||
setVat('9');
|
||||
setSupplierId('');
|
||||
setDiscountType('value');
|
||||
setDiscountValue('');
|
||||
setApplyDiscountToNet(false);
|
||||
setShowAdvanced(false);
|
||||
setMinStockLevel('');
|
||||
setStorageInstructions('');
|
||||
setShelfLifeDays('');
|
||||
setNotes('');
|
||||
setNewCategoryName('');
|
||||
setNewSubcategoryName('');
|
||||
setNewSupplierName('');
|
||||
setCreatingCategory(false);
|
||||
setCreatingSubcategory(false);
|
||||
setCreatingSupplier(false);
|
||||
setSaving(false);
|
||||
setError('');
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Clear subcategory when category changes
|
||||
useEffect(() => {
|
||||
setSubcategoryId('');
|
||||
}, [categoryId]);
|
||||
|
||||
const filteredSubcategories = categoryId && categoryId !== CREATE_NEW
|
||||
? subcategories.filter(s => s.categoryId === categoryId)
|
||||
: subcategories;
|
||||
|
||||
// Price calculations
|
||||
const gross = parseFloat(grossPrice) || 0;
|
||||
const vatRate = parseFloat(vat) || 0;
|
||||
const netPrice = gross * (1 + vatRate / 100);
|
||||
const discount = parseFloat(discountValue) || 0;
|
||||
|
||||
const { effectiveNet, effectiveGross } = useMemo(() => {
|
||||
if (discount <= 0) return { effectiveNet: netPrice, effectiveGross: gross };
|
||||
|
||||
if (applyDiscountToNet) {
|
||||
const discountAmount = discountType === 'value' ? discount : netPrice * discount / 100;
|
||||
const effNet = netPrice - discountAmount;
|
||||
const effGross = vatRate > 0 ? effNet / (1 + vatRate / 100) : effNet;
|
||||
return { effectiveNet: effNet, effectiveGross: effGross };
|
||||
} else {
|
||||
const discountAmount = discountType === 'value' ? discount : gross * discount / 100;
|
||||
const effGross = gross - discountAmount;
|
||||
const effNet = effGross * (1 + vatRate / 100);
|
||||
return { effectiveNet: effNet, effectiveGross: effGross };
|
||||
}
|
||||
}, [gross, vatRate, netPrice, discount, discountType, applyDiscountToNet]);
|
||||
|
||||
// Create-new handlers
|
||||
const handleCreateCategory = async () => {
|
||||
if (!newCategoryName.trim()) return;
|
||||
setCreatingCategory(true);
|
||||
const res = await fetch('/api/categories', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newCategoryName.trim() }),
|
||||
});
|
||||
const created = await res.json();
|
||||
setCategories(prev => [...prev, created].sort((a, b) => a.name.localeCompare(b.name)));
|
||||
setCategoryId(created._id);
|
||||
setNewCategoryName('');
|
||||
setCreatingCategory(false);
|
||||
};
|
||||
|
||||
const handleCreateSubcategory = async () => {
|
||||
if (!newSubcategoryName.trim() || !categoryId || categoryId === CREATE_NEW) return;
|
||||
setCreatingSubcategory(true);
|
||||
const res = await fetch('/api/subcategories', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newSubcategoryName.trim(), categoryId }),
|
||||
});
|
||||
const created = await res.json();
|
||||
setSubcategories(prev => [...prev, created].sort((a, b) => a.name.localeCompare(b.name)));
|
||||
setSubcategoryId(created._id);
|
||||
setNewSubcategoryName('');
|
||||
setCreatingSubcategory(false);
|
||||
};
|
||||
|
||||
const handleCreateSupplier = async () => {
|
||||
if (!newSupplierName.trim()) return;
|
||||
setCreatingSupplier(true);
|
||||
const res = await fetch('/api/suppliers', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newSupplierName.trim() }),
|
||||
});
|
||||
const created = await res.json();
|
||||
setSuppliers(prev => [...prev, created].sort((a, b) => a.name.localeCompare(b.name)));
|
||||
setSupplierId(created._id);
|
||||
setNewSupplierName('');
|
||||
setCreatingSupplier(false);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setError('');
|
||||
|
||||
if (!name.trim()) { setError('Ingredient name is required.'); return; }
|
||||
if (!categoryId || categoryId === CREATE_NEW) { setError('Please select a category.'); return; }
|
||||
if (!subcategoryId || subcategoryId === CREATE_NEW) { setError('Please select a subcategory.'); return; }
|
||||
if (!quantity || parseFloat(quantity) <= 0) { setError('Quantity must be greater than 0.'); return; }
|
||||
if (!unit.trim()) { setError('Unit is required.'); return; }
|
||||
if (!grossPrice || gross <= 0) { setError('Gross price must be greater than 0.'); return; }
|
||||
if (!supplierId || supplierId === CREATE_NEW) { setError('Please select a supplier.'); return; }
|
||||
|
||||
setSaving(true);
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
name: name.trim(),
|
||||
categoryId,
|
||||
subcategoryId,
|
||||
quantity: parseFloat(quantity),
|
||||
unit: unit.trim(),
|
||||
unitPrice: gross,
|
||||
vat: vatRate,
|
||||
supplierId,
|
||||
};
|
||||
|
||||
if (discount > 0) {
|
||||
payload.discountType = discountType;
|
||||
payload.discountValue = discount;
|
||||
payload.applyDiscountToNet = applyDiscountToNet;
|
||||
}
|
||||
|
||||
if (showAdvanced) {
|
||||
if (minStockLevel) payload.minStockLevel = parseFloat(minStockLevel);
|
||||
if (storageInstructions.trim()) payload.storageInstructions = storageInstructions.trim();
|
||||
if (shelfLifeDays) payload.shelfLifeDays = parseInt(shelfLifeDays);
|
||||
if (notes.trim()) payload.notes = notes.trim();
|
||||
}
|
||||
|
||||
const res = await fetch('/api/ingredients', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
setError(data.error || 'Failed to save ingredient.');
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(false);
|
||||
onSaved();
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const inputClass = 'w-full px-3 py-2 rounded-lg border border-gray-300 bg-white text-gray-900 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors';
|
||||
const labelClass = 'block text-sm font-medium text-gray-700 mb-1';
|
||||
const selectClass = 'w-full px-3 py-2 rounded-lg border border-gray-300 bg-white text-gray-900 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors';
|
||||
|
||||
const fmt = (n: number) => n < 0 ? `-€${Math.abs(n).toFixed(2)}` : `€${n.toFixed(2)}`;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto">
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-black/40" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl mx-4 my-8 sm:my-16">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Add Ingredient</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-6 py-5 space-y-5">
|
||||
{error && (
|
||||
<div className="px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ingredient Name */}
|
||||
<div>
|
||||
<label className={labelClass}>Ingredient Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="e.g. Organic Butter"
|
||||
className={inputClass}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category & Subcategory */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label className={labelClass}>Category</label>
|
||||
<select
|
||||
value={categoryId}
|
||||
onChange={e => setCategoryId(e.target.value)}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="">Select category…</option>
|
||||
<option value={CREATE_NEW}>+ Create new…</option>
|
||||
{categories.map(c => (
|
||||
<option key={c._id} value={c._id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{categoryId === CREATE_NEW && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newCategoryName}
|
||||
onChange={e => setNewCategoryName(e.target.value)}
|
||||
placeholder="Category name"
|
||||
className={inputClass}
|
||||
onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), handleCreateCategory())}
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreateCategory}
|
||||
disabled={creatingCategory || !newCategoryName.trim()}
|
||||
className="px-3 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 disabled:opacity-50 whitespace-nowrap transition-colors"
|
||||
>
|
||||
{creatingCategory ? '…' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Subcategory */}
|
||||
<div>
|
||||
<label className={labelClass}>Subcategory</label>
|
||||
<select
|
||||
value={subcategoryId}
|
||||
onChange={e => setSubcategoryId(e.target.value)}
|
||||
className={selectClass}
|
||||
disabled={!categoryId || categoryId === CREATE_NEW}
|
||||
>
|
||||
<option value="">Select subcategory…</option>
|
||||
<option value={CREATE_NEW}>+ Create new…</option>
|
||||
{filteredSubcategories.map(s => (
|
||||
<option key={s._id} value={s._id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{subcategoryId === CREATE_NEW && categoryId && categoryId !== CREATE_NEW && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newSubcategoryName}
|
||||
onChange={e => setNewSubcategoryName(e.target.value)}
|
||||
placeholder="Subcategory name"
|
||||
className={inputClass}
|
||||
onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), handleCreateSubcategory())}
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreateSubcategory}
|
||||
disabled={creatingSubcategory || !newSubcategoryName.trim()}
|
||||
className="px-3 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 disabled:opacity-50 whitespace-nowrap transition-colors"
|
||||
>
|
||||
{creatingSubcategory ? '…' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quantity, Unit & Supplier */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Quantity</label>
|
||||
<input
|
||||
type="number"
|
||||
value={quantity}
|
||||
onChange={e => setQuantity(e.target.value)}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
step="any"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Unit</label>
|
||||
<input
|
||||
type="text"
|
||||
value={unit}
|
||||
onChange={e => setUnit(e.target.value)}
|
||||
placeholder="kg, L, pcs…"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className={labelClass}>Supplier</label>
|
||||
<select
|
||||
value={supplierId}
|
||||
onChange={e => setSupplierId(e.target.value)}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="">Select supplier…</option>
|
||||
<option value={CREATE_NEW}>+ Create new…</option>
|
||||
{suppliers.map(s => (
|
||||
<option key={s._id} value={s._id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{supplierId === CREATE_NEW && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newSupplierName}
|
||||
onChange={e => setNewSupplierName(e.target.value)}
|
||||
placeholder="Supplier name"
|
||||
className={inputClass}
|
||||
onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), handleCreateSupplier())}
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreateSupplier}
|
||||
disabled={creatingSupplier || !newSupplierName.trim()}
|
||||
className="px-3 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 disabled:opacity-50 whitespace-nowrap transition-colors"
|
||||
>
|
||||
{creatingSupplier ? '…' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">Pricing</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Gross Price (€)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={grossPrice}
|
||||
onChange={e => setGrossPrice(e.target.value)}
|
||||
placeholder="0.00"
|
||||
min="0"
|
||||
step="0.01"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>VAT (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={vat}
|
||||
onChange={e => setVat(e.target.value)}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
step="0.1"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Net Price</label>
|
||||
<div className="px-3 py-2 rounded-lg border border-gray-200 bg-gray-50 text-sm text-gray-700 font-medium">
|
||||
{fmt(netPrice)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Discount */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">Discount</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Type</label>
|
||||
<div className="flex items-center gap-4 mt-1">
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="discountType"
|
||||
checked={discountType === 'value'}
|
||||
onChange={() => setDiscountType('value')}
|
||||
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
Fixed value (€)
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="discountType"
|
||||
checked={discountType === 'percent'}
|
||||
onChange={() => setDiscountType('percent')}
|
||||
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
Percentage (%)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>
|
||||
{discountType === 'value' ? 'Discount Amount (€)' : 'Discount Rate (%)'}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={discountValue}
|
||||
onChange={e => setDiscountValue(e.target.value)}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
step={discountType === 'value' ? '0.01' : '0.1'}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 mt-3 text-sm text-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={applyDiscountToNet}
|
||||
onChange={e => setApplyDiscountToNet(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
Apply discount to net price
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="grid grid-cols-2 gap-4 rounded-lg bg-gray-50 border border-gray-200 p-4">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Effective Gross</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1">{fmt(effectiveGross)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Effective Net</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1">{fmt(effectiveNet)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Options */}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${showAdvanced ? 'rotate-90' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
Advanced options
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="mt-3 space-y-4 pl-6 border-l-2 border-gray-200">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Min Stock Level</label>
|
||||
<input
|
||||
type="number"
|
||||
value={minStockLevel}
|
||||
onChange={e => setMinStockLevel(e.target.value)}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
step="any"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Shelf Life (days)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={shelfLifeDays}
|
||||
onChange={e => setShelfLifeDays(e.target.value)}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Storage Instructions</label>
|
||||
<input
|
||||
type="text"
|
||||
value={storageInstructions}
|
||||
onChange={e => setStorageInstructions(e.target.value)}
|
||||
placeholder="e.g. Keep refrigerated at 2-8°C"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Notes</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={e => setNotes(e.target.value)}
|
||||
placeholder="Additional notes…"
|
||||
rows={2}
|
||||
className={inputClass + ' resize-none'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={saving}
|
||||
className="px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-700 text-sm font-medium hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-5 py-2.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user