diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 39c8e6f..5b1ef7c 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -42,4 +42,6 @@ model Config { finalDepositDate String? cookDay String? payIdInfo String? @default("Your PayID Here") + maxTotalWeight Float? @default(100.0) + maxItemWeight Float? @default(50.0) } diff --git a/backend/src/server.ts b/backend/src/server.ts index f74faca..50d5ff0 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -138,13 +138,15 @@ app.delete('/api/stock/:id', async (req, res) => { // Update Config app.post('/api/config', async (req, res) => { try { - const { adminCode, finalDepositDate, cookDay, payIdInfo } = req.body; + const { adminCode, finalDepositDate, cookDay, payIdInfo, maxTotalWeight, maxItemWeight } = req.body; const config = await prisma.config.findFirst(); const data: any = {}; if (adminCode !== undefined) data.adminCode = adminCode; if (finalDepositDate !== undefined) data.finalDepositDate = finalDepositDate; if (cookDay !== undefined) data.cookDay = cookDay; if (payIdInfo !== undefined) data.payIdInfo = payIdInfo; + if (maxTotalWeight !== undefined) data.maxTotalWeight = maxTotalWeight !== null ? parseFloat(maxTotalWeight) : null; + if (maxItemWeight !== undefined) data.maxItemWeight = maxItemWeight !== null ? parseFloat(maxItemWeight) : null; if (config) { await prisma.config.update({ @@ -168,8 +170,8 @@ app.post('/api/reset', async (req, res) => { await prisma.stockItem.deleteMany(); await prisma.config.deleteMany(); - // Seed default admin code - await prisma.config.create({ data: { adminCode: '1234' } }); + // Seed default admin code and limits + await prisma.config.create({ data: { adminCode: '1234', maxTotalWeight: 100.0, maxItemWeight: 50.0 } }); // Seed some default stock items await prisma.stockItem.createMany({ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 50bab6c..786125a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -57,24 +57,80 @@ function App() { }; const addToCart = (item: any) => { + const maxItem = config?.maxItemWeight ?? 50; + const maxTotal = config?.maxTotalWeight ?? 100; + + let isAllowed = true; + let errorMsg = ''; + setCart(prev => { const existing = prev.find(i => i.id === item.id); + const currentQty = existing ? existing.qty : 0; + const targetItemQty = currentQty + 1; + const currentTotalWeight = prev.reduce((acc, i) => acc + i.qty, 0); + const targetTotalWeight = currentTotalWeight + 1; + + if (targetItemQty > maxItem) { + isAllowed = false; + errorMsg = `Limit reached: Max ${maxItem}${item.unit} per item.`; + return prev; + } + + if (targetTotalWeight > maxTotal) { + isAllowed = false; + errorMsg = `Limit reached: Max ${maxTotal}kg total order weight.`; + return prev; + } + if (existing) { return prev.map(i => i.id === item.id ? { ...i, qty: i.qty + 1 } : i); } return [...prev, { ...item, qty: 1 }]; }); - showNotification(`Added 1${item.unit} ${item.name}`); + + if (!isAllowed) { + showNotification(errorMsg); + } else { + showNotification(`Added 1${item.unit} ${item.name}`); + } }; const updateCartQty = (itemId: number, delta: number) => { - setCart(prev => prev.map(i => { - if (i.id === itemId) { - const newQty = i.qty + delta; - return newQty > 0 ? { ...i, qty: newQty } : i; + const maxItem = config?.maxItemWeight ?? 50; + const maxTotal = config?.maxTotalWeight ?? 100; + + let isAllowed = true; + let errorMsg = ''; + + setCart(prev => { + const existing = prev.find(i => i.id === itemId); + if (!existing) return prev; + + const newQty = existing.qty + delta; + if (newQty <= 0) return prev; + + if (delta > 0) { + if (newQty > maxItem) { + isAllowed = false; + errorMsg = `Limit reached: Max ${maxItem}${existing.unit} per item.`; + return prev; + } + + const currentTotalWeight = prev.reduce((acc, i) => acc + i.qty, 0); + const targetTotalWeight = currentTotalWeight + delta; + if (targetTotalWeight > maxTotal) { + isAllowed = false; + errorMsg = `Limit reached: Max ${maxTotal}kg total order weight.`; + return prev; + } } - return i; - })); + + return prev.map(i => i.id === itemId ? { ...i, qty: newQty } : i); + }); + + if (!isAllowed) { + showNotification(errorMsg); + } }; const removeFromCart = (itemId: number) => { diff --git a/frontend/src/components/AdminDashboard.tsx b/frontend/src/components/AdminDashboard.tsx index 19621d2..93f314f 100644 --- a/frontend/src/components/AdminDashboard.tsx +++ b/frontend/src/components/AdminDashboard.tsx @@ -5,6 +5,27 @@ import { API } from '../services/api'; export default function AdminDashboard({ stock, orders, config, refreshData, notify, promptConfirm }: any) { const [tab, setTab] = useState<'orders' | 'stock' | 'settings'>('orders'); const [newAdminCode, setNewAdminCode] = useState(''); + const [settingsForm, setSettingsForm] = useState(null); + + useEffect(() => { + if (config && !settingsForm) { + setSettingsForm({ + finalDepositDate: config.finalDepositDate || '', + cookDay: config.cookDay || '', + payIdInfo: config.payIdInfo || '', + maxTotalWeight: config.maxTotalWeight !== undefined && config.maxTotalWeight !== null ? config.maxTotalWeight : 100, + maxItemWeight: config.maxItemWeight !== undefined && config.maxItemWeight !== null ? config.maxItemWeight : 50 + }); + } + }, [config, settingsForm]); + + const handleSaveSettings = async () => { + if (!settingsForm) return; + await API.updateConfig(settingsForm); + notify("Settings Saved"); + setSettingsForm(null); + refreshData(); + }; const totalRevenue = orders.reduce((acc: number, order: any) => acc + (order.estimatedTotal || 0), 0); const totalKg = orders.reduce((acc: number, order: any) => acc + (order.totalWeight || 0), 0); @@ -175,39 +196,72 @@ export default function AdminDashboard({ stock, orders, config, refreshData, not

Admin Settings

-
-

Scheduling & Deadlines

-
+ {settingsForm ? ( +
+

Scheduling & Deadlines

+
+
+ + setSettingsForm({ ...settingsForm, finalDepositDate: e.target.value })} + className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none" + /> +
+
+ + setSettingsForm({ ...settingsForm, cookDay: e.target.value })} + className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none" + /> +
+
- + API.updateConfig({ finalDepositDate: e.target.value }).then(refreshData)} + type="text" + placeholder="e.g. 0400 000 000 or email@domain.com" + value={settingsForm.payIdInfo} + onChange={(e) => setSettingsForm({ ...settingsForm, payIdInfo: e.target.value })} className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none" />
-
- - API.updateConfig({ cookDay: e.target.value }).then(refreshData)} - className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none" - /> + +

Order Limits

+
+
+ + setSettingsForm({ ...settingsForm, maxTotalWeight: parseFloat(e.target.value) || 0 })} + className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none" + /> +
+
+ + setSettingsForm({ ...settingsForm, maxItemWeight: parseFloat(e.target.value) || 0 })} + className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none" + /> +
+ +
-
- - API.updateConfig({ payIdInfo: e.target.value }).then(refreshData)} - className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none" - /> -
-
+ ) : ( +
Loading settings...
+ )}
@@ -231,18 +285,20 @@ function AdminStockItem({ item, onUpdate, onDelete }: any) { const [editData, setEditData] = useState({}); useEffect(() => { - setEditData({ - name: item.name || "", - category: item.category || "", - description: item.description || "", - price: item.price || 0, - unit: item.unit || "kg", - bulk1Threshold: item.bulk1Threshold || 0, - bulk1Price: item.bulk1Price || 0, - bulk2Threshold: item.bulk2Threshold || 0, - bulk2Price: item.bulk2Price || 0 - }); - }, [item]); + if (!isEditing) { + setEditData({ + name: item.name || "", + category: item.category || "", + description: item.description || "", + price: item.price || 0, + unit: item.unit || "kg", + bulk1Threshold: item.bulk1Threshold || 0, + bulk1Price: item.bulk1Price || 0, + bulk2Threshold: item.bulk2Threshold || 0, + bulk2Price: item.bulk2Price || 0 + }); + } + }, [item, isEditing]); const handleSave = () => { onUpdate({ diff --git a/frontend/src/components/CartModal.tsx b/frontend/src/components/CartModal.tsx index 67857f2..fba1108 100644 --- a/frontend/src/components/CartModal.tsx +++ b/frontend/src/components/CartModal.tsx @@ -167,6 +167,11 @@ export default function CartModal({ cart, onClose, onRemove, onUpdateQty, onSubm

Deposit Required

Order will not be cooked until the 30% deposit is paid. We accept PayID and Cash.

+ {(config?.maxTotalWeight || config?.maxItemWeight) && ( +

+ Limits: Max {config.maxTotalWeight ?? 100}kg total weight | Max {config.maxItemWeight ?? 50}kg per item. +

+ )}