From f27bd572e4a9e98236f12e7d699212a9a14b6dee Mon Sep 17 00:00:00 2001 From: Gemini CLI Date: Mon, 27 Jul 2026 16:26:57 +1000 Subject: [PATCH] feat: add safe fresh batch inventory reset --- .gitignore | 1 + Dockerfile | 2 +- backend/src/server.ts | 391 ++++++++++++--------- docker-compose.yml | 4 +- frontend/src/App.tsx | 38 +- frontend/src/components/AdminDashboard.tsx | 108 +++--- frontend/src/components/CartModal.tsx | 2 + frontend/src/components/CustomerView.tsx | 4 +- frontend/src/services/api.ts | 97 +++-- 9 files changed, 400 insertions(+), 247 deletions(-) diff --git a/.gitignore b/.gitignore index d23ae0c..e92674c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ backend/prisma/dev.db frontend/dist/ backend/dist/ .DS_Store +.serena/ diff --git a/Dockerfile b/Dockerfile index 9f6ad9e..265b5dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,4 +41,4 @@ RUN mkdir -p /app/data EXPOSE 3000 # Start script -CMD ["sh", "-c", "npx prisma db push --accept-data-loss && node dist/server.js"] +CMD ["sh", "-c", "npx prisma db push && node dist/server.js"] diff --git a/backend/src/server.ts b/backend/src/server.ts index 3f6335b..362f01a 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -11,15 +11,44 @@ const PORT = process.env.PORT || 3000; app.use(cors()); app.use(express.json()); +const requireAdmin: express.RequestHandler = async (req, res, next) => { + try { + const adminCode = req.get('x-admin-code'); + const config = await prisma.config.findUnique({ where: { id: 1 } }); + + if (!adminCode || !config || adminCode !== config.adminCode) { + res.status(401).json({ error: 'Admin authentication required' }); + return; + } + + next(); + } catch (error) { + console.error('Admin authentication failed:', error); + res.status(500).json({ error: 'Failed to authenticate admin' }); + } +}; + +const parseId = (value: string | string[] | undefined) => { + const id = Array.isArray(value) ? NaN : Number(value); + return Number.isInteger(id) && id > 0 ? id : null; +}; + // API Routes // Fetch all data (Stock, Orders, Config, FAQs) app.get('/api/data', async (req, res) => { try { - let stock = await prisma.stockItem.findMany(); + const stock = await prisma.stockItem.findMany(); const orders = await prisma.order.findMany(); - const config = await prisma.config.findFirst(); + const config = await prisma.config.findUnique({ where: { id: 1 } }); const faqs = await prisma.fAQ.findMany({ orderBy: { order: 'asc' } }); + + const suppliedAdminCode = req.get('x-admin-code'); + const isAdmin = Boolean(suppliedAdminCode && config && suppliedAdminCode === config.adminCode); + if (suppliedAdminCode && !isAdmin) { + res.status(401).json({ error: 'Admin session expired' }); + return; + } // Parse order items from string to JSON const parsedOrders = orders.map(order => ({ @@ -30,42 +59,40 @@ app.get('/api/data', async (req, res) => { // Tally total ordered kg per stock item across all orders const orderedQtyById: Record = {}; for (const order of parsedOrders) { + if (order.status === 'Cancelled') continue; for (const item of order.items) { orderedQtyById[item.id] = (orderedQtyById[item.id] || 0) + (item.qty || 0); } } - // Auto-deactivate items that have hit their maxStock limit - for (const item of stock) { - if (item.maxStock !== null && item.maxStock !== undefined) { - const ordered = orderedQtyById[item.id] || 0; - const isOutOfStock = ordered >= item.maxStock; - // Only auto-disable if it's currently marked as available but is actually out of stock - if (isOutOfStock && item.available) { - await prisma.stockItem.update({ - where: { id: item.id }, - data: { available: false } - }); - } - } - } + // Attach computed batch availability without overwriting the admin's availability setting + const stockWithRemaining = stock.map(item => { + const orderedQty = orderedQtyById[item.id] || 0; + const remainingStock = item.maxStock !== null + ? Math.max(0, item.maxStock - orderedQty) + : null; - // Re-fetch stock after potential auto-updates - stock = await prisma.stockItem.findMany(); - - // Attach remaining stock info to each item - const stockWithRemaining = stock.map(item => ({ - ...item, - orderedQty: orderedQtyById[item.id] || 0, - remainingStock: item.maxStock !== null && item.maxStock !== undefined - ? Math.max(0, item.maxStock - (orderedQtyById[item.id] || 0)) - : null - })); + return { + ...item, + available: item.available && (remainingStock === null || remainingStock > 0), + configuredAvailable: item.available, + orderedQty, + remainingStock + }; + }); // Security: Don't send the admin code to the client - const configResponse = config ? { ...config, adminCode: undefined } : null; + const configResponse = config ? { + id: config.id, + finalDepositDate: config.finalDepositDate, + cookDay: config.cookDay, + payIdInfo: config.payIdInfo, + termsOfService: config.termsOfService, + orderingPolicy: config.orderingPolicy, + depositPercentage: config.depositPercentage + } : null; - res.json({ stock: stockWithRemaining, orders: parsedOrders, config: configResponse, faqs }); + res.json({ stock: stockWithRemaining, orders: isAdmin ? parsedOrders : [], config: configResponse, faqs }); } catch (error) { console.error(error); res.status(500).json({ error: 'Failed to fetch data' }); @@ -76,13 +103,10 @@ app.get('/api/data', async (req, res) => { app.post('/api/login', async (req, res) => { try { const { code } = req.body; - console.log(`Login attempt with code: ${code}`); - const config = await prisma.config.findFirst(); + const config = await prisma.config.findUnique({ where: { id: 1 } }); if (config && config.adminCode === code) { - console.log('Login successful'); res.json({ success: true }); } else { - console.log(`Login failed. Expected: ${config?.adminCode}, Got: ${code}`); res.status(401).json({ error: 'Invalid admin code' }); } } catch (error) { @@ -94,67 +118,97 @@ app.post('/api/login', async (req, res) => { // Submit a new order app.post('/api/orders', async (req, res) => { try { - const { name, phone, items, totalWeight, estimatedTotal } = req.body; - - // 1. Validate Stock Availability - const stockItems = await prisma.stockItem.findMany(); - const allOrders = await prisma.order.findMany(); - - const orderedQtyById: Record = {}; - for (const order of allOrders) { - const parsedItems = JSON.parse(order.items); - for (const item of parsedItems) { - orderedQtyById[item.id] = (orderedQtyById[item.id] || 0) + (item.qty || 0); - } + const { name, phone, items } = req.body; + + if (typeof name !== 'string' || !name.trim() || typeof phone !== 'string' || !phone.trim()) { + throw new Error('Name and phone are required'); + } + if (!Array.isArray(items) || items.length === 0) { + throw new Error('At least one item is required'); } - let computedTotalWeight = 0; - let computedEstimatedTotal = 0; + const updatedOrder = await prisma.$transaction(async (tx) => { + const stockItems = await tx.stockItem.findMany(); + const allOrders = await tx.order.findMany({ + where: { status: { not: 'Cancelled' } }, + select: { items: true } + }); + const orderedQtyById: Record = {}; - for (const item of items) { - const dbItem = stockItems.find(s => s.id === item.id); - if (!dbItem) throw new Error(`Item ${item.name} not found`); - if (!dbItem.available) throw new Error(`Sorry, ${dbItem.name} has just sold out!`); - - if (dbItem.maxStock !== null && dbItem.maxStock !== undefined) { - const alreadyOrdered = orderedQtyById[dbItem.id] || 0; - if (alreadyOrdered + item.qty > dbItem.maxStock) { - throw new Error(`Not enough stock for ${dbItem.name}. Only ${Math.max(0, dbItem.maxStock - alreadyOrdered)}${dbItem.unit} remaining.`); + for (const order of allOrders) { + const parsedItems = JSON.parse(order.items); + for (const item of parsedItems) { + orderedQtyById[item.id] = (orderedQtyById[item.id] || 0) + (item.qty || 0); } } - // Recalculate prices server-side - const getPrice = (stockItem: any, qty: number) => { - if (stockItem.bulk2Threshold > 0 && qty >= stockItem.bulk2Threshold && stockItem.bulk2Price > 0) return stockItem.bulk2Price; - if (stockItem.bulk1Threshold > 0 && qty >= stockItem.bulk1Threshold && stockItem.bulk1Price > 0) return stockItem.bulk1Price; - return stockItem.price; - }; + const seenItemIds = new Set(); + const validatedItems: Array<{ + id: number; + name: string; + qty: number; + unit: string; + finalPricePerUnit: number; + lineTotal: number; + }> = []; + let computedTotalWeight = 0; + let computedEstimatedTotal = 0; - const finalPrice = getPrice(dbItem, item.qty); - item.finalPricePerUnit = finalPrice; - item.lineTotal = finalPrice * item.qty; - - computedTotalWeight += item.qty; - computedEstimatedTotal += item.lineTotal; - } + for (const item of items) { + if (!Number.isInteger(item?.id) || typeof item?.qty !== 'number' || !Number.isFinite(item.qty) || item.qty <= 0) { + throw new Error('Each order item must have a valid item ID and quantity greater than zero'); + } + if (seenItemIds.has(item.id)) { + throw new Error('Duplicate items are not allowed'); + } + seenItemIds.add(item.id); - // 2. Create the order - const order = await prisma.order.create({ - data: { - name, - phone, - totalWeight: computedTotalWeight, - estimatedTotal: computedEstimatedTotal, - items: JSON.stringify(items), - status: 'Pending' + const dbItem = stockItems.find(stockItem => stockItem.id === item.id); + if (!dbItem) throw new Error('An item in this order no longer exists'); + if (!dbItem.available) throw new Error(`Sorry, ${dbItem.name} has just sold out!`); + + if (dbItem.maxStock !== null) { + const alreadyOrdered = orderedQtyById[dbItem.id] || 0; + if (alreadyOrdered + item.qty > dbItem.maxStock) { + throw new Error(`Not enough stock for ${dbItem.name}. Only ${Math.max(0, dbItem.maxStock - alreadyOrdered)}${dbItem.unit} remaining.`); + } + } + + let finalPrice = dbItem.price; + if (dbItem.bulk2Threshold > 0 && item.qty >= dbItem.bulk2Threshold && dbItem.bulk2Price > 0) { + finalPrice = dbItem.bulk2Price; + } else if (dbItem.bulk1Threshold > 0 && item.qty >= dbItem.bulk1Threshold && dbItem.bulk1Price > 0) { + finalPrice = dbItem.bulk1Price; + } + + const lineTotal = finalPrice * item.qty; + validatedItems.push({ + id: dbItem.id, + name: dbItem.name, + qty: item.qty, + unit: dbItem.unit, + finalPricePerUnit: finalPrice, + lineTotal + }); + computedTotalWeight += item.qty; + computedEstimatedTotal += lineTotal; } - }); - // 3. Generate and update order number based on ID (to avoid race conditions) - const orderNumber = `#${order.id + 1000}`; - const updatedOrder = await prisma.order.update({ - where: { id: order.id }, - data: { orderNumber } + const order = await tx.order.create({ + data: { + name: name.trim(), + phone: phone.trim(), + totalWeight: computedTotalWeight, + estimatedTotal: computedEstimatedTotal, + items: JSON.stringify(validatedItems), + status: 'Pending' + } + }); + + return tx.order.update({ + where: { id: order.id }, + data: { orderNumber: `#${order.id + 1000}` } + }); }); res.json(updatedOrder); @@ -165,12 +219,22 @@ app.post('/api/orders', async (req, res) => { }); // Update Order Status -app.patch('/api/orders/:id/status', async (req, res) => { +app.patch('/api/orders/:id/status', requireAdmin, async (req, res) => { try { const { id } = req.params; const { status } = req.body; + const orderId = parseId(id); + const allowedStatuses = ['Pending', 'Deposit Paid', 'Paid', 'Cooked', 'Completed', 'Cancelled']; + if (orderId === null) { + res.status(400).json({ error: 'Invalid order ID' }); + return; + } + if (!allowedStatuses.includes(status)) { + res.status(400).json({ error: 'Invalid order status' }); + return; + } const order = await prisma.order.update({ - where: { id: parseInt(id) }, + where: { id: orderId }, data: { status } }); res.json(order); @@ -181,10 +245,15 @@ app.patch('/api/orders/:id/status', async (req, res) => { }); // Delete an order -app.delete('/api/orders/:id', async (req, res) => { +app.delete('/api/orders/:id', requireAdmin, async (req, res) => { try { const { id } = req.params; - await prisma.order.delete({ where: { id: parseInt(id) } }); + const orderId = parseId(id); + if (orderId === null) { + res.status(400).json({ error: 'Invalid order ID' }); + return; + } + await prisma.order.delete({ where: { id: orderId } }); res.sendStatus(200); } catch (error) { console.error(error); @@ -192,21 +261,34 @@ app.delete('/api/orders/:id', async (req, res) => { } }); -// Clear all orders -app.delete('/api/orders', async (req, res) => { +// Start a fresh cooking batch while preserving the menu and site settings +app.post('/api/batches/reset', requireAdmin, async (req, res) => { try { - await prisma.order.deleteMany(); - res.sendStatus(200); + const [clearedOrders, reactivatedItems] = await prisma.$transaction([ + prisma.order.deleteMany(), + prisma.stockItem.updateMany({ + where: { available: false }, + data: { available: true } + }) + ]); + + res.json({ + clearedOrders: clearedOrders.count, + reactivatedItems: reactivatedItems.count + }); } catch (error) { console.error(error); - res.status(500).json({ error: 'Failed to clear orders' }); + res.status(500).json({ error: 'Failed to start a new cooking batch' }); } }); // Update or Create Stock Item -app.post('/api/stock', async (req, res) => { +app.post('/api/stock', requireAdmin, async (req, res) => { try { - const { orderedQty, remainingStock, ...item } = req.body; // strip virtual fields + const { orderedQty, remainingStock, configuredAvailable, ...item } = req.body; // strip virtual fields + if (typeof configuredAvailable === 'boolean') { + item.available = configuredAvailable; + } if (item.id) { const updated = await prisma.stockItem.update({ where: { id: item.id }, @@ -227,10 +309,15 @@ app.post('/api/stock', async (req, res) => { }); // Delete Stock Item -app.delete('/api/stock/:id', async (req, res) => { +app.delete('/api/stock/:id', requireAdmin, async (req, res) => { try { const { id } = req.params; - await prisma.stockItem.delete({ where: { id: parseInt(id) } }); + const stockItemId = parseId(id); + if (stockItemId === null) { + res.status(400).json({ error: 'Invalid stock item ID' }); + return; + } + await prisma.stockItem.delete({ where: { id: stockItemId } }); res.sendStatus(200); } catch (error) { console.error(error); @@ -239,17 +326,34 @@ app.delete('/api/stock/:id', async (req, res) => { }); // Update Config -app.post('/api/config', async (req, res) => { +app.post('/api/config', requireAdmin, async (req, res) => { try { const { adminCode, finalDepositDate, cookDay, payIdInfo, termsOfService, orderingPolicy, depositPercentage } = req.body; const data: any = {}; - if (adminCode !== undefined) data.adminCode = adminCode; + if (adminCode !== undefined) { + if (typeof adminCode !== 'string' || adminCode.length < 4) { + res.status(400).json({ error: 'Admin code must be at least 4 characters' }); + return; + } + data.adminCode = adminCode; + } if (finalDepositDate !== undefined) data.finalDepositDate = finalDepositDate; if (cookDay !== undefined) data.cookDay = cookDay; if (payIdInfo !== undefined) data.payIdInfo = payIdInfo; if (termsOfService !== undefined) data.termsOfService = termsOfService; if (orderingPolicy !== undefined) data.orderingPolicy = orderingPolicy; - if (depositPercentage !== undefined) data.depositPercentage = parseInt(depositPercentage); + if (depositPercentage !== undefined) { + if (depositPercentage === '') { + res.status(400).json({ error: 'Deposit percentage is required' }); + return; + } + const parsedDepositPercentage = Number(depositPercentage); + if (!Number.isInteger(parsedDepositPercentage) || parsedDepositPercentage < 0 || parsedDepositPercentage > 100) { + res.status(400).json({ error: 'Deposit percentage must be a whole number from 0 to 100' }); + return; + } + data.depositPercentage = parsedDepositPercentage; + } // Use upsert to ensure we always use ID 1 await prisma.config.upsert({ @@ -266,7 +370,7 @@ app.post('/api/config', async (req, res) => { }); // Update or Create FAQ Item -app.post('/api/faqs', async (req, res) => { +app.post('/api/faqs', requireAdmin, async (req, res) => { try { const item = req.body; if (item.id) { @@ -289,10 +393,15 @@ app.post('/api/faqs', async (req, res) => { }); // Delete FAQ Item -app.delete('/api/faqs/:id', async (req, res) => { +app.delete('/api/faqs/:id', requireAdmin, async (req, res) => { try { const { id } = req.params; - await prisma.fAQ.delete({ where: { id: parseInt(id) } }); + const faqId = parseId(id); + if (faqId === null) { + res.status(400).json({ error: 'Invalid FAQ ID' }); + return; + } + await prisma.fAQ.delete({ where: { id: faqId } }); res.sendStatus(200); } catch (error) { console.error(error); @@ -300,49 +409,6 @@ app.delete('/api/faqs/:id', async (req, res) => { } }); -// Reset Database to defaults -app.post('/api/reset', async (req, res) => { - try { - await prisma.order.deleteMany(); - await prisma.stockItem.deleteMany(); - await prisma.config.deleteMany(); - await prisma.fAQ.deleteMany(); - - // Seed default admin code and limits with ID 1 - await prisma.config.create({ - data: { - id: 1, - adminCode: '1234', - payIdInfo: 'Your PayID Here', - termsOfService: 'To secure your order, a 30% non-refundable deposit is required upfront before smoking begins. Payments can be made via PayID or Cash. The remaining balance is payable upon pickup.', - orderingPolicy: 'Deposits cover material costs and are final. PayID details provided after ordering.', - depositPercentage: 30 - } - }); - - // Seed some default stock items - await prisma.stockItem.createMany({ - data: [ - { name: 'Beef Brisket', category: 'Beef', price: 45, unit: 'kg', description: 'Low and slow smoked brisket', available: true }, - { name: 'Pork Shoulder', category: 'Pork', price: 35, unit: 'kg', description: 'Pulled pork perfection', available: true } - ] - }); - - // Seed some default FAQs - await prisma.fAQ.createMany({ - data: [ - { question: 'When is pickup?', answer: 'Pickups are usually scheduled for the afternoon of the Cook Day. We will contact you to confirm the exact time.', order: 1 }, - { question: 'How do I pay the deposit?', answer: 'Once you submit your order, you can pay the 30% deposit via PayID or Cash.', order: 2 } - ] - }); - - res.sendStatus(200); - } catch (error) { - console.error(error); - res.status(500).json({ error: 'Failed to reset database' }); - } -}); - // Serve Static Files in Production const frontendPath = fs.existsSync(path.join(__dirname, '../frontend-dist')) ? path.join(__dirname, '../frontend-dist') @@ -354,18 +420,23 @@ app.get(/^\/(?!api).*/, (req, res) => { res.sendFile(path.join(frontendPath, 'index.html')); }); -app.listen(PORT, async () => { - // Ensure a default config exists - const config = await prisma.config.findFirst(); - if (!config) { - await prisma.config.create({ - data: { - id: 1, - adminCode: '1234', - payIdInfo: 'Your PayID Here' - } - }); - console.log('Default config seeded'); - } - console.log(`Server is running on http://localhost:${PORT}`); +const startServer = async () => { + await prisma.config.upsert({ + where: { id: 1 }, + update: {}, + create: { + id: 1, + adminCode: '1234', + payIdInfo: 'Your PayID Here' + } + }); + + app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); + }); +}; + +startServer().catch((error) => { + console.error('Failed to start server:', error); + process.exit(1); }); diff --git a/docker-compose.yml b/docker-compose.yml index 2a7972d..2b44af2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,9 +4,9 @@ services: ports: - "3000:3000" volumes: - - meat-data:/app/backend/prisma + - meat-data:/app/data environment: - - DATABASE_URL=file:./dev.db + - DATABASE_URL=file:/app/data/dev.db restart: always volumes: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a320f5..92c1fbb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { API } from './services/api'; +import { API, ApiError } from './services/api'; import { Icons } from './components/Icons'; import CustomerView from './components/CustomerView'; import AdminDashboard from './components/AdminDashboard'; @@ -21,17 +21,28 @@ function App() { const [confirmModal, setConfirmModal] = useState<{ show: boolean, message: string, onConfirm: (() => void) | null }>({ show: false, message: '', onConfirm: null }); const [pickerItem, setPickerItem] = useState(null); + const showNotification = (msg: string) => { + setNotification(msg); + setTimeout(() => setNotification(null), 3000); + }; + const refreshData = async () => { try { const data = await API.fetchData(); - setStock(data.stock); - setOrders(data.orders.sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())); + setStock(data.stock || []); + setOrders((data.orders || []).sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())); setFaqs(data.faqs || []); if (data.config) { setConfig(data.config); } } catch (err) { console.error("Server connection failed.", err); + if (err instanceof ApiError && err.status === 401) { + API.logout(); + setOrders([]); + setView('customer'); + showNotification('Admin session expired. Please log in again.'); + } } }; @@ -49,18 +60,18 @@ function App() { clearInterval(intervalId); }; }, []); - const showNotification = (msg: string) => { - setNotification(msg); - setTimeout(() => setNotification(null), 3000); - }; - - const promptConfirm = (message: string, action: () => void) => { + const promptConfirm = (message: string, action: () => void | Promise) => { setConfirmModal({ show: true, message, onConfirm: async () => { - await action(); - setConfirmModal({ show: false, message: '', onConfirm: null }); + try { + await action(); + } catch (error) { + showNotification(error instanceof Error ? error.message : 'Action failed'); + } finally { + setConfirmModal({ show: false, message: '', onConfirm: null }); + } } }); }; @@ -128,9 +139,10 @@ function App() { await API.login(adminCodeInput); setView('admin'); setShowAdminLogin(false); + await refreshData(); setAdminCodeInput(''); showNotification("Logged in as Admin"); - } catch (err) { + } catch { showNotification("Incorrect Admin Code"); } }; @@ -159,7 +171,7 @@ function App() { )} )} - diff --git a/frontend/src/components/AdminDashboard.tsx b/frontend/src/components/AdminDashboard.tsx index f1bb270..a178367 100644 --- a/frontend/src/components/AdminDashboard.tsx +++ b/frontend/src/components/AdminDashboard.tsx @@ -7,6 +7,10 @@ export default function AdminDashboard({ stock, orders, config, faqs, refreshDat const [newAdminCode, setNewAdminCode] = useState(''); const [settingsForm, setSettingsForm] = useState(null); + const notifyError = (error: unknown) => { + notify(error instanceof Error ? error.message : "Action failed"); + }; + useEffect(() => { if (config && !settingsForm) { setSettingsForm({ @@ -15,17 +19,21 @@ export default function AdminDashboard({ stock, orders, config, faqs, refreshDat payIdInfo: config.payIdInfo || '', termsOfService: config.termsOfService || '', orderingPolicy: config.orderingPolicy || '', - depositPercentage: config.depositPercentage || 30 + depositPercentage: config.depositPercentage ?? 30 }); } }, [config, settingsForm]); const handleSaveSettings = async () => { if (!settingsForm) return; - await API.updateConfig(settingsForm); - notify("Settings Saved"); - setSettingsForm(null); - refreshData(); + try { + await API.updateConfig(settingsForm); + notify("Settings Saved"); + setSettingsForm(null); + refreshData(); + } catch (error) { + notifyError(error); + } }; const totalRevenue = orders.reduce((acc: number, order: any) => acc + (order.estimatedTotal || 0), 0); @@ -37,10 +45,15 @@ export default function AdminDashboard({ stock, orders, config, faqs, refreshDat notify("Code must be at least 4 characters"); return; } - await API.updateConfig({ adminCode: newAdminCode }); - setNewAdminCode(''); - notify("Admin Password Updated"); - refreshData(); + try { + await API.updateConfig({ adminCode: newAdminCode }); + API.setAdminCode(newAdminCode); + setNewAdminCode(''); + notify("Admin Password Updated"); + refreshData(); + } catch (error) { + notifyError(error); + } }; @@ -55,16 +68,30 @@ export default function AdminDashboard({ stock, orders, config, faqs, refreshDat bulk1Threshold: 0, bulk1Price: 0, bulk2Threshold: 0, bulk2Price: 0 }; - await API.updateStock(newItem); - refreshData(); - notify("New Item Added"); + try { + await API.updateStock(newItem); + refreshData(); + notify("New Item Added"); + } catch (error) { + notifyError(error); + } }; const updateStockItem = async (id: number, updates: any) => { const currentItem = stock.find((s: any) => s.id === id) || { id }; - await API.updateStock({ ...currentItem, ...updates }); - refreshData(); - notify("Stock updated"); + const configuredAvailable = updates.available ?? currentItem.configuredAvailable ?? currentItem.available; + try { + await API.updateStock({ + ...currentItem, + ...updates, + available: configuredAvailable, + configuredAvailable + }); + refreshData(); + notify("Stock updated"); + } catch (error) { + notifyError(error); + } }; const deleteStockItem = (id: number) => { @@ -81,16 +108,24 @@ export default function AdminDashboard({ stock, orders, config, faqs, refreshDat answer: "Answer here...", order: faqs.length + 1 }; - await API.updateFAQ(newFAQ); - refreshData(); - notify("New FAQ Added"); + try { + await API.updateFAQ(newFAQ); + refreshData(); + notify("New FAQ Added"); + } catch (error) { + notifyError(error); + } }; const updateFAQ = async (id: number, updates: any) => { const currentFAQ = faqs.find((f: any) => f.id === id) || { id }; - await API.updateFAQ({ ...currentFAQ, ...updates }); - refreshData(); - notify("FAQ updated"); + try { + await API.updateFAQ({ ...currentFAQ, ...updates }); + refreshData(); + notify("FAQ updated"); + } catch (error) { + notifyError(error); + } }; const deleteFAQ = (id: number) => { @@ -101,19 +136,11 @@ export default function AdminDashboard({ stock, orders, config, faqs, refreshDat }); }; - const clearAllOrders = () => { - promptConfirm("WARNING: This will delete ALL current order history. Proceed?", async () => { - await API.clearOrders(); + const resetBatch = () => { + promptConfirm("Start a fresh cooking batch? This clears current orders and reactivates every inventory item, while preserving your menu and settings.", async () => { + await API.resetBatch(); refreshData(); - notify("All Orders Cleared"); - }); - }; - - const resetDatabase = () => { - promptConfirm("Wipe all data and reset to defaults?", async () => { - await API.resetDB(); - refreshData(); - notify("Database Reset to Defaults"); + notify("Fresh Cooking Batch Started"); }); }; @@ -198,7 +225,7 @@ export default function AdminDashboard({ stock, orders, config, faqs, refreshDat