fix click dosent do nothing

This commit is contained in:
CelaniDe 2026-02-11 22:10:39 +01:00
parent 4fbc8003a0
commit 2ad1edfea9
6 changed files with 504 additions and 37 deletions

View File

@ -45,11 +45,21 @@ export async function GET(
name: 1, name: 1,
category: { $arrayElemAt: ['$categoryDoc.name', 0] }, category: { $arrayElemAt: ['$categoryDoc.name', 0] },
subcategory: { $arrayElemAt: ['$subcategoryDoc.name', 0] }, subcategory: { $arrayElemAt: ['$subcategoryDoc.name', 0] },
categoryId: 1,
subcategoryId: 1,
supplierId: 1,
quantity: 1, quantity: 1,
unit: 1, unit: 1,
unitPrice: 1, unitPrice: 1,
vat: 1, vat: 1,
supplier: { $arrayElemAt: ['$supplierDoc.name', 0] }, supplier: { $arrayElemAt: ['$supplierDoc.name', 0] },
discountType: 1,
discountValue: 1,
applyDiscountToNet: 1,
minStockLevel: 1,
storageInstructions: 1,
shelfLifeDays: 1,
notes: 1,
createdAt: 1, createdAt: 1,
updatedAt: 1, updatedAt: 1,
}, },
@ -85,6 +95,13 @@ export async function PUT(
if (body.unitPrice !== undefined) update.unitPrice = body.unitPrice; if (body.unitPrice !== undefined) update.unitPrice = body.unitPrice;
if (body.vat !== undefined) update.vat = body.vat; if (body.vat !== undefined) update.vat = body.vat;
if (body.supplierId !== undefined) update.supplierId = new ObjectId(body.supplierId); if (body.supplierId !== undefined) update.supplierId = new ObjectId(body.supplierId);
if (body.discountType !== undefined) update.discountType = body.discountType;
if (body.discountValue !== undefined) update.discountValue = body.discountValue;
if (body.applyDiscountToNet !== undefined) update.applyDiscountToNet = body.applyDiscountToNet;
if (body.minStockLevel !== undefined) update.minStockLevel = body.minStockLevel;
if (body.storageInstructions !== undefined) update.storageInstructions = body.storageInstructions;
if (body.shelfLifeDays !== undefined) update.shelfLifeDays = body.shelfLifeDays;
if (body.notes !== undefined) update.notes = body.notes;
const db = await getDb(); const db = await getDb();
const result = await db.collection('ingredients').findOneAndUpdate( const result = await db.collection('ingredients').findOneAndUpdate(

View File

@ -4,6 +4,8 @@ import React, { useState, useEffect, useCallback } from 'react';
import Sidebar from '@/components/Sidebar'; import Sidebar from '@/components/Sidebar';
import IngredientTable, { Ingredient } from '@/components/IngredientTable'; import IngredientTable, { Ingredient } from '@/components/IngredientTable';
import AddIngredientModal from '@/components/AddIngredientModal'; import AddIngredientModal from '@/components/AddIngredientModal';
import ViewIngredientModal from '@/components/ViewIngredientModal';
import DeleteConfirmModal from '@/components/DeleteConfirmModal';
interface Category { interface Category {
_id: string; _id: string;
@ -36,6 +38,9 @@ export default function IngredientsPage() {
const [allSubcategories, setAllSubcategories] = useState<Subcategory[]>([]); const [allSubcategories, setAllSubcategories] = useState<Subcategory[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [editIngredientId, setEditIngredientId] = useState<string | null>(null);
const [viewIngredientId, setViewIngredientId] = useState<string | null>(null);
const [deleteIngredient, setDeleteIngredient] = useState<{ id: string; name: string } | null>(null);
const limit = 10; const limit = 10;
const totalPages = Math.ceil(total / limit); const totalPages = Math.ceil(total / limit);
@ -252,7 +257,12 @@ export default function IngredientsPage() {
<p className="text-gray-500">No ingredients found.</p> <p className="text-gray-500">No ingredients found.</p>
</div> </div>
) : ( ) : (
<IngredientTable ingredients={ingredients} /> <IngredientTable
ingredients={ingredients}
onView={(id) => setViewIngredientId(id)}
onEdit={(id) => setEditIngredientId(id)}
onDelete={(id, name) => setDeleteIngredient({ id, name })}
/>
)} )}
{/* Pagination */} {/* Pagination */}
@ -308,9 +318,28 @@ export default function IngredientsPage() {
</main> </main>
<AddIngredientModal <AddIngredientModal
open={isAddModalOpen} open={isAddModalOpen || !!editIngredientId}
onClose={() => setIsAddModalOpen(false)} onClose={() => { setIsAddModalOpen(false); setEditIngredientId(null); }}
onSaved={fetchIngredients} onSaved={fetchIngredients}
ingredientId={editIngredientId ?? undefined}
/>
<ViewIngredientModal
open={!!viewIngredientId}
ingredientId={viewIngredientId}
onClose={() => setViewIngredientId(null)}
/>
<DeleteConfirmModal
open={!!deleteIngredient}
ingredientName={deleteIngredient?.name ?? ''}
onClose={() => setDeleteIngredient(null)}
onConfirm={async () => {
if (!deleteIngredient) return;
await fetch(`/api/ingredients/${deleteIngredient.id}`, { method: 'DELETE' });
setDeleteIngredient(null);
fetchIngredients();
}}
/> />
</div> </div>
); );

View File

@ -22,11 +22,14 @@ interface AddIngredientModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSaved: () => void; onSaved: () => void;
ingredientId?: string;
} }
const CREATE_NEW = '__create_new__'; const CREATE_NEW = '__create_new__';
export default function AddIngredientModal({ open, onClose, onSaved }: AddIngredientModalProps) { export default function AddIngredientModal({ open, onClose, onSaved, ingredientId }: AddIngredientModalProps) {
const isEditMode = !!ingredientId;
const [loadingData, setLoadingData] = useState(false);
// Reference data // Reference data
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
const [subcategories, setSubcategories] = useState<Subcategory[]>([]); const [subcategories, setSubcategories] = useState<Subcategory[]>([]);
@ -80,9 +83,48 @@ export default function AddIngredientModal({ open, onClose, onSaved }: AddIngred
}); });
}, [open]); }, [open]);
// Reset form when modal opens // Reset form when modal opens (add mode) or fetch data (edit mode)
useEffect(() => { useEffect(() => {
if (open) { if (!open) return;
// Always reset transient state
setNewCategoryName('');
setNewSubcategoryName('');
setNewSupplierName('');
setCreatingCategory(false);
setCreatingSubcategory(false);
setCreatingSupplier(false);
setSaving(false);
setError('');
if (ingredientId) {
// Edit mode: fetch ingredient data after reference data loads
setLoadingData(true);
fetch(`/api/ingredients/${ingredientId}`)
.then(r => r.json())
.then(data => {
setName(data.name || '');
setCategoryId(data.categoryId || '');
setSubcategoryId(data.subcategoryId || '');
setQuantity(String(data.quantity ?? ''));
setUnit(data.unit || '');
setGrossPrice(String(data.unitPrice ?? ''));
setVat(String(data.vat ?? '9'));
setSupplierId(data.supplierId || '');
setDiscountType(data.discountType || 'value');
setDiscountValue(data.discountValue ? String(data.discountValue) : '');
setApplyDiscountToNet(data.applyDiscountToNet || false);
const hasAdvanced = data.minStockLevel || data.storageInstructions || data.shelfLifeDays || data.notes;
setShowAdvanced(!!hasAdvanced);
setMinStockLevel(data.minStockLevel ? String(data.minStockLevel) : '');
setStorageInstructions(data.storageInstructions || '');
setShelfLifeDays(data.shelfLifeDays ? String(data.shelfLifeDays) : '');
setNotes(data.notes || '');
setLoadingData(false);
});
} else {
// Add mode: reset all fields
setLoadingData(false);
setName(''); setName('');
setCategoryId(''); setCategoryId('');
setSubcategoryId(''); setSubcategoryId('');
@ -99,21 +141,17 @@ export default function AddIngredientModal({ open, onClose, onSaved }: AddIngred
setStorageInstructions(''); setStorageInstructions('');
setShelfLifeDays(''); setShelfLifeDays('');
setNotes(''); setNotes('');
setNewCategoryName('');
setNewSubcategoryName('');
setNewSupplierName('');
setCreatingCategory(false);
setCreatingSubcategory(false);
setCreatingSupplier(false);
setSaving(false);
setError('');
} }
}, [open]); }, [open, ingredientId]);
// Clear subcategory when category changes // Clear subcategory when category changes (only in add mode to avoid overwriting fetched data)
const [categoryUserChanged, setCategoryUserChanged] = useState(false);
useEffect(() => { useEffect(() => {
if (categoryUserChanged) {
setSubcategoryId(''); setSubcategoryId('');
}, [categoryId]); setCategoryUserChanged(false);
}
}, [categoryId, categoryUserChanged]);
const filteredSubcategories = categoryId && categoryId !== CREATE_NEW const filteredSubcategories = categoryId && categoryId !== CREATE_NEW
? subcategories.filter(s => s.categoryId === categoryId) ? subcategories.filter(s => s.categoryId === categoryId)
@ -224,8 +262,11 @@ export default function AddIngredientModal({ open, onClose, onSaved }: AddIngred
if (notes.trim()) payload.notes = notes.trim(); if (notes.trim()) payload.notes = notes.trim();
} }
const res = await fetch('/api/ingredients', { const url = isEditMode ? `/api/ingredients/${ingredientId}` : '/api/ingredients';
method: 'POST', const method = isEditMode ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
@ -259,7 +300,7 @@ export default function AddIngredientModal({ open, onClose, onSaved }: AddIngred
<div className="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl mx-4 my-8 sm:my-16"> <div className="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl mx-4 my-8 sm:my-16">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200"> <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> <h2 className="text-lg font-semibold text-gray-900">{isEditMode ? 'Edit Ingredient' : 'Add Ingredient'}</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition-colors"> <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"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
@ -269,12 +310,22 @@ export default function AddIngredientModal({ open, onClose, onSaved }: AddIngred
{/* Body */} {/* Body */}
<div className="px-6 py-5 space-y-5"> <div className="px-6 py-5 space-y-5">
{error && ( {loadingData && (
<div className="flex items-center justify-center py-12">
<svg className="animate-spin h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-sm text-gray-500">Loading ingredient data...</span>
</div>
)}
{!loadingData && error && (
<div className="px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"> <div className="px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700">
{error} {error}
</div> </div>
)} )}
{!loadingData && <>
{/* Ingredient Name */} {/* Ingredient Name */}
<div> <div>
<label className={labelClass}>Ingredient Name</label> <label className={labelClass}>Ingredient Name</label>
@ -295,7 +346,7 @@ export default function AddIngredientModal({ open, onClose, onSaved }: AddIngred
<label className={labelClass}>Category</label> <label className={labelClass}>Category</label>
<select <select
value={categoryId} value={categoryId}
onChange={e => setCategoryId(e.target.value)} onChange={e => { setCategoryId(e.target.value); setCategoryUserChanged(true); }}
className={selectClass} className={selectClass}
> >
<option value="">Select category</option> <option value="">Select category</option>
@ -593,6 +644,7 @@ export default function AddIngredientModal({ open, onClose, onSaved }: AddIngred
</div> </div>
)} )}
</div> </div>
</>}
</div> </div>
{/* Footer */} {/* Footer */}
@ -606,7 +658,7 @@ export default function AddIngredientModal({ open, onClose, onSaved }: AddIngred
</button> </button>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving || loadingData}
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" 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'} {saving ? 'Saving…' : 'Save'}

View File

@ -0,0 +1,61 @@
'use client';
import React, { useState } from 'react';
interface DeleteConfirmModalProps {
open: boolean;
ingredientName: string;
onClose: () => void;
onConfirm: () => Promise<void>;
}
export default function DeleteConfirmModal({ open, ingredientName, onClose, onConfirm }: DeleteConfirmModalProps) {
const [deleting, setDeleting] = useState(false);
if (!open) return null;
const handleDelete = async () => {
setDeleting(true);
await onConfirm();
setDeleting(false);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/40" onClick={onClose} />
<div className="relative bg-white rounded-xl shadow-2xl w-full max-w-md mx-4">
<div className="px-6 py-5">
<div className="flex items-center gap-3 mb-4">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</div>
<h2 className="text-lg font-semibold text-gray-900">Delete Ingredient</h2>
</div>
<p className="text-sm text-gray-600">
Are you sure you want to delete <span className="font-semibold text-gray-900">{ingredientName}</span>? This action cannot be undone.
</p>
</div>
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200">
<button
onClick={onClose}
disabled={deleting}
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={handleDelete}
disabled={deleting}
className="px-4 py-2.5 rounded-lg bg-red-600 text-white text-sm font-medium hover:bg-red-700 transition-colors disabled:opacity-50"
>
{deleting ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
export interface Ingredient { export interface Ingredient {
_id: string; _id: string;
@ -19,10 +19,33 @@ export interface Ingredient {
interface IngredientTableProps { interface IngredientTableProps {
ingredients: Ingredient[]; ingredients: Ingredient[];
onView: (id: string) => void;
onEdit: (id: string) => void;
onDelete: (id: string, name: string) => void;
} }
export default function IngredientTable({ ingredients }: IngredientTableProps) { export default function IngredientTable({ ingredients, onView, onEdit, onDelete }: IngredientTableProps) {
const [selectedIngredients, setSelectedIngredients] = useState<Set<string>>(new Set()); const [selectedIngredients, setSelectedIngredients] = useState<Set<string>>(new Set());
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const [menuPos, setMenuPos] = useState<{ top: number; right: number } | null>(null);
const menuRef = useRef<HTMLDivElement>(null);
// Close menu on outside click or scroll
useEffect(() => {
if (!openMenuId) return;
const handleClick = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setOpenMenuId(null);
}
};
const handleScroll = () => setOpenMenuId(null);
document.addEventListener('mousedown', handleClick);
window.addEventListener('scroll', handleScroll, true);
return () => {
document.removeEventListener('mousedown', handleClick);
window.removeEventListener('scroll', handleScroll, true);
};
}, [openMenuId]);
const toggleIngredient = (id: string) => { const toggleIngredient = (id: string) => {
const newSelected = new Set(selectedIngredients); const newSelected = new Set(selectedIngredients);
@ -46,10 +69,41 @@ export default function IngredientTable({ ingredients }: IngredientTableProps) {
return unitPrice * (1 + vat / 100); return unitPrice * (1 + vat / 100);
}; };
const handleMenuAction = useCallback((action: 'view' | 'edit' | 'delete', id: string, name: string) => {
setOpenMenuId(null);
if (action === 'view') onView(id);
else if (action === 'edit') onEdit(id);
else onDelete(id, name);
}, [onView, onEdit, onDelete]);
const toggleMenu = (ingredientId: string, e: React.MouseEvent<HTMLButtonElement>) => {
if (openMenuId === ingredientId) {
setOpenMenuId(null);
setMenuPos(null);
} else {
const rect = e.currentTarget.getBoundingClientRect();
setMenuPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
setOpenMenuId(ingredientId);
}
};
const renderMenuButton = (ingredient: Ingredient) => (
<button
onClick={(e) => toggleMenu(ingredient._id, e)}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</button>
);
const openIngredient = openMenuId ? ingredients.find(i => i._id === openMenuId) : null;
return ( return (
<> <>
{/* Desktop & Tablet Table View */} {/* Desktop & Tablet Table View */}
<div className="hidden md:block bg-white rounded-lg border border-gray-200 overflow-hidden"> <div className="hidden md:block bg-white rounded-lg border border-gray-200">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
@ -127,11 +181,7 @@ export default function IngredientTable({ ingredients }: IngredientTableProps) {
</a> </a>
</td> </td>
<td className="px-4 lg:px-6 py-4"> <td className="px-4 lg:px-6 py-4">
<button className="text-gray-400 hover:text-gray-600"> {renderMenuButton(ingredient)}
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</button>
</td> </td>
</tr> </tr>
))} ))}
@ -157,11 +207,7 @@ export default function IngredientTable({ ingredients }: IngredientTableProps) {
<h3 className="text-sm font-semibold text-gray-900">{ingredient.name}</h3> <h3 className="text-sm font-semibold text-gray-900">{ingredient.name}</h3>
<p className="text-xs text-gray-500 mt-0.5">ID: {ingredient.code}</p> <p className="text-xs text-gray-500 mt-0.5">ID: {ingredient.code}</p>
</div> </div>
<button className="text-gray-400 hover:text-gray-600"> {renderMenuButton(ingredient)}
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</button>
</div> </div>
<div className="mt-3 grid grid-cols-2 gap-3"> <div className="mt-3 grid grid-cols-2 gap-3">
@ -210,6 +256,44 @@ export default function IngredientTable({ ingredients }: IngredientTableProps) {
</div> </div>
))} ))}
</div> </div>
{/* Fixed-position dropdown menu (renders outside overflow containers) */}
{openMenuId && menuPos && openIngredient && (
<div
ref={menuRef}
className="fixed w-40 bg-white rounded-lg border border-gray-200 shadow-lg z-50 py-1"
style={{ top: menuPos.top, right: menuPos.right }}
>
<button
onClick={() => handleMenuAction('view', openIngredient._id, openIngredient.name)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
View
</button>
<button
onClick={() => handleMenuAction('edit', openIngredient._id, openIngredient.name)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
Edit
</button>
<button
onClick={() => handleMenuAction('delete', openIngredient._id, openIngredient.name)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete
</button>
</div>
)}
</> </>
); );
} }

View File

@ -0,0 +1,224 @@
'use client';
import React, { useState, useEffect } from 'react';
interface IngredientDetail {
_id: string;
code: string;
name: string;
category: string;
subcategory: string;
quantity: number;
unit: string;
unitPrice: number;
vat: number;
supplier: string;
discountType?: 'value' | 'percent';
discountValue?: number;
applyDiscountToNet?: boolean;
minStockLevel?: number;
storageInstructions?: string;
shelfLifeDays?: number;
notes?: string;
createdAt: string;
updatedAt: string;
}
interface ViewIngredientModalProps {
open: boolean;
ingredientId: string | null;
onClose: () => void;
}
export default function ViewIngredientModal({ open, ingredientId, onClose }: ViewIngredientModalProps) {
const [ingredient, setIngredient] = useState<IngredientDetail | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!open || !ingredientId) return;
setLoading(true);
setIngredient(null);
fetch(`/api/ingredients/${ingredientId}`)
.then(r => r.json())
.then(data => {
setIngredient(data);
setLoading(false);
});
}, [open, ingredientId]);
if (!open) return null;
const fmt = (n: number) => n < 0 ? `-\u20AC${Math.abs(n).toFixed(2)}` : `\u20AC${n.toFixed(2)}`;
const netPrice = ingredient ? ingredient.unitPrice * (1 + ingredient.vat / 100) : 0;
const hasDiscount = ingredient && ingredient.discountValue && ingredient.discountValue > 0;
const hasAdvanced = ingredient && (ingredient.minStockLevel || ingredient.storageInstructions || ingredient.shelfLifeDays || ingredient.notes);
return (
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto">
<div className="fixed inset-0 bg-black/40" onClick={onClose} />
<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">Ingredient Details</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">
{loading && (
<div className="flex items-center justify-center py-12">
<svg className="animate-spin h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-sm text-gray-500">Loading...</span>
</div>
)}
{!loading && ingredient && (
<div className="space-y-5">
{/* Name & Code */}
<div>
<h3 className="text-xl font-bold text-gray-900">{ingredient.name}</h3>
<p className="text-sm text-gray-500 mt-0.5">Code: {ingredient.code}</p>
</div>
{/* Category / Subcategory */}
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Category</p>
<p className="text-sm text-gray-900 mt-1">{ingredient.category}</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Subcategory</p>
<p className="text-sm text-gray-900 mt-1">{ingredient.subcategory}</p>
</div>
</div>
{/* Quantity / Unit */}
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Quantity</p>
<p className="text-sm text-gray-900 mt-1">{ingredient.quantity} {ingredient.unit}</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Supplier</p>
<p className="text-sm text-blue-600 font-medium mt-1">{ingredient.supplier}</p>
</div>
</div>
{/* Pricing */}
<div>
<h4 className="text-sm font-semibold text-gray-900 mb-3">Pricing</h4>
<div className="grid grid-cols-3 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">Gross Price</p>
<p className="text-lg font-bold text-gray-900 mt-1">{fmt(ingredient.unitPrice)}</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">VAT</p>
<p className="text-lg font-bold text-gray-900 mt-1">{ingredient.vat}%</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Net Price</p>
<p className="text-lg font-bold text-gray-900 mt-1">{fmt(netPrice)}</p>
</div>
</div>
</div>
{/* Discount */}
{hasDiscount && (
<div>
<h4 className="text-sm font-semibold text-gray-900 mb-3">Discount</h4>
<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">Type</p>
<p className="text-sm text-gray-900 mt-1">
{ingredient.discountType === 'percent' ? 'Percentage' : 'Fixed value'}
</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Value</p>
<p className="text-sm text-gray-900 mt-1">
{ingredient.discountType === 'percent'
? `${ingredient.discountValue}%`
: fmt(ingredient.discountValue!)}
</p>
</div>
{ingredient.applyDiscountToNet && (
<div className="col-span-2">
<p className="text-xs text-gray-500 italic">Applied to net price</p>
</div>
)}
</div>
</div>
)}
{/* Advanced */}
{hasAdvanced && (
<div>
<h4 className="text-sm font-semibold text-gray-900 mb-3">Additional Details</h4>
<div className="space-y-3 pl-4 border-l-2 border-gray-200">
{ingredient.minStockLevel != null && (
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Min Stock Level</p>
<p className="text-sm text-gray-900 mt-0.5">{ingredient.minStockLevel}</p>
</div>
)}
{ingredient.shelfLifeDays != null && (
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Shelf Life</p>
<p className="text-sm text-gray-900 mt-0.5">{ingredient.shelfLifeDays} days</p>
</div>
)}
{ingredient.storageInstructions && (
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Storage Instructions</p>
<p className="text-sm text-gray-900 mt-0.5">{ingredient.storageInstructions}</p>
</div>
)}
{ingredient.notes && (
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Notes</p>
<p className="text-sm text-gray-900 mt-0.5">{ingredient.notes}</p>
</div>
)}
</div>
</div>
)}
{/* Timestamps */}
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-200">
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Created</p>
<p className="text-sm text-gray-700 mt-0.5">{new Date(ingredient.createdAt).toLocaleString()}</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Updated</p>
<p className="text-sm text-gray-700 mt-0.5">{new Date(ingredient.updatedAt).toLocaleString()}</p>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-end px-6 py-4 border-t border-gray-200">
<button
onClick={onClose}
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"
>
Close
</button>
</div>
</div>
</div>
);
}