Files
meat-monster/backend/src/server.ts
T

305 lines
9.2 KiB
TypeScript

import express from 'express';
import cors from 'cors';
import { PrismaClient } from '@prisma/client';
import path from 'path';
import fs from 'fs';
const prisma = new PrismaClient();
const app = express();
const PORT = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// API Routes
// Fetch all data (Stock, Orders, Config)
app.get('/api/data', async (req, res) => {
try {
let stock = await prisma.stockItem.findMany();
const orders = await prisma.order.findMany();
const config = await prisma.config.findFirst();
// Parse order items from string to JSON
const parsedOrders = orders.map(order => ({
...order,
items: JSON.parse(order.items)
}));
// Tally total ordered kg per stock item across all orders
const orderedQtyById: Record<number, number> = {};
for (const order of parsedOrders) {
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 }
});
}
}
}
// 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
}));
// 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;
// 1. Validate Stock Availability
const stockItems = await prisma.stockItem.findMany();
const allOrders = await prisma.order.findMany();
const orderedQtyById: Record<number, number> = {};
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: {
name,
phone,
totalWeight: computedTotalWeight,
estimatedTotal: computedEstimatedTotal,
items: JSON.stringify(items),
status: 'Pending'
}
});
// 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(400).json({ error: error.message || 'Failed to submit order' });
}
});
// Update Order Status
app.patch('/api/orders/:id/status', async (req, res) => {
try {
const { id } = req.params;
const { status } = req.body;
const order = await prisma.order.update({
where: { id: parseInt(id) },
data: { status }
});
res.json(order);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to update order status' });
}
});
// Delete an order
app.delete('/api/orders/:id', async (req, res) => {
try {
const { id } = req.params;
await prisma.order.delete({ where: { id: parseInt(id) } });
res.sendStatus(200);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to delete order' });
}
});
// Clear all orders
app.delete('/api/orders', async (req, res) => {
try {
await prisma.order.deleteMany();
res.sendStatus(200);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to clear orders' });
}
});
// Update or Create Stock Item
app.post('/api/stock', async (req, res) => {
try {
const { orderedQty, remainingStock, ...item } = req.body; // strip virtual fields
if (item.id) {
const updated = await prisma.stockItem.update({
where: { id: item.id },
data: {
...item,
id: undefined // Don't update ID
}
});
res.json(updated);
} else {
const created = await prisma.stockItem.create({ data: item });
res.json(created);
}
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to update stock' });
}
});
// Delete Stock Item
app.delete('/api/stock/:id', async (req, res) => {
try {
const { id } = req.params;
await prisma.stockItem.delete({ where: { id: parseInt(id) } });
res.sendStatus(200);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to delete stock item' });
}
});
// Update Config
app.post('/api/config', async (req, res) => {
try {
const { adminCode, finalDepositDate, cookDay, payIdInfo } = req.body;
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;
// Use upsert to ensure we always use ID 1
await prisma.config.upsert({
where: { id: 1 },
update: data,
create: { ...data, id: 1 }
});
res.sendStatus(200);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to update config' });
}
});
// Reset Database to defaults
app.post('/api/reset', async (req, res) => {
try {
await prisma.order.deleteMany();
await prisma.stockItem.deleteMany();
await prisma.config.deleteMany();
// Seed default admin code and limits with ID 1
await prisma.config.create({
data: {
id: 1,
adminCode: '1234',
payIdInfo: 'Your PayID Here'
}
});
// 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 }
]
});
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')
: path.join(__dirname, '../../frontend/dist');
app.use(express.static(frontendPath));
app.get(/^\/(?!api).*/, (req, res) => {
res.sendFile(path.join(frontendPath, 'index.html'));
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});