225 lines
9.6 KiB
TypeScript
225 lines
9.6 KiB
TypeScript
'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>
|
|
);
|
|
}
|