Feature: Add configurable total and itemized order limits, and fix settings input auto-reset bug
This commit is contained in:
1 parent
b482331500
commit
8e55716a94
5 files changed
+169
-48
No files matched your search
@@ -42,4 +42,6 @@ model Config {
|
|||||||
finalDepositDate String?
|
finalDepositDate String?
|
||||||
cookDay String?
|
cookDay String?
|
||||||
payIdInfo String? @default("Your PayID Here")
|
payIdInfo String? @default("Your PayID Here")
|
||||||
|
maxTotalWeight Float? @default(100.0)
|
||||||
|
maxItemWeight Float? @default(50.0)
|
||||||
}
|
}
|
||||||
@@ -138,13 +138,15 @@ app.delete('/api/stock/:id', async (req, res) => {
|
|||||||
// Update Config
|
// Update Config
|
||||||
app.post('/api/config', async (req, res) => {
|
app.post('/api/config', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { adminCode, finalDepositDate, cookDay, payIdInfo } = req.body;
|
const { adminCode, finalDepositDate, cookDay, payIdInfo, maxTotalWeight, maxItemWeight } = req.body;
|
||||||
const config = await prisma.config.findFirst();
|
const config = await prisma.config.findFirst();
|
||||||
const data: any = {};
|
const data: any = {};
|
||||||
if (adminCode !== undefined) data.adminCode = adminCode;
|
if (adminCode !== undefined) data.adminCode = adminCode;
|
||||||
if (finalDepositDate !== undefined) data.finalDepositDate = finalDepositDate;
|
if (finalDepositDate !== undefined) data.finalDepositDate = finalDepositDate;
|
||||||
if (cookDay !== undefined) data.cookDay = cookDay;
|
if (cookDay !== undefined) data.cookDay = cookDay;
|
||||||
if (payIdInfo !== undefined) data.payIdInfo = payIdInfo;
|
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) {
|
if (config) {
|
||||||
await prisma.config.update({
|
await prisma.config.update({
|
||||||
@@ -168,8 +170,8 @@ app.post('/api/reset', async (req, res) => {
|
|||||||
await prisma.stockItem.deleteMany();
|
await prisma.stockItem.deleteMany();
|
||||||
await prisma.config.deleteMany();
|
await prisma.config.deleteMany();
|
||||||
|
|
||||||
// Seed default admin code
|
// Seed default admin code and limits
|
||||||
await prisma.config.create({ data: { adminCode: '1234' } });
|
await prisma.config.create({ data: { adminCode: '1234', maxTotalWeight: 100.0, maxItemWeight: 50.0 } });
|
||||||
|
|
||||||
// Seed some default stock items
|
// Seed some default stock items
|
||||||
await prisma.stockItem.createMany({
|
await prisma.stockItem.createMany({
|
||||||
|
|||||||
+63
-7
@@ -57,24 +57,80 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addToCart = (item: any) => {
|
const addToCart = (item: any) => {
|
||||||
|
const maxItem = config?.maxItemWeight ?? 50;
|
||||||
|
const maxTotal = config?.maxTotalWeight ?? 100;
|
||||||
|
|
||||||
|
let isAllowed = true;
|
||||||
|
let errorMsg = '';
|
||||||
|
|
||||||
setCart(prev => {
|
setCart(prev => {
|
||||||
const existing = prev.find(i => i.id === item.id);
|
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) {
|
if (existing) {
|
||||||
return prev.map(i => i.id === item.id ? { ...i, qty: i.qty + 1 } : i);
|
return prev.map(i => i.id === item.id ? { ...i, qty: i.qty + 1 } : i);
|
||||||
}
|
}
|
||||||
return [...prev, { ...item, qty: 1 }];
|
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) => {
|
const updateCartQty = (itemId: number, delta: number) => {
|
||||||
setCart(prev => prev.map(i => {
|
const maxItem = config?.maxItemWeight ?? 50;
|
||||||
if (i.id === itemId) {
|
const maxTotal = config?.maxTotalWeight ?? 100;
|
||||||
const newQty = i.qty + delta;
|
|
||||||
return newQty > 0 ? { ...i, qty: newQty } : i;
|
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) => {
|
const removeFromCart = (itemId: number) => {
|
||||||
|
|||||||
@@ -5,6 +5,27 @@ import { API } from '../services/api';
|
|||||||
export default function AdminDashboard({ stock, orders, config, refreshData, notify, promptConfirm }: any) {
|
export default function AdminDashboard({ stock, orders, config, refreshData, notify, promptConfirm }: any) {
|
||||||
const [tab, setTab] = useState<'orders' | 'stock' | 'settings'>('orders');
|
const [tab, setTab] = useState<'orders' | 'stock' | 'settings'>('orders');
|
||||||
const [newAdminCode, setNewAdminCode] = useState('');
|
const [newAdminCode, setNewAdminCode] = useState('');
|
||||||
|
const [settingsForm, setSettingsForm] = useState<any>(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 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);
|
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
|
|||||||
<div className="max-w-md mx-auto text-center pt-10">
|
<div className="max-w-md mx-auto text-center pt-10">
|
||||||
<div className="text-stone-500 mb-4 mx-auto flex justify-center"><Icons.Settings /></div>
|
<div className="text-stone-500 mb-4 mx-auto flex justify-center"><Icons.Settings /></div>
|
||||||
<h3 className="text-xl font-bold text-white mb-6">Admin Settings</h3>
|
<h3 className="text-xl font-bold text-white mb-6">Admin Settings</h3>
|
||||||
<div className="bg-stone-950 p-6 rounded-xl border border-stone-800 mb-8 space-y-6 text-left">
|
{settingsForm ? (
|
||||||
<h4 className="text-sm font-bold text-red-500 uppercase tracking-widest border-b border-stone-800 pb-2">Scheduling & Deadlines</h4>
|
<div className="bg-stone-950 p-6 rounded-xl border border-stone-800 mb-8 space-y-6 text-left">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<h4 className="text-sm font-bold text-red-500 uppercase tracking-widest border-b border-stone-800 pb-2">Scheduling & Deadlines</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">Final Deposit Date</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={settingsForm.finalDepositDate}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">Scheduled Cook Day</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={settingsForm.cookDay}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">Final Deposit Date</label>
|
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">PayID Details</label>
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type="text"
|
||||||
value={config?.finalDepositDate || ''}
|
placeholder="e.g. 0400 000 000 or email@domain.com"
|
||||||
onChange={(e) => API.updateConfig({ finalDepositDate: e.target.value }).then(refreshData)}
|
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"
|
className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">Scheduled Cook Day</label>
|
<h4 className="text-sm font-bold text-red-500 uppercase tracking-widest border-b border-stone-800 pt-4 pb-2">Order Limits</h4>
|
||||||
<input
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
type="date"
|
<div>
|
||||||
value={config?.cookDay || ''}
|
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">Max Total Order (kg)</label>
|
||||||
onChange={(e) => API.updateConfig({ cookDay: e.target.value }).then(refreshData)}
|
<input
|
||||||
className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none"
|
type="number"
|
||||||
/>
|
value={settingsForm.maxTotalWeight}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">Max Per Item (kg)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={settingsForm.maxItemWeight}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSaveSettings}
|
||||||
|
className="w-full bg-red-700 hover:bg-red-600 text-white font-bold py-3.5 rounded-lg transition-colors uppercase tracking-widest text-xs font-black shadow-lg shadow-red-900/20 active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
) : (
|
||||||
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">PayID Details</label>
|
<div className="text-stone-500 py-10 italic">Loading settings...</div>
|
||||||
<input
|
)}
|
||||||
type="text"
|
|
||||||
placeholder="e.g. 0400 000 000 or email@domain.com"
|
|
||||||
value={config?.payIdInfo || ''}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-stone-950 p-6 rounded-xl border border-stone-800 mb-8">
|
<div className="bg-stone-950 p-6 rounded-xl border border-stone-800 mb-8">
|
||||||
<label className="block text-xs font-bold text-stone-500 uppercase mb-2 text-left">Update Admin Password</label>
|
<label className="block text-xs font-bold text-stone-500 uppercase mb-2 text-left">Update Admin Password</label>
|
||||||
@@ -231,18 +285,20 @@ function AdminStockItem({ item, onUpdate, onDelete }: any) {
|
|||||||
const [editData, setEditData] = useState<any>({});
|
const [editData, setEditData] = useState<any>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditData({
|
if (!isEditing) {
|
||||||
name: item.name || "",
|
setEditData({
|
||||||
category: item.category || "",
|
name: item.name || "",
|
||||||
description: item.description || "",
|
category: item.category || "",
|
||||||
price: item.price || 0,
|
description: item.description || "",
|
||||||
unit: item.unit || "kg",
|
price: item.price || 0,
|
||||||
bulk1Threshold: item.bulk1Threshold || 0,
|
unit: item.unit || "kg",
|
||||||
bulk1Price: item.bulk1Price || 0,
|
bulk1Threshold: item.bulk1Threshold || 0,
|
||||||
bulk2Threshold: item.bulk2Threshold || 0,
|
bulk1Price: item.bulk1Price || 0,
|
||||||
bulk2Price: item.bulk2Price || 0
|
bulk2Threshold: item.bulk2Threshold || 0,
|
||||||
});
|
bulk2Price: item.bulk2Price || 0
|
||||||
}, [item]);
|
});
|
||||||
|
}
|
||||||
|
}, [item, isEditing]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
|
|||||||
@@ -167,6 +167,11 @@ export default function CartModal({ cart, onClose, onRemove, onUpdateQty, onSubm
|
|||||||
<div className="text-xs bg-stone-950 p-3 rounded border border-stone-800 text-stone-400">
|
<div className="text-xs bg-stone-950 p-3 rounded border border-stone-800 text-stone-400">
|
||||||
<p className="font-bold text-red-500 mb-1 uppercase tracking-widest">Deposit Required</p>
|
<p className="font-bold text-red-500 mb-1 uppercase tracking-widest">Deposit Required</p>
|
||||||
<p>Order will not be cooked until the 30% deposit is paid. We accept PayID and Cash.</p>
|
<p>Order will not be cooked until the 30% deposit is paid. We accept PayID and Cash.</p>
|
||||||
|
{(config?.maxTotalWeight || config?.maxItemWeight) && (
|
||||||
|
<p className="mt-2 border-t border-stone-850 pt-2 text-[10px] text-stone-500 italic">
|
||||||
|
Limits: Max {config.maxTotalWeight ?? 100}kg total weight | Max {config.maxItemWeight ?? 50}kg per item.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in new issue
Block a user