diff --git a/backend/src/server.ts b/backend/src/server.ts index 95dc9c9..57f7298 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -38,11 +38,12 @@ app.get('/api/data', async (req, res) => { for (const item of stock) { if (item.maxStock !== null && item.maxStock !== undefined) { const ordered = orderedQtyById[item.id] || 0; - const shouldBeAvailable = ordered < item.maxStock; - if (item.available !== shouldBeAvailable) { + 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: shouldBeAvailable } + data: { available: false } }); } } @@ -60,38 +61,101 @@ app.get('/api/data', async (req, res) => { : null })); - res.json({ stock: stockWithRemaining, orders: parsedOrders, config }); + // Security: Don't send the admin code to the client + const configResponse = config ? { ...config, adminCode: undefined } : null; + + res.json({ stock: stockWithRemaining, orders: parsedOrders, config: configResponse }); } catch (error) { console.error(error); res.status(500).json({ error: 'Failed to fetch data' }); } }); +// Admin Login +app.post('/api/login', async (req, res) => { + try { + const { code } = req.body; + const config = await prisma.config.findFirst(); + if (config && config.adminCode === code) { + res.json({ success: true }); + } else { + res.status(401).json({ error: 'Invalid admin code' }); + } + } catch (error) { + res.status(500).json({ error: 'Login failed' }); + } +}); + // Submit a new order app.post('/api/orders', async (req, res) => { try { const { name, phone, items, totalWeight, estimatedTotal } = req.body; - // Generate simple order number - const lastOrder = await prisma.order.findFirst({ orderBy: { id: 'desc' } }); - const nextId = (lastOrder?.id || 0) + 1; - const orderNumber = `#${nextId + 1000}`; + // 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); + } + } + let computedTotalWeight = 0; + let computedEstimatedTotal = 0; + + 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.`); + } + } + + // 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 finalPrice = getPrice(dbItem, item.qty); + item.finalPricePerUnit = finalPrice; + item.lineTotal = finalPrice * item.qty; + + computedTotalWeight += item.qty; + computedEstimatedTotal += item.lineTotal; + } + + // 2. Create the order const order = await prisma.order.create({ data: { - orderNumber, name, phone, - totalWeight, - estimatedTotal, + totalWeight: computedTotalWeight, + estimatedTotal: computedEstimatedTotal, items: JSON.stringify(items), status: 'Pending' } }); - res.json(order); - } catch (error) { + + // 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 } + }); + + res.json(updatedOrder); + } catch (error: any) { console.error(error); - res.status(500).json({ error: 'Failed to submit order' }); + res.status(400).json({ error: error.message || 'Failed to submit order' }); } }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4cb8122..56141f1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,24 +16,16 @@ function App() { const [showAdminLogin, setShowAdminLogin] = useState(false); const [adminCodeInput, setAdminCodeInput] = useState(''); const [notification, setNotification] = useState(null); - const [adminCode, setAdminCode] = useState('1234'); const [config, setConfig] = useState(null); const [confirmModal, setConfirmModal] = useState<{ show: boolean, message: string, onConfirm: (() => void) | null }>({ show: false, message: '', onConfirm: null }); const [pickerItem, setPickerItem] = useState(null); - useEffect(() => { - refreshData(); - const intervalId = setInterval(refreshData, 5000); - return () => clearInterval(intervalId); - }, []); - 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())); if (data.config) { - setAdminCode(data.config.adminCode || '1234'); setConfig(data.config); } setIsLoading(false); @@ -42,6 +34,11 @@ function App() { } }; + useEffect(() => { + refreshData(); + const intervalId = setInterval(refreshData, 5000); + return () => clearInterval(intervalId); + }, []); const showNotification = (msg: string) => { setNotification(msg); setTimeout(() => setNotification(null), 3000); @@ -110,19 +107,20 @@ function App() { refreshData(); showNotification("Order Request Sent!"); return order; - } catch (error) { - showNotification("Error submitting order."); + } catch (error: any) { + showNotification(error.message || "Error submitting order."); throw error; } }; - const handleAdminLogin = () => { - if (adminCodeInput === adminCode) { + const handleAdminLogin = async () => { + try { + await API.login(adminCodeInput); setView('admin'); setShowAdminLogin(false); setAdminCodeInput(''); showNotification("Logged in as Admin"); - } else { + } catch (err) { showNotification("Incorrect Admin Code"); } }; diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 1efda57..c9abeb5 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -36,5 +36,14 @@ export const API = { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg) }), - resetDB: () => fetch(`${API_BASE}/api/reset`, { method: 'POST' }) + resetDB: () => fetch(`${API_BASE}/api/reset`, { method: 'POST' }), + login: async (code: string) => { + const res = await fetch(`${API_BASE}/api/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code }) + }); + if (!res.ok) throw new Error('Invalid code'); + return res.json(); + } };