Files
meat-monster/frontend/src/components/AdminDashboard.tsx
T

378 lines
26 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Icons } from './Icons';
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<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 totalKg = orders.reduce((acc: number, order: any) => acc + (order.totalWeight || 0), 0);
const activeRequests = orders.length;
const handleUpdatePassword = async () => {
if (newAdminCode.length < 4) {
notify("Code must be at least 4 characters");
return;
}
await API.updateConfig({ adminCode: newAdminCode });
setNewAdminCode('');
notify("Admin Password Updated");
refreshData();
};
const addNewStockItem = async () => {
const newItem = {
name: "New Meat Item",
category: "Beef",
price: 20,
unit: "kg",
available: false,
description: "Description here",
bulk1Threshold: 0, bulk1Price: 0,
bulk2Threshold: 0, bulk2Price: 0
};
await API.updateStock(newItem);
refreshData();
notify("New Item Added");
};
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 deleteStockItem = (id: number) => {
promptConfirm("Permanently delete this item from stock?", async () => {
await API.deleteStock(id);
refreshData();
notify("Item deleted");
});
};
const clearAllOrders = () => {
promptConfirm("WARNING: This will delete ALL current order history. Proceed?", async () => {
await API.clearOrders();
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");
});
};
return (
<div className="bg-stone-900 rounded-xl border border-stone-800 overflow-hidden min-h-[600px] shadow-2xl">
<div className="flex border-b border-stone-800 bg-stone-950">
<button onClick={() => setTab('orders')} className={`flex-1 py-4 font-bold text-center transition-colors uppercase tracking-wider text-sm ${tab === 'orders' ? 'bg-stone-900 text-red-500 border-t-2 border-red-500' : 'text-stone-600 hover:text-stone-300'}`}>Requests</button>
<button onClick={() => setTab('stock')} className={`flex-1 py-4 font-bold text-center transition-colors uppercase tracking-wider text-sm ${tab === 'stock' ? 'bg-stone-900 text-red-500 border-t-2 border-red-500' : 'text-stone-600 hover:text-stone-300'}`}>Inventory</button>
<button onClick={() => setTab('settings')} className={`flex-1 py-4 font-bold text-center transition-colors uppercase tracking-wider text-sm ${tab === 'settings' ? 'bg-stone-900 text-red-500 border-t-2 border-red-500' : 'text-stone-600 hover:text-stone-300'}`}>Settings</button>
</div>
<div className="p-6">
{tab === 'stock' && (
<div className="space-y-6">
<div className="flex justify-between items-end border-b border-stone-800 pb-4">
<h3 className="text-xl font-bold text-white">Stock Configuration</h3>
<p className="text-xs text-stone-500">Edit names, prices, and descriptions</p>
</div>
<div className="grid gap-4">
{stock.map((item: any) => (
<AdminStockItem key={item.id} item={item} onUpdate={(data: any) => updateStockItem(item.id, data)} onDelete={() => deleteStockItem(item.id)} />
))}
</div>
<div className="flex justify-between items-center pt-10 border-t border-stone-800 mt-10">
<button onClick={addNewStockItem} className="bg-stone-800 hover:bg-stone-700 text-white px-4 py-2 rounded font-bold flex items-center gap-2 border border-stone-700"><Icons.Plus /> Add New Stock Item</button>
</div>
</div>
)}
{tab === 'orders' && (
<div className="space-y-6">
<div className="grid grid-cols-3 gap-4 mb-8">
<div className="bg-[#0c0a09] p-4 rounded border border-stone-800 text-center shadow-lg">
<p className="text-stone-500 text-[10px] md:text-xs uppercase font-bold tracking-widest mb-1">Est. Revenue</p>
<p className="text-2xl md:text-3xl font-black text-green-500">${totalRevenue.toFixed(2)}</p>
</div>
<div className="bg-[#0c0a09] p-4 rounded border border-stone-800 text-center shadow-lg">
<p className="text-stone-500 text-[10px] md:text-xs uppercase font-bold tracking-widest mb-1">Total Prep (kg)</p>
<p className="text-2xl md:text-3xl font-black text-stone-200">{totalKg}<span className="text-lg text-stone-600 ml-1">kg</span></p>
</div>
<div className="bg-[#0c0a09] p-4 rounded border border-stone-800 text-center shadow-lg">
<p className="text-stone-500 text-[10px] md:text-xs uppercase font-bold tracking-widest mb-1">Requests</p>
<p className="text-2xl md:text-3xl font-black text-red-500">{activeRequests}</p>
</div>
</div>
<h3 className="text-xl font-bold text-white mb-4 border-b border-stone-800 pb-2">Incoming List</h3>
{orders.length === 0 ? (
<div className="text-center py-10 bg-stone-950 rounded border border-dashed border-stone-800">
<p className="text-stone-500 italic">No active orders. Time to smoke some meat!</p>
</div>
) : (
<div className="space-y-4">
{orders.map((order: any) => (
<div key={order.id} className="bg-stone-950 p-5 rounded-lg border border-stone-800 relative group hover:border-stone-700 transition-colors shadow-md">
<div className="flex justify-between items-start mb-4">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-[10px] font-black bg-red-900/30 text-red-500 px-2 py-0.5 rounded border border-red-900/50 uppercase tracking-tighter">{order.orderNumber || `#${order.id}`}</span>
<h4 className="font-bold text-lg text-white">{order.name}</h4>
</div>
<p className="text-sm text-stone-300 font-mono">{order.phone}</p>
</div>
<div className="flex gap-2">
<select
value={order.status || 'Pending'}
onChange={(e) => API.updateOrderStatus(order.id, e.target.value).then(refreshData)}
className="bg-stone-900 border border-stone-800 rounded px-2 py-1 text-xs text-stone-400 focus:border-red-600 outline-none"
>
<option value="Pending">Pending</option>
<option value="Deposit Paid">Deposit Paid</option>
<option value="Paid">Paid</option>
<option value="Cooked">Cooked</option>
<option value="Completed">Completed</option>
<option value="Cancelled">Cancelled</option>
</select>
<button onClick={() => promptConfirm(`Delete order ${order.orderNumber || order.name}?`, () => API.deleteOrder(order.id).then(refreshData))} className="p-2 text-stone-600 hover:text-red-500 transition-colors bg-stone-900 rounded"><Icons.Trash /></button>
</div>
</div>
<div className="bg-stone-900 rounded p-3 border border-stone-800/50 mb-4">
{order.items.map((item: any, idx: number) => (
<div key={idx} className="flex justify-between text-sm py-1 border-b border-stone-800/30 last:border-0">
<span className="text-stone-300">{item.qty}{item.unit} {item.name}</span>
<span className="text-stone-500 font-mono">${(item.lineTotal || 0).toFixed(2)}</span>
</div>
))}
</div>
<div className="flex justify-between items-end">
<div className="text-[10px] text-stone-500 space-y-1">
<p className="flex items-center gap-1 uppercase font-bold"><Icons.Clock /> {new Date(order.timestamp).toLocaleString('en-AU')}</p>
<p className="flex items-center gap-1 uppercase font-bold"><Icons.ShoppingCart /> {order.totalWeight}kg Weight</p>
</div>
<div className="text-right">
<p className="text-[10px] text-stone-500 uppercase font-bold">Estimated Total</p>
<p className="text-xl font-black text-red-500">${order.estimatedTotal?.toFixed(2)}</p>
<p className="text-[10px] text-red-800 font-bold uppercase tracking-tighter">30% Deposit: ${(order.estimatedTotal * 0.3).toFixed(2)}</p>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{tab === 'settings' && (
<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>
<h3 className="text-xl font-bold text-white mb-6">Admin Settings</h3>
{settingsForm ? (
<div className="bg-stone-950 p-6 rounded-xl border border-stone-800 mb-8 space-y-6 text-left">
<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>
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">PayID Details</label>
<input
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"
/>
</div>
<h4 className="text-sm font-bold text-red-500 uppercase tracking-widest border-b border-stone-800 pt-4 pb-2">Order Limits</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">Max Total Order (kg)</label>
<input
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>
<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 className="text-stone-500 py-10 italic">Loading settings...</div>
)}
<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>
<input type="text" placeholder="New Code" value={newAdminCode} onChange={(e) => setNewAdminCode(e.target.value)} className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white mb-4 focus:border-red-600 outline-none" />
<button onClick={handleUpdatePassword} disabled={!newAdminCode} className="w-full bg-red-700 hover:bg-red-600 disabled:bg-stone-800 disabled:text-stone-600 text-white font-bold py-3 rounded transition-colors">Update Password</button>
</div>
<div className="space-y-4 border-t border-stone-800 pt-8">
<p className="text-xs font-bold text-stone-500 uppercase">Danger Zone</p>
<button onClick={clearAllOrders} className="w-full py-3 border border-red-900/50 text-red-500 hover:bg-red-900/20 rounded text-sm font-bold transition-colors">Clear All Order History</button>
<button onClick={resetDatabase} className="text-xs text-stone-600 hover:text-red-500 underline block w-full">Reset Entire Database to Defaults</button>
</div>
</div>
)}
</div>
</div>
);
}
function AdminStockItem({ item, onUpdate, onDelete }: any) {
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState<any>({});
useEffect(() => {
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({
...editData,
price: parseFloat(editData.price),
bulk1Threshold: parseFloat(editData.bulk1Threshold),
bulk1Price: parseFloat(editData.bulk1Price),
bulk2Threshold: parseFloat(editData.bulk2Threshold),
bulk2Price: parseFloat(editData.bulk2Price)
});
setIsEditing(false);
};
return (
<div className="bg-stone-950 p-4 rounded-lg border border-stone-800 flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
{!isEditing ? (
<div className="flex-1">
<p className="font-bold text-white text-lg">{item.name}</p>
<p className="text-xs text-stone-500 italic">{item.description}</p>
</div>
) : (
<div className="flex-1 flex flex-col gap-2 mr-4">
<input type="text" value={editData.name} onChange={e => setEditData({ ...editData, name: e.target.value })} className="bg-black text-white border border-stone-700 rounded px-2 py-1 font-bold w-full" placeholder="Item Name" />
<textarea value={editData.description} onChange={e => setEditData({ ...editData, description: e.target.value })} className="bg-black text-stone-400 border border-stone-700 rounded px-2 py-1 text-xs w-full h-16" placeholder="Description" />
</div>
)}
{!isEditing && <span className={`text-[10px] px-1.5 py-0.5 rounded font-bold uppercase ${item.available ? 'bg-green-900 text-green-400' : 'bg-red-900 text-red-400'}`}>{item.available ? 'Active' : 'Disabled'}</span>}
</div>
<div className="flex items-center gap-2">
{isEditing ? (
<>
<button onClick={handleSave} className="p-2 bg-green-600 hover:bg-green-500 text-white rounded"><Icons.Save /></button>
<button onClick={onDelete} className="p-2 bg-red-900 hover:bg-red-700 text-white rounded"><Icons.Trash /></button>
</>
) : (
<button onClick={() => setIsEditing(true)} className="p-2 bg-stone-800 hover:bg-stone-700 text-stone-400 hover:text-white rounded"><Icons.Edit /></button>
)}
<button onClick={() => onUpdate({ available: !item.available })} className={`px-3 py-2 rounded font-bold text-xs uppercase w-24 text-center ${item.available ? 'bg-stone-800 text-red-400 border border-red-900/30 hover:bg-red-900/20' : 'bg-green-900/20 text-green-400 border border-green-900/30 hover:bg-green-900/40'}`}>{item.available ? 'Sold Out' : 'Activate'}</button>
</div>
</div>
{!isEditing ? (
<div className="text-sm text-stone-500 flex flex-wrap gap-4">
<span className="bg-stone-900 px-2 py-1 rounded">Standard: <b className="text-stone-300">${item.price}/{item.unit}</b></span>
{item.bulk1Threshold > 0 && <span className="bg-stone-900 px-2 py-1 rounded text-yellow-600">Tier 1: <b className="text-yellow-500">${item.bulk1Price}</b> @ {item.bulk1Threshold}{item.unit}+</span>}
{item.bulk2Threshold > 0 && <span className="bg-stone-900 px-2 py-1 rounded text-green-600">Tier 2: <b className="text-green-500">${item.bulk2Price}</b> @ {item.bulk2Threshold}{item.unit}+</span>}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 bg-stone-900 p-3 rounded border border-stone-700">
<div className="space-y-2">
<p className="text-xs font-bold text-stone-400 uppercase">Base Info</p>
<label className="block text-[10px] text-stone-500">Price ($)</label>
<input type="number" value={editData.price} onChange={e => setEditData({ ...editData, price: e.target.value })} className="w-full bg-black text-white border border-stone-700 rounded px-2 py-1 text-sm" />
<label className="block text-[10px] text-stone-500">Unit (kg/ea)</label>
<input type="text" value={editData.unit} onChange={e => setEditData({ ...editData, unit: e.target.value })} className="w-full bg-black text-white border border-stone-700 rounded px-2 py-1 text-sm" />
</div>
<div className="space-y-2 border-l border-stone-800 pl-4">
<p className="text-xs font-bold text-yellow-500 uppercase">Tier 1 Bulk</p>
<label className="block text-[10px] text-stone-500">Threshold ({editData.unit})</label>
<input type="number" value={editData.bulk1Threshold} onChange={e => setEditData({ ...editData, bulk1Threshold: e.target.value })} className="w-full bg-black text-white border border-stone-700 rounded px-2 py-1 text-sm" />
<label className="block text-[10px] text-stone-500">Price ($)</label>
<input type="number" value={editData.bulk1Price} onChange={e => setEditData({ ...editData, bulk1Price: e.target.value })} className="w-full bg-black text-white border border-stone-700 rounded px-2 py-1 text-sm" />
</div>
<div className="space-y-2 border-l border-stone-800 pl-4">
<p className="text-xs font-bold text-green-500 uppercase">Tier 2 Super Bulk</p>
<label className="block text-[10px] text-stone-500">Threshold ({editData.unit})</label>
<input type="number" value={editData.bulk2Threshold} onChange={e => setEditData({ ...editData, bulk2Threshold: e.target.value })} className="w-full bg-black text-white border border-stone-700 rounded px-2 py-1 text-sm" />
<label className="block text-[10px] text-stone-500">Price ($)</label>
<input type="number" value={editData.bulk2Price} onChange={e => setEditData({ ...editData, bulk2Price: e.target.value })} className="w-full bg-black text-white border border-stone-700 rounded px-2 py-1 text-sm" />
</div>
</div>
)}
</div>
);
}