Cohere_asr / RestoPOS.jsx
mohdasif81's picture
Upload folder using huggingface_hub
1117853 verified
// ============================================================
// RESTOFLO — Full Restaurant Management System
// Real-time persistent storage | Odoo-style | All modules
// ============================================================
import { useState, useEffect, useCallback, useRef } from "react";
// ── CONSTANTS ────────────────────────────────────────────────
const STORAGE_KEY = "restoflo_v3";
const POLL_INTERVAL = 3000;
const MODULES = [
{ id: "dashboard", label: "Dashboard", icon: "⬛", group: "Main" },
{ id: "pos", label: "Point of Sale", icon: "🧾", group: "Main" },
{ id: "tables", label: "Table Map", icon: "🪑", group: "Main" },
{ id: "kitchen", label: "Kitchen / KOT", icon: "🍳", group: "Main" },
{ id: "orders", label: "Orders", icon: "📋", group: "Operations" },
{ id: "billing", label: "Billing", icon: "💳", group: "Operations" },
{ id: "menu", label: "Menu Items", icon: "🍽", group: "Operations" },
{ id: "categories", label: "Categories", icon: "🏷", group: "Operations" },
{ id: "inventory", label: "Inventory", icon: "📦", group: "Operations" },
{ id: "reports", label: "Reports", icon: "📊", group: "Finance" },
{ id: "accounting", label: "Accounting", icon: "📒", group: "Finance" },
{ id: "expenses", label: "Expenses", icon: "💸", group: "Finance" },
{ id: "payroll", label: "Payroll", icon: "💰", group: "Finance" },
{ id: "crm", label: "CRM / Customers", icon: "👤", group: "Sales" },
{ id: "subscriptions", label: "Subscriptions", icon: "🔄", group: "Sales" },
{ id: "ecommerce", label: "eCommerce", icon: "🛒", group: "Sales" },
{ id: "website", label: "Website Builder", icon: "🌐", group: "Websites" },
{ id: "livechat", label: "Live Chat", icon: "💬", group: "Websites" },
{ id: "elearning", label: "eLearning", icon: "🎓", group: "Websites" },
{ id: "employees", label: "Employees", icon: "🧑‍💼", group: "HR" },
{ id: "recruitment", label: "Recruitment", icon: "📣", group: "HR" },
{ id: "timeoff", label: "Time Off", icon: "🏖", group: "HR" },
{ id: "appraisals", label: "Appraisals", icon: "⭐", group: "HR" },
{ id: "fleet", label: "Fleet", icon: "🚗", group: "HR" },
{ id: "marketing", label: "Email Marketing", icon: "📧", group: "Marketing" },
{ id: "sms", label: "SMS Marketing", icon: "📱", group: "Marketing" },
{ id: "events", label: "Events", icon: "🎫", group: "Marketing" },
{ id: "surveys", label: "Surveys", icon: "📝", group: "Marketing" },
{ id: "project", label: "Projects", icon: "🗂", group: "Services" },
{ id: "helpdesk", label: "Helpdesk", icon: "🎫", group: "Services" },
{ id: "appointments", label: "Appointments", icon: "📅", group: "Services" },
{ id: "purchase", label: "Purchase", icon: "🛍", group: "Supply Chain" },
{ id: "quality", label: "Quality Control", icon: "✅", group: "Supply Chain" },
{ id: "maintenance", label: "Maintenance", icon: "🔧", group: "Supply Chain" },
{ id: "discuss", label: "Discuss", icon: "💭", group: "Productivity" },
{ id: "calendar", label: "Calendar", icon: "📆", group: "Productivity" },
{ id: "users", label: "Users & Staff", icon: "👥", group: "Admin" },
{ id: "access", label: "Access Control", icon: "🔐", group: "Admin" },
{ id: "settings", label: "Settings", icon: "⚙️", group: "Admin" },
];
const ADMIN_MODULES = MODULES.map(m => m.id);
const INITIAL_PERMS = ["pos", "tables", "kitchen", "orders", "menu"];
const COLORS = ["#875BF7", "#039855", "#EF4444", "#F59E0B", "#3B82F6", "#EC4899", "#14B8A6", "#8B5CF6"];
function initDB() {
return {
users: [
{ id: "admin", name: "Admin User", role: "admin", pass: "admin123", email: "admin@restoflo.com", phone: "", color: "#875BF7", permissions: ADMIN_MODULES, active: true, pin: "0000" },
{ id: "waiter1", name: "Ravi Kumar", role: "waiter", pass: "pass123", email: "ravi@restoflo.com", phone: "+91 98100 11111", color: "#3B82F6", permissions: INITIAL_PERMS, active: true, pin: "1111" },
{ id: "waiter2", name: "Priya Sharma", role: "waiter", pass: "pass456", email: "priya@restoflo.com", phone: "+91 98100 22222", color: "#039855", permissions: ["pos","tables","kitchen"], active: true, pin: "2222" },
{ id: "cashier1", name: "Arun Singh", role: "cashier", pass: "cash789", email: "arun@restoflo.com", phone: "+91 98100 33333", color: "#F59E0B", permissions: ["billing","orders","reports","accounting"], active: true, pin: "3333" },
{ id: "manager1", name: "Sunita Reddy", role: "manager", pass: "mgr456", email: "sunita@restoflo.com", phone: "+91 98100 44444", color: "#EC4899", permissions: ["dashboard","pos","tables","kitchen","orders","billing","menu","categories","inventory","reports","crm","employees"], active: true, pin: "4444" },
],
categories: [
{ id: "starter", name: "Starters", icon: "🥗", color: "#039855", sortOrder: 1 },
{ id: "main", name: "Main Course", icon: "🍛", color: "#F59E0B", sortOrder: 2 },
{ id: "biryani", name: "Biryani", icon: "🍚", color: "#875BF7", sortOrder: 3 },
{ id: "breads", name: "Breads", icon: "🫓", color: "#EF4444", sortOrder: 4 },
{ id: "drinks", name: "Drinks", icon: "🥤", color: "#3B82F6", sortOrder: 5 },
{ id: "desserts",name: "Desserts", icon: "🍰", color: "#EC4899", sortOrder: 6 },
],
menuItems: [
{ id: 1, name: "Chicken Tikka", cat: "starter", price: 280, tax: 5, type: "nonveg", emoji: "🍗", desc: "Tender marinated chicken grilled in tandoor", status: "active", cost: 120, stock: 999 },
{ id: 2, name: "Paneer Tikka", cat: "starter", price: 220, tax: 5, type: "veg", emoji: "🧀", desc: "Cottage cheese marinated and grilled", status: "active", cost: 90, stock: 999 },
{ id: 3, name: "Veg Spring Roll", cat: "starter", price: 160, tax: 5, type: "veg", emoji: "🥗", desc: "Crispy rolls with fresh vegetables", status: "active", cost: 60, stock: 999 },
{ id: 4, name: "Crispy Prawns", cat: "starter", price: 380, tax: 5, type: "nonveg", emoji: "🦐", desc: "Golden fried prawns with cocktail sauce", status: "active", cost: 180, stock: 999 },
{ id: 5, name: "Chicken Biryani", cat: "biryani", price: 320, tax: 5, type: "nonveg", emoji: "🍚", desc: "Aromatic basmati with slow-cooked chicken", status: "active", cost: 130, stock: 999 },
{ id: 6, name: "Veg Biryani", cat: "biryani", price: 260, tax: 5, type: "veg", emoji: "🌾", desc: "Fragrant rice with seasonal vegetables", status: "active", cost: 90, stock: 999 },
{ id: 7, name: "Mutton Biryani", cat: "biryani", price: 420, tax: 5, type: "nonveg", emoji: "🍲", desc: "Rich slow-cooked mutton dum biryani", status: "active", cost: 190, stock: 999 },
{ id: 8, name: "Prawn Biryani", cat: "biryani", price: 480, tax: 5, type: "nonveg", emoji: "🦐", desc: "Coastal style prawn biryani", status: "active", cost: 220, stock: 999 },
{ id: 9, name: "Butter Chicken", cat: "main", price: 300, tax: 5, type: "nonveg", emoji: "🍛", desc: "Creamy tomato-based chicken curry", status: "active", cost: 120, stock: 999 },
{ id: 10, name: "Paneer Butter Masala", cat: "main", price: 260, tax: 5, type: "veg", emoji: "🧀", desc: "Rich buttery paneer in tomato gravy", status: "active", cost: 100, stock: 999 },
{ id: 11, name: "Dal Tadka", cat: "main", price: 180, tax: 5, type: "veg", emoji: "🫕", desc: "Yellow lentils with aromatic tempering", status: "active", cost: 50, stock: 999 },
{ id: 12, name: "Rogan Josh", cat: "main", price: 380, tax: 5, type: "nonveg", emoji: "🍖", desc: "Kashmiri slow-cooked lamb curry", status: "active", cost: 160, stock: 999 },
{ id: 13, name: "Naan", cat: "breads", price: 40, tax: 5, type: "veg", emoji: "🫓", desc: "Soft leavened tandoor flatbread", status: "active", cost: 12, stock: 999 },
{ id: 14, name: "Tandoori Roti", cat: "breads", price: 30, tax: 5, type: "veg", emoji: "🥙", desc: "Whole wheat tandoor bread", status: "active", cost: 8, stock: 999 },
{ id: 15, name: "Garlic Naan", cat: "breads", price: 60, tax: 5, type: "veg", emoji: "🧄", desc: "Naan topped with garlic and butter", status: "active", cost: 18, stock: 999 },
{ id: 16, name: "Mango Lassi", cat: "drinks", price: 80, tax: 5, type: "veg", emoji: "🥭", desc: "Chilled mango yogurt smoothie", status: "active", cost: 25, stock: 999 },
{ id: 17, name: "Masala Chai", cat: "drinks", price: 40, tax: 5, type: "veg", emoji: "☕", desc: "Spiced Indian milk tea", status: "active", cost: 10, stock: 999 },
{ id: 18, name: "Fresh Lime Soda", cat: "drinks", price: 60, tax: 5, type: "veg", emoji: "🍋", desc: "Refreshing lime soda with mint", status: "active", cost: 15, stock: 999 },
{ id: 19, name: "Gulab Jamun", cat: "desserts",price: 100, tax: 5, type: "veg", emoji: "🍮", desc: "Soft milk dumplings in rose syrup", status: "active", cost: 30, stock: 999 },
{ id: 20, name: "Kulfi", cat: "desserts",price: 120, tax: 5, type: "veg", emoji: "🍦", desc: "Traditional Indian ice cream", status: "active", cost: 35, stock: 999 },
],
tables: Array.from({ length: 16 }, (_, i) => ({
num: i + 1,
status: ["free","free","occupied","free","occupied","reserved","free","free","occupied","free","free","reserved","free","occupied","free","free"][i],
pax: [2,4,6,2,4,4,2,6,4,2,4,8,2,4,4,6][i],
section: i < 6 ? "Indoor" : i < 12 ? "Outdoor" : "Private",
currentOrder: null,
})),
orders: [
{ id: "ORD-0001", table: 3, items: [{ id: 5, name: "Chicken Biryani", qty: 2, price: 320, tax: 5 }, { id: 16, name: "Mango Lassi", qty: 2, price: 80, tax: 5 }], subtotal: 800, taxAmount: 40, total: 840, waiter: "waiter1", waiterName: "Ravi Kumar", status: "served", time: Date.now() - 3600000, note: "", paymentMethod: null },
{ id: "ORD-0002", table: 6, items: [{ id: 9, name: "Butter Chicken", qty: 1, price: 300, tax: 5 }, { id: 13, name: "Naan", qty: 3, price: 40, tax: 5 }], subtotal: 420, taxAmount: 21, total: 441, waiter: "waiter2", waiterName: "Priya Sharma", status: "preparing", time: Date.now() - 1800000, note: "Less spice", paymentMethod: null },
{ id: "ORD-0003", table: 1, items: [{ id: 2, name: "Paneer Tikka", qty: 1, price: 220, tax: 5 }, { id: 6, name: "Veg Biryani", qty: 2, price: 260, tax: 5 }], subtotal: 740, taxAmount: 37, total: 777, waiter: "waiter1", waiterName: "Ravi Kumar", status: "new", time: Date.now() - 600000, note: "", paymentMethod: null },
{ id: "ORD-0004", table: 9, items: [{ id: 11, name: "Dal Tadka", qty: 1, price: 180, tax: 5 }, { id: 14, name: "Tandoori Roti", qty: 4, price: 30, tax: 5 }], subtotal: 300, taxAmount: 15, total: 315, waiter: "waiter1", waiterName: "Ravi Kumar", status: "billed", time: Date.now() - 7200000, note: "", paymentMethod: "Cash" },
],
inventory: [
{ id: 1, name: "Chicken", category: "Protein", unit: "kg", stock: 15, reorderAt: 5, cost: 180, supplier: "Fresh Farms" },
{ id: 2, name: "Basmati Rice", category: "Grains", unit: "kg", stock: 50, reorderAt: 10, cost: 90, supplier: "Grain Hub" },
{ id: 3, name: "Paneer", category: "Dairy", unit: "kg", stock: 8, reorderAt: 3, cost: 280, supplier: "Dairy Fresh" },
{ id: 4, name: "Cooking Oil", category: "Oils", unit: "liters",stock: 25, reorderAt: 8, cost: 120, supplier: "Oil Co" },
{ id: 5, name: "Onions", category: "Vegetables",unit: "kg", stock: 3, reorderAt: 5, cost: 30, supplier: "Veggie Mart" },
{ id: 6, name: "Tomatoes", category: "Vegetables",unit: "kg", stock: 12, reorderAt: 4, cost: 40, supplier: "Veggie Mart" },
{ id: 7, name: "Mutton", category: "Protein", unit: "kg", stock: 6, reorderAt: 3, cost: 450, supplier: "Meat House" },
{ id: 8, name: "Prawns", category: "Seafood", unit: "kg", stock: 4, reorderAt: 2, cost: 520, supplier: "Sea Fresh" },
],
customers: [
{ id: 1, name: "Ramesh Iyer", phone: "+91 98001 11111", email: "ramesh@gmail.com", visits: 12, totalSpent: 8400, lastVisit: "Today", tag: "VIP", loyaltyPts: 840 },
{ id: 2, name: "Sunita Mehta", phone: "+91 98001 22222", email: "sunita@gmail.com", visits: 5, totalSpent: 3200, lastVisit: "Yesterday", tag: "Regular", loyaltyPts: 320 },
{ id: 3, name: "Kiran Rao", phone: "+91 98001 33333", email: "kiran@gmail.com", visits: 28, totalSpent: 19600, lastVisit: "2 days ago", tag: "VIP", loyaltyPts: 1960 },
{ id: 4, name: "Deepa Nair", phone: "+91 98001 44444", email: "deepa@gmail.com", visits: 2, totalSpent: 1200, lastVisit: "Last week", tag: "New", loyaltyPts: 120 },
],
expenses: [
{ id: 1, desc: "Gas Cylinder", amount: 2800, category: "Utilities", date: "2026-03-25", status: "paid", receipt: true },
{ id: 2, desc: "Staff Uniform", amount: 4500, category: "HR", date: "2026-03-20", status: "paid", receipt: true },
{ id: 3, desc: "Cleaning Supplies",amount: 1200, category: "Ops", date: "2026-03-28", status: "pending", receipt: false },
],
settings: {
restaurantName: "The Grand Restoflo",
address: "123, MG Road, Bengaluru - 560001",
phone: "+91 80 4567 8900",
gst: "29AABCU9603R1Z1",
fssai: "12345678901234",
currency: "₹",
gstRate: 5,
serviceCharge: 2,
logo: "🍽",
theme: "light",
kotPrinter: "Kitchen Printer 1",
timezone: "Asia/Kolkata",
branches: [{ id: "main", name: "Main Branch - MG Road", active: true }],
},
orderCounter: 5,
lastUpdated: Date.now(),
};
}
// ── STORAGE LAYER (Real-time persistent) ─────────────────────
async function loadDB() {
try {
const result = await window.storage.get(STORAGE_KEY, true);
if (result?.value) return JSON.parse(result.value);
} catch {}
return null;
}
async function saveDB(db) {
try {
const data = { ...db, lastUpdated: Date.now() };
await window.storage.set(STORAGE_KEY, JSON.stringify(data), true);
return data;
} catch (e) {
console.error("Storage save failed", e);
return db;
}
}
// ── HELPERS ──────────────────────────────────────────────────
const fmt = (n) => "₹" + Number(n || 0).toLocaleString("en-IN");
const fmtTime = (ts) => new Date(ts).toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit" });
const fmtDate = (ts) => new Date(ts).toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" });
const uid = () => Math.random().toString(36).slice(2, 9);
const statusColor = { new: "#F59E0B", preparing: "#3B82F6", served: "#039855", billed: "#6B7280", cancelled: "#EF4444" };
const statusBg = { new: "#FEF3C7", preparing: "#DBEAFE", served: "#D1FAE5", billed: "#F3F4F6", cancelled: "#FEE2E2" };
// ── MAIN APP ─────────────────────────────────────────────────
export default function App() {
const [db, setDb] = useState(null);
const [loading, setLoading] = useState(true);
const [currentUser, setCurrentUser] = useState(null);
const [page, setPage] = useState("dashboard");
const [sidebarOpen, setSidebarOpen] = useState(true);
const [toast, setToast] = useState([]);
const [modal, setModal] = useState(null);
const [posState, setPosState] = useState({ items: [], table: null, note: "" });
const lastUpdatedRef = useRef(0);
// Load DB
useEffect(() => {
(async () => {
const stored = await loadDB();
setDb(stored || initDB());
setLoading(false);
})();
}, []);
// Real-time polling
useEffect(() => {
if (!currentUser) return;
const interval = setInterval(async () => {
try {
const result = await window.storage.get(STORAGE_KEY, true);
if (result?.value) {
const remote = JSON.parse(result.value);
if (remote.lastUpdated > lastUpdatedRef.current) {
lastUpdatedRef.current = remote.lastUpdated;
setDb(remote);
}
}
} catch {}
}, POLL_INTERVAL);
return () => clearInterval(interval);
}, [currentUser]);
const mutate = useCallback(async (updater) => {
setDb(prev => {
const next = typeof updater === "function" ? updater(prev) : updater;
saveDB(next).then(saved => {
lastUpdatedRef.current = saved.lastUpdated;
setDb(saved);
});
return next;
});
}, []);
const notify = useCallback((msg, type = "success") => {
const id = uid();
setToast(t => [...t, { id, msg, type }]);
setTimeout(() => setToast(t => t.filter(x => x.id !== id)), 3500);
}, []);
if (loading) return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100vh", background: "#F8F9FC", fontFamily: "system-ui" }}>
<div style={{ textAlign: "center" }}>
<div style={{ fontSize: 48, marginBottom: 16 }}>🍽</div>
<div style={{ fontSize: 18, fontWeight: 700, color: "#111" }}>RestofLo</div>
<div style={{ fontSize: 13, color: "#888", marginTop: 6 }}>Loading your restaurant...</div>
<div style={{ marginTop: 20, width: 200, height: 3, background: "#E5E7EB", borderRadius: 99, overflow: "hidden" }}>
<div style={{ height: "100%", width: "60%", background: "#875BF7", borderRadius: 99, animation: "pulse 1.5s ease-in-out infinite" }} />
</div>
</div>
</div>
);
if (!currentUser) return <LoginScreen db={db} onLogin={u => { setCurrentUser(u); lastUpdatedRef.current = db.lastUpdated; }} />;
const canAccess = (mod) => currentUser.role === "admin" || currentUser.permissions.includes(mod);
const gotoPage = (p) => { if (canAccess(p)) { setPage(p); } else notify("Access denied for this module", "error"); };
const theme = db.settings?.theme === "dark" ? darkTheme : lightTheme;
return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh", background: theme.bg, color: theme.text, fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif", overflow: "hidden" }}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #D1D5DB; border-radius: 99px; }
input, select, textarea { font-family: inherit; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes spin { to { transform: rotate(360deg); } }
.page-anim { animation: fadeIn 0.25s ease; }
.hover-row:hover { background: ${theme.hover} !important; }
.sidebar-item:hover { background: ${theme.sidebarHover} !important; }
.menu-card:hover { box-shadow: 0 4px 20px rgba(135,91,247,0.2); transform: translateY(-2px); border-color: #875BF7 !important; }
.btn-primary:hover { background: #7C3AED !important; }
.table-tile:hover { transform: scale(1.04); }
`}</style>
{/* TOPBAR */}
<Topbar db={db} theme={theme} currentUser={currentUser} page={page} sidebarOpen={sidebarOpen}
setSidebarOpen={setSidebarOpen} onLogout={() => setCurrentUser(null)} onPageChange={gotoPage} />
<div style={{ display: "flex", flex: 1, overflow: "hidden" }}>
{/* SIDEBAR */}
{sidebarOpen && (
<Sidebar theme={theme} page={page} currentUser={currentUser} canAccess={canAccess} onPageChange={gotoPage}
newKOT={db.orders.filter(o => o.status === "new").length}
db={db} />
)}
{/* MAIN */}
<main style={{ flex: 1, overflow: "auto", background: theme.mainBg }}>
<PageRenderer page={page} db={db} mutate={mutate} notify={notify} currentUser={currentUser}
theme={theme} modal={modal} setModal={setModal} posState={posState} setPosState={setPosState}
canAccess={canAccess} gotoPage={gotoPage} />
</main>
</div>
{/* TOAST */}
<div style={{ position: "fixed", top: 16, right: 16, zIndex: 9999, display: "flex", flexDirection: "column", gap: 8 }}>
{toast.map(t => (
<div key={t.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 16px", background: t.type === "error" ? "#FEF2F2" : t.type === "info" ? "#EFF6FF" : "#F0FDF4", border: `1px solid ${t.type === "error" ? "#FECACA" : t.type === "info" ? "#BFDBFE" : "#BBF7D0"}`, borderLeft: `4px solid ${t.type === "error" ? "#EF4444" : t.type === "info" ? "#3B82F6" : "#22C55E"}`, borderRadius: 10, fontSize: 13, fontWeight: 500, color: "#111", minWidth: 260, boxShadow: "0 4px 20px rgba(0,0,0,0.1)", animation: "slideIn 0.3s ease" }}>
<span>{t.type === "error" ? "❌" : t.type === "info" ? "ℹ️" : "✅"}</span>
{t.msg}
</div>
))}
</div>
{/* MODAL */}
{modal && <ModalLayer modal={modal} setModal={setModal} db={db} mutate={mutate} notify={notify} theme={theme} currentUser={currentUser} />}
</div>
);
}
// ── THEMES ───────────────────────────────────────────────────
const lightTheme = {
bg: "#FFFFFF", mainBg: "#F8F9FC", surface: "#FFFFFF", surface2: "#F3F4F6",
border: "#E5E7EB", text: "#111827", text2: "#6B7280", text3: "#9CA3AF",
sidebar: "#FFFFFF", sidebarBorder: "#E5E7EB", sidebarHover: "#F5F3FF",
sidebarActive: "#EDE9FE", sidebarActiveText: "#875BF7",
topbar: "#FFFFFF", topbarBorder: "#E5E7EB",
hover: "#F9FAFB", accent: "#875BF7", accentBg: "#EDE9FE",
card: "#FFFFFF", cardBorder: "#E5E7EB",
input: "#FFFFFF", inputBorder: "#D1D5DB",
tableHeader: "#F9FAFB",
badge: { green: { bg: "#D1FAE5", text: "#065F46" }, red: { bg: "#FEE2E2", text: "#991B1B" }, orange: { bg: "#FEF3C7", text: "#92400E" }, blue: { bg: "#DBEAFE", text: "#1E40AF" }, purple: { bg: "#EDE9FE", text: "#5B21B6" }, gray: { bg: "#F3F4F6", text: "#374151" } }
};
const darkTheme = {
bg: "#0F1117", mainBg: "#141821", surface: "#1C2030", surface2: "#252A3A",
border: "#2D3548", text: "#F1F5F9", text2: "#94A3B8", text3: "#64748B",
sidebar: "#1C2030", sidebarBorder: "#2D3548", sidebarHover: "#252A3A",
sidebarActive: "#312E81", sidebarActiveText: "#A78BFA",
topbar: "#1C2030", topbarBorder: "#2D3548",
hover: "#252A3A", accent: "#875BF7", accentBg: "#312E81",
card: "#1C2030", cardBorder: "#2D3548",
input: "#252A3A", inputBorder: "#2D3548",
tableHeader: "#252A3A",
badge: { green: { bg: "#064E3B", text: "#6EE7B7" }, red: { bg: "#7F1D1D", text: "#FCA5A5" }, orange: { bg: "#78350F", text: "#FCD34D" }, blue: { bg: "#1E3A5F", text: "#93C5FD" }, purple: { bg: "#312E81", text: "#C4B5FD" }, gray: { bg: "#374151", text: "#D1D5DB" } }
};
// ── LOGIN ─────────────────────────────────────────────────────
function LoginScreen({ db, onLogin }) {
const [role, setRole] = useState("admin");
const [uid, setUid] = useState("admin");
const [pass, setPass] = useState("admin123");
const [err, setErr] = useState("");
const [showPass, setShowPass] = useState(false);
const login = () => {
const u = db.users.find(x => x.id === uid.trim() && x.pass === pass.trim() && x.active);
if (!u) { setErr("Invalid credentials. Please try again."); return; }
onLogin(u);
};
const demoUsers = db.users.filter(u => u.id !== "admin");
return (
<div style={{ minHeight: "100vh", background: "linear-gradient(135deg, #F8F9FC 0%, #EDE9FE 100%)", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "'Plus Jakarta Sans', system-ui" }}>
<style>{`@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap'); * { box-sizing: border-box; margin: 0; padding: 0; }`}</style>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 0, maxWidth: 900, width: "90vw", background: "#fff", borderRadius: 24, boxShadow: "0 32px 80px rgba(0,0,0,0.15)", overflow: "hidden" }}>
{/* Left panel */}
<div style={{ background: "linear-gradient(160deg, #875BF7 0%, #312E81 100%)", padding: "48px 40px", display: "flex", flexDirection: "column", justifyContent: "space-between", color: "#fff" }}>
<div>
<div style={{ fontSize: 32, marginBottom: 8 }}>🍽</div>
<div style={{ fontSize: 28, fontWeight: 800, letterSpacing: "-1px" }}>RestofLo</div>
<div style={{ fontSize: 13, opacity: 0.7, marginTop: 4 }}>Restaurant Management System</div>
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 600, opacity: 0.8, marginBottom: 14, textTransform: "uppercase", letterSpacing: "0.5px" }}>Quick Access — Demo Users</div>
{demoUsers.map(u => (
<div key={u.id} onClick={() => { setUid(u.id); setPass(u.pass); setRole("staff"); }} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", background: "rgba(255,255,255,0.12)", borderRadius: 10, marginBottom: 8, cursor: "pointer", border: "1px solid rgba(255,255,255,0.1)", transition: ".15s" }}>
<div style={{ width: 32, height: 32, borderRadius: "50%", background: u.color, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 13, fontWeight: 700 }}>{u.name[0]}</div>
<div>
<div style={{ fontSize: 13, fontWeight: 600 }}>{u.name}</div>
<div style={{ fontSize: 11, opacity: 0.7 }}>{u.id} · {u.role}</div>
</div>
</div>
))}
</div>
<div style={{ fontSize: 11, opacity: 0.5 }}>© 2026 RestofLo. All rights reserved.</div>
</div>
{/* Right panel */}
<div style={{ padding: "48px 40px", display: "flex", flexDirection: "column", justifyContent: "center" }}>
<div style={{ marginBottom: 32 }}>
<div style={{ fontSize: 24, fontWeight: 800, color: "#111", letterSpacing: "-0.5px" }}>Sign in</div>
<div style={{ fontSize: 13, color: "#888", marginTop: 4 }}>Access your restaurant dashboard</div>
</div>
{err && <div style={{ background: "#FEF2F2", border: "1px solid #FECACA", borderRadius: 8, padding: "10px 14px", fontSize: 13, color: "#991B1B", marginBottom: 16 }}>⚠️ {err}</div>}
<div style={{ marginBottom: 16 }}>
<label style={{ display: "block", fontSize: 12, fontWeight: 600, color: "#374151", marginBottom: 6, textTransform: "uppercase", letterSpacing: "0.5px" }}>Login ID</label>
<input value={uid} onChange={e => { setUid(e.target.value); setErr(""); }} onKeyDown={e => e.key === "Enter" && login()}
placeholder="admin" style={{ width: "100%", padding: "12px 14px", border: "1.5px solid #D1D5DB", borderRadius: 10, fontSize: 14, outline: "none", transition: ".2s" }}
onFocus={e => e.target.style.borderColor = "#875BF7"} onBlur={e => e.target.style.borderColor = "#D1D5DB"} />
</div>
<div style={{ marginBottom: 24, position: "relative" }}>
<label style={{ display: "block", fontSize: 12, fontWeight: 600, color: "#374151", marginBottom: 6, textTransform: "uppercase", letterSpacing: "0.5px" }}>Password</label>
<input type={showPass ? "text" : "password"} value={pass} onChange={e => { setPass(e.target.value); setErr(""); }} onKeyDown={e => e.key === "Enter" && login()}
placeholder="••••••••" style={{ width: "100%", padding: "12px 40px 12px 14px", border: "1.5px solid #D1D5DB", borderRadius: 10, fontSize: 14, outline: "none" }}
onFocus={e => e.target.style.borderColor = "#875BF7"} onBlur={e => e.target.style.borderColor = "#D1D5DB"} />
<span onClick={() => setShowPass(s => !s)} style={{ position: "absolute", right: 12, top: 36, cursor: "pointer", fontSize: 16, color: "#888" }}>{showPass ? "🙈" : "👁"}</span>
</div>
<button onClick={login} className="btn-primary" style={{ width: "100%", padding: "13px", background: "#875BF7", color: "#fff", border: "none", borderRadius: 10, fontSize: 14, fontWeight: 700, cursor: "pointer", transition: ".2s" }}>
Sign In →
</button>
<div style={{ marginTop: 20, padding: 14, background: "#F5F3FF", borderRadius: 10, fontSize: 12, color: "#5B21B6" }}>
<strong>Admin:</strong> admin / admin123
</div>
</div>
</div>
</div>
);
}
// ── TOPBAR ────────────────────────────────────────────────────
function Topbar({ db, theme: t, currentUser, page, sidebarOpen, setSidebarOpen, onLogout, onPageChange }) {
const [dropOpen, setDropOpen] = useState(false);
const pageTitle = MODULES.find(m => m.id === page)?.label || "Dashboard";
return (
<div style={{ height: 56, background: t.topbar, borderBottom: `1px solid ${t.topbarBorder}`, display: "flex", alignItems: "center", padding: "0 16px", gap: 12, flexShrink: 0, position: "relative", zIndex: 100 }}>
<button onClick={() => setSidebarOpen(s => !s)} style={{ width: 32, height: 32, border: "none", background: "transparent", cursor: "pointer", fontSize: 18, color: t.text2, borderRadius: 6, display: "flex", alignItems: "center", justifyContent: "center" }}>
</button>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontSize: 20 }}>{db.settings?.logo || "🍽"}</span>
<span style={{ fontWeight: 800, fontSize: 16, color: "#875BF7" }}>{db.settings?.restaurantName || "RestofLo"}</span>
</div>
<div style={{ width: 1, height: 20, background: t.border, margin: "0 4px" }} />
<div style={{ fontSize: 13, color: t.text2, fontWeight: 500 }}>{pageTitle}</div>
<div style={{ flex: 1 }} />
{/* Search */}
<div style={{ display: "flex", alignItems: "center", gap: 8, background: t.surface2, border: `1px solid ${t.border}`, borderRadius: 8, padding: "0 12px", height: 34, minWidth: 200 }}>
<span style={{ fontSize: 13, color: t.text3 }}>🔍</span>
<input placeholder="Search anything..." style={{ border: "none", background: "transparent", fontSize: 13, color: t.text, outline: "none", width: "100%" }} />
</div>
{/* KOT badge */}
{currentUser.role === "admin" && (
<button onClick={() => onPageChange("kitchen")} style={{ position: "relative", padding: "6px 12px", background: "#FEF3C7", border: "1px solid #FCD34D", borderRadius: 8, cursor: "pointer", fontSize: 13, fontWeight: 600, color: "#92400E", display: "flex", alignItems: "center", gap: 6 }}>
🍳 Kitchen
{db.orders.filter(o => o.status === "new").length > 0 && (
<span style={{ background: "#EF4444", color: "#fff", fontSize: 10, fontWeight: 700, padding: "1px 6px", borderRadius: 99 }}>{db.orders.filter(o => o.status === "new").length}</span>
)}
</button>
)}
{/* User */}
<div style={{ position: "relative" }}>
<div onClick={() => setDropOpen(s => !s)} style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer", padding: "4px 8px", borderRadius: 8, border: `1px solid ${t.border}` }}>
<div style={{ width: 28, height: 28, borderRadius: "50%", background: currentUser.color, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 12, fontWeight: 700, color: "#fff" }}>{currentUser.name[0]}</div>
<div>
<div style={{ fontSize: 12, fontWeight: 600, color: t.text }}>{currentUser.name}</div>
<div style={{ fontSize: 10, color: t.text3 }}>{currentUser.role}</div>
</div>
<span style={{ fontSize: 10, color: t.text3 }}></span>
</div>
{dropOpen && (
<div style={{ position: "absolute", right: 0, top: "calc(100% + 6px)", background: t.surface, border: `1px solid ${t.border}`, borderRadius: 10, boxShadow: "0 8px 32px rgba(0,0,0,0.12)", minWidth: 180, zIndex: 999, padding: 6 }}>
<div style={{ padding: "8px 12px", fontSize: 12, color: t.text2 }}>Signed in as <strong>{currentUser.id}</strong></div>
<div style={{ height: 1, background: t.border, margin: "4px 0" }} />
{["settings", "access"].map(p => currentUser.role === "admin" && (
<div key={p} onClick={() => { onPageChange(p); setDropOpen(false); }} style={{ padding: "8px 12px", fontSize: 13, cursor: "pointer", borderRadius: 6, color: t.text }} className="hover-row">{p === "settings" ? "⚙️ Settings" : "🔐 Access Control"}</div>
))}
<div onClick={() => { onLogout(); setDropOpen(false); }} style={{ padding: "8px 12px", fontSize: 13, cursor: "pointer", borderRadius: 6, color: "#EF4444" }} className="hover-row">🚪 Sign Out</div>
</div>
)}
</div>
</div>
);
}
// ── SIDEBAR ───────────────────────────────────────────────────
function Sidebar({ theme: t, page, currentUser, canAccess, onPageChange, newKOT, db }) {
const groups = [...new Set(MODULES.map(m => m.group))];
return (
<div style={{ width: 220, background: t.sidebar, borderRight: `1px solid ${t.sidebarBorder}`, overflow: "auto", flexShrink: 0 }}>
{groups.map(g => {
const items = MODULES.filter(m => m.group === g && (currentUser.role === "admin" || canAccess(m.id)));
if (!items.length) return null;
return (
<div key={g} style={{ padding: "12px 8px 4px" }}>
<div style={{ fontSize: 10, fontWeight: 700, color: t.text3, textTransform: "uppercase", letterSpacing: "0.8px", padding: "0 8px 6px" }}>{g}</div>
{items.map(m => {
const active = page === m.id;
return (
<div key={m.id} onClick={() => onPageChange(m.id)} className="sidebar-item"
style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 10px", borderRadius: 7, cursor: "pointer", marginBottom: 1, background: active ? t.sidebarActive : "transparent", color: active ? t.sidebarActiveText : t.text2, fontSize: 13, fontWeight: active ? 600 : 500, transition: ".15s", position: "relative" }}>
<span style={{ fontSize: 14, width: 18, textAlign: "center" }}>{m.icon}</span>
<span style={{ flex: 1 }}>{m.label}</span>
{m.id === "kitchen" && newKOT > 0 && <span style={{ background: "#EF4444", color: "#fff", fontSize: 9, fontWeight: 700, padding: "1px 5px", borderRadius: 99 }}>{newKOT}</span>}
</div>
);
})}
</div>
);
})}
<div style={{ height: 24 }} />
</div>
);
}
// ── PAGE RENDERER ─────────────────────────────────────────────
function PageRenderer({ page, db, mutate, notify, currentUser, theme, modal, setModal, posState, setPosState, canAccess, gotoPage }) {
const props = { db, mutate, notify, currentUser, theme: theme, t: theme, setModal, canAccess, gotoPage };
const pages = {
dashboard: <DashboardPage {...props} />,
pos: <POSPage {...props} posState={posState} setPosState={setPosState} />,
tables: <TablesPage {...props} />,
kitchen: <KitchenPage {...props} />,
orders: <OrdersPage {...props} />,
billing: <BillingPage {...props} />,
menu: <MenuPage {...props} />,
categories: <CategoriesPage {...props} />,
inventory: <InventoryPage {...props} />,
reports: <ReportsPage {...props} />,
accounting: <AccountingPage {...props} />,
expenses: <ExpensesPage {...props} />,
payroll: <PayrollPage {...props} />,
crm: <CRMPage {...props} />,
employees: <EmployeesPage {...props} />,
users: <UsersPage {...props} />,
access: <AccessPage {...props} />,
settings: <SettingsPage {...props} />,
};
return (
<div className="page-anim" style={{ minHeight: "100%" }}>
{pages[page] || <GenericPage title={MODULES.find(m => m.id === page)?.label || page} icon={MODULES.find(m => m.id === page)?.icon} theme={theme} />}
</div>
);
}
// ── SHARED COMPONENTS ─────────────────────────────────────────
function PageHeader({ title, subtitle, actions, t }) {
return (
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 24 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 800, color: t.text, letterSpacing: "-0.3px" }}>{title}</h2>
{subtitle && <div style={{ fontSize: 13, color: t.text2, marginTop: 2 }}>{subtitle}</div>}
</div>
{actions && <div style={{ display: "flex", gap: 8 }}>{actions}</div>}
</div>
);
}
function Btn({ children, onClick, variant = "primary", size = "md", disabled = false, style: sx = {} }) {
const base = { border: "none", cursor: disabled ? "not-allowed" : "pointer", fontFamily: "inherit", fontWeight: 600, borderRadius: 8, display: "inline-flex", alignItems: "center", gap: 6, transition: ".15s", opacity: disabled ? 0.5 : 1 };
const sizes = { sm: { fontSize: 12, padding: "6px 12px" }, md: { fontSize: 13, padding: "8px 16px" }, lg: { fontSize: 14, padding: "11px 20px" } };
const variants = { primary: { background: "#875BF7", color: "#fff" }, secondary: { background: "#F3F4F6", color: "#374151", border: "1px solid #E5E7EB" }, danger: { background: "#FEE2E2", color: "#991B1B", border: "1px solid #FECACA" }, success: { background: "#D1FAE5", color: "#065F46", border: "1px solid #A7F3D0" }, ghost: { background: "transparent", color: "#875BF7", border: "1px solid #EDE9FE" } };
return <button onClick={onClick} disabled={disabled} className={variant === "primary" ? "btn-primary" : ""} style={{ ...base, ...sizes[size], ...variants[variant], ...sx }}>{children}</button>;
}
function Input({ label, value, onChange, placeholder, type = "text", style: sx = {} }) {
return (
<div style={{ marginBottom: 14 }}>
{label && <label style={{ display: "block", fontSize: 12, fontWeight: 600, color: "#374151", marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.4px" }}>{label}</label>}
<input type={type} value={value} onChange={e => onChange(e.target.value)} placeholder={placeholder}
style={{ width: "100%", padding: "10px 12px", border: "1.5px solid #D1D5DB", borderRadius: 8, fontSize: 13, outline: "none", fontFamily: "inherit", ...sx }}
onFocus={e => e.target.style.borderColor = "#875BF7"} onBlur={e => e.target.style.borderColor = "#D1D5DB"} />
</div>
);
}
function Select({ label, value, onChange, options, style: sx = {} }) {
return (
<div style={{ marginBottom: 14 }}>
{label && <label style={{ display: "block", fontSize: 12, fontWeight: 600, color: "#374151", marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.4px" }}>{label}</label>}
<select value={value} onChange={e => onChange(e.target.value)} style={{ width: "100%", padding: "10px 12px", border: "1.5px solid #D1D5DB", borderRadius: 8, fontSize: 13, outline: "none", fontFamily: "inherit", background: "#fff", cursor: "pointer", ...sx }}>
{options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</div>
);
}
function Badge({ label, color = "gray", t }) {
const c = t?.badge?.[color] || lightTheme.badge[color];
return <span style={{ display: "inline-flex", alignItems: "center", padding: "2px 9px", borderRadius: 99, fontSize: 11, fontWeight: 600, background: c.bg, color: c.text }}>{label}</span>;
}
function Card({ children, style: sx = {}, t }) {
return <div style={{ background: t?.card || "#fff", border: `1px solid ${t?.cardBorder || "#E5E7EB"}`, borderRadius: 12, padding: 20, ...sx }}>{children}</div>;
}
function StatCard({ icon, value, label, change, changeUp, color = "#875BF7", t }) {
return (
<Card t={t} style={{ position: "relative", overflow: "hidden" }}>
<div style={{ position: "absolute", top: -16, right: -16, width: 80, height: 80, borderRadius: "50%", background: color, opacity: 0.07 }} />
<div style={{ fontSize: 22, marginBottom: 10 }}>{icon}</div>
<div style={{ fontSize: 26, fontWeight: 800, color: t?.text || "#111", fontFamily: "inherit", letterSpacing: "-0.5px" }}>{value}</div>
<div style={{ fontSize: 12, color: t?.text2 || "#888", marginTop: 3 }}>{label}</div>
{change && <div style={{ marginTop: 8, display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", borderRadius: 99, fontSize: 11, fontWeight: 600, background: changeUp ? "#D1FAE5" : "#FEE2E2", color: changeUp ? "#065F46" : "#991B1B" }}>{changeUp ? "↑" : "↓"} {change}</div>}
</Card>
);
}
function Table({ cols, rows, t }) {
return (
<div style={{ overflowX: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ background: t?.tableHeader || "#F9FAFB" }}>
{cols.map((c, i) => <th key={i} style={{ padding: "10px 14px", textAlign: "left", fontSize: 11, fontWeight: 700, color: t?.text3 || "#9CA3AF", textTransform: "uppercase", letterSpacing: "0.5px", whiteSpace: "nowrap", borderBottom: `1px solid ${t?.border || "#E5E7EB"}` }}>{c}</th>)}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} className="hover-row" style={{ borderBottom: `1px solid ${t?.border || "#E5E7EB"}` }}>
{row.map((cell, j) => <td key={j} style={{ padding: "11px 14px", fontSize: 13, color: t?.text || "#111" }}>{cell}</td>)}
</tr>
))}
{rows.length === 0 && <tr><td colSpan={cols.length} style={{ padding: "40px 14px", textAlign: "center", color: t?.text3 || "#9CA3AF", fontSize: 13 }}>No data found</td></tr>}
</tbody>
</table>
</div>
);
}
function Toggle({ checked, onChange }) {
return (
<div onClick={() => onChange(!checked)} style={{ width: 40, height: 22, borderRadius: 99, background: checked ? "#875BF7" : "#D1D5DB", cursor: "pointer", position: "relative", transition: ".2s", flexShrink: 0 }}>
<div style={{ position: "absolute", width: 16, height: 16, borderRadius: "50%", background: "#fff", top: 3, left: checked ? 21 : 3, transition: ".2s", boxShadow: "0 1px 3px rgba(0,0,0,0.2)" }} />
</div>
);
}
// ── DASHBOARD ─────────────────────────────────────────────────
function DashboardPage({ db, mutate, notify, t, setModal }) {
const todayOrders = db.orders;
const totalRevenue = todayOrders.filter(o => o.status === "billed").reduce((a, o) => a + o.total, 0);
const activeOrders = todayOrders.filter(o => !["billed","cancelled"].includes(o.status)).length;
const occupiedTables = db.tables.filter(t => t.status === "occupied").length;
const newKOT = todayOrders.filter(o => o.status === "new").length;
const hourlyData = [0,0,0,0,1200,2400,4800,3600,6000,8000,9200,10400,7200,6800,5400,4200,3800,5600,6200,4400,3200,1800,800,0];
const hours = ["6am","7am","8am","9am","10am","11am","12pm","1pm","2pm","3pm","4pm","5pm","6pm","7pm","8pm","9pm","10pm","11pm"];
const peakHours = hourlyData.slice(4, 22);
const maxH = Math.max(...peakHours);
return (
<div style={{ padding: 24 }}>
<PageHeader title="Dashboard" subtitle={`${fmtDate(Date.now())} · Live Overview`} t={t}
actions={<Btn onClick={() => notify("Refreshed!", "info")} variant="secondary" size="sm">↻ Refresh</Btn>} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 16, marginBottom: 24 }}>
<StatCard icon="💰" value={fmt(totalRevenue)} label="Revenue Today" change="+12.4%" changeUp t={t} color="#875BF7" />
<StatCard icon="📋" value={activeOrders} label="Active Orders" change={`${newKOT} new`} changeUp={newKOT > 0} t={t} color="#F59E0B" />
<StatCard icon="🪑" value={`${occupiedTables}/${db.tables.length}`} label="Tables Occupied" t={t} color="#3B82F6" />
<StatCard icon="👥" value={db.users.filter(u => u.active && u.role !== "admin").length} label="Staff Active" change="All on duty" changeUp t={t} color="#039855" />
</div>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16, marginBottom: 16 }}>
<Card t={t}>
<div style={{ fontWeight: 700, fontSize: 14, marginBottom: 16, color: t.text }}>Hourly Sales — Today</div>
<div style={{ display: "flex", alignItems: "flex-end", gap: 4, height: 100 }}>
{peakHours.map((v, i) => (
<div key={i} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 3 }}>
<div style={{ width: "100%", borderRadius: "3px 3px 0 0", background: i === 8 ? "#875BF7" : "#EDE9FE", height: maxH ? (v / maxH) * 88 : 4, minHeight: 4, transition: ".5s" }} />
<div style={{ fontSize: 9, color: t.text3, whiteSpace: "nowrap" }}>{hours[i]}</div>
</div>
))}
</div>
</Card>
<Card t={t}>
<div style={{ fontWeight: 700, fontSize: 14, marginBottom: 16, color: t.text }}>Table Status</div>
{["free","occupied","reserved"].map(s => {
const count = db.tables.filter(x => x.status === s).length;
const colors = { free: "#039855", occupied: "#F59E0B", reserved: "#875BF7" };
return (
<div key={s} style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
<div style={{ width: 8, height: 8, borderRadius: "50%", background: colors[s] }} />
<div style={{ flex: 1, fontSize: 13, color: t.text, textTransform: "capitalize" }}>{s}</div>
<div style={{ fontWeight: 700, fontSize: 14, color: t.text }}>{count}</div>
<div style={{ width: 80, height: 6, background: t.surface2, borderRadius: 99 }}>
<div style={{ height: "100%", width: `${(count / db.tables.length) * 100}%`, background: colors[s], borderRadius: 99, transition: ".5s" }} />
</div>
</div>
);
})}
</Card>
</div>
<Card t={t}>
<div style={{ fontWeight: 700, fontSize: 14, marginBottom: 16, color: t.text }}>Recent Orders</div>
<Table t={t} cols={["Order #", "Table", "Items", "Amount", "Waiter", "Status", "Time"]}
rows={db.orders.slice(0, 8).map(o => [
<strong>{o.id}</strong>,
`Table ${o.table}`,
`${o.items.length} items`,
<strong style={{ color: "#875BF7" }}>{fmt(o.total)}</strong>,
o.waiterName,
<span style={{ display: "inline-flex", alignItems: "center", padding: "2px 9px", borderRadius: 99, fontSize: 11, fontWeight: 600, background: statusBg[o.status], color: statusColor[o.status] }}>{o.status}</span>,
fmtTime(o.time),
])} />
</Card>
</div>
);
}
// ── POS ───────────────────────────────────────────────────────
function POSPage({ db, mutate, notify, t, posState, setPosState, currentUser }) {
const [activeCat, setActiveCat] = useState("all");
const [search, setSearch] = useState("");
const items = db.menuItems.filter(m => m.status === "active" && (activeCat === "all" || m.cat === activeCat) && (!search || m.name.toLowerCase().includes(search.toLowerCase())));
const addItem = (item) => {
setPosState(s => {
const ex = s.items.find(x => x.id === item.id);
if (ex) return { ...s, items: s.items.map(x => x.id === item.id ? { ...x, qty: x.qty + 1 } : x) };
return { ...s, items: [...s.items, { id: item.id, name: item.name, price: item.price, tax: item.tax, qty: 1 }] };
});
};
const removeItem = (id) => setPosState(s => {
const ex = s.items.find(x => x.id === id);
if (ex?.qty > 1) return { ...s, items: s.items.map(x => x.id === id ? { ...x, qty: x.qty - 1 } : x) };
return { ...s, items: s.items.filter(x => x.id !== id) };
});
const subtotal = posState.items.reduce((a, i) => a + i.price * i.qty, 0);
const taxAmt = Math.round(subtotal * (db.settings.gstRate / 100));
const svcAmt = Math.round(subtotal * (db.settings.serviceCharge / 100));
const total = subtotal + taxAmt + svcAmt;
const placeOrder = async () => {
if (!posState.table) { notify("Select a table first", "error"); return; }
if (!posState.items.length) { notify("Add items to order", "error"); return; }
const orderId = `ORD-${String(db.orderCounter).padStart(4, "0")}`;
const order = {
id: orderId, table: parseInt(posState.table),
items: posState.items.map(i => ({ ...i })),
subtotal, taxAmount: taxAmt, total,
waiter: currentUser.id, waiterName: currentUser.name,
status: "new", time: Date.now(), note: posState.note, paymentMethod: null,
};
await mutate(db => ({
...db,
orders: [order, ...db.orders],
orderCounter: db.orderCounter + 1,
tables: db.tables.map(t => t.num === parseInt(posState.table) ? { ...t, status: "occupied", currentOrder: orderId } : t),
}));
setPosState({ items: [], table: null, note: "" });
notify(`Order ${orderId} placed for Table ${posState.table}! ✅`, "success");
};
return (
<div style={{ display: "flex", height: "100%", overflow: "hidden" }}>
{/* Left */}
<div style={{ flex: 1, overflow: "auto", padding: 20, borderRight: `1px solid ${t.border}` }}>
{/* Controls */}
<div style={{ display: "flex", gap: 10, marginBottom: 16, alignItems: "center" }}>
<div style={{ display: "flex", alignItems: "center", gap: 6, background: t.surface2, border: `1px solid ${t.border}`, borderRadius: 8, padding: "0 12px", flex: 1 }}>
<span style={{ color: t.text3, fontSize: 13 }}>🔍</span>
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search menu..." style={{ border: "none", background: "transparent", padding: "9px 0", fontSize: 13, color: t.text, outline: "none", width: "100%" }} />
</div>
<select value={posState.table || ""} onChange={e => setPosState(s => ({ ...s, table: e.target.value }))}
style={{ padding: "9px 12px", border: `1.5px solid ${posState.table ? "#875BF7" : t.border}`, borderRadius: 8, fontSize: 13, fontFamily: "inherit", background: t.card, color: t.text, cursor: "pointer", outline: "none" }}>
<option value="">Select Table</option>
{db.tables.map(tbl => <option key={tbl.num} value={tbl.num}>Table {tbl.num} ({tbl.section} · {tbl.status})</option>)}
</select>
</div>
{/* Categories */}
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 16 }}>
{[{ id: "all", name: "All Items", icon: "🍽" }, ...db.categories].map(c => (
<button key={c.id} onClick={() => setActiveCat(c.id)} style={{ padding: "6px 14px", borderRadius: 99, border: `1.5px solid ${activeCat === c.id ? "#875BF7" : t.border}`, background: activeCat === c.id ? "#875BF7" : t.card, color: activeCat === c.id ? "#fff" : t.text2, fontSize: 13, fontWeight: 600, cursor: "pointer", transition: ".15s" }}>
{c.icon} {c.name}
</button>
))}
</div>
{/* Menu grid */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(155px, 1fr))", gap: 10 }}>
{items.map(item => (
<div key={item.id} onClick={() => addItem(item)} className="menu-card" style={{ background: t.card, border: `1.5px solid ${t.cardBorder}`, borderRadius: 12, padding: 14, cursor: "pointer", transition: ".2s", position: "relative" }}>
<div style={{ position: "absolute", top: 8, right: 8, width: 12, height: 12, border: `2px solid ${item.type === "veg" ? "#039855" : "#EF4444"}`, borderRadius: 2, display: "flex", alignItems: "center", justifyContent: "center" }}>
<div style={{ width: 6, height: 6, borderRadius: "50%", background: item.type === "veg" ? "#039855" : "#EF4444" }} />
</div>
<div style={{ fontSize: 28, marginBottom: 8 }}>{item.emoji}</div>
<div style={{ fontSize: 13, fontWeight: 700, color: t.text, marginBottom: 3, lineHeight: 1.3 }}>{item.name}</div>
<div style={{ fontSize: 11, color: t.text3, marginBottom: 8 }}>{item.desc?.slice(0, 40)}...</div>
<div style={{ fontSize: 16, fontWeight: 800, color: "#875BF7" }}>{fmt(item.price)}</div>
</div>
))}
</div>
</div>
{/* Order Panel */}
<div style={{ width: 340, display: "flex", flexDirection: "column", background: t.surface }}>
<div style={{ padding: "16px 20px", borderBottom: `1px solid ${t.border}` }}>
<div style={{ fontSize: 15, fontWeight: 800, color: t.text }}>Current Order</div>
{posState.table && <div style={{ fontSize: 12, color: "#875BF7", fontWeight: 600, marginTop: 3 }}>Table {posState.table}</div>}
</div>
<div style={{ flex: 1, overflow: "auto", padding: "10px 16px" }}>
{posState.items.length === 0 ? (
<div style={{ textAlign: "center", padding: "60px 20px", color: t.text3 }}>
<div style={{ fontSize: 36, marginBottom: 10 }}>🛒</div>
<div style={{ fontSize: 13 }}>Tap a menu item to add</div>
</div>
) : posState.items.map(item => (
<div key={item.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 0", borderBottom: `1px solid ${t.border}` }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 600, color: t.text }}>{item.name}</div>
<div style={{ fontSize: 12, color: t.text2 }}>{fmt(item.price)} each</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<button onClick={() => removeItem(item.id)} style={{ width: 24, height: 24, borderRadius: 6, border: `1px solid ${t.border}`, background: t.surface2, cursor: "pointer", fontSize: 14, fontWeight: 700, color: t.text, display: "flex", alignItems: "center", justifyContent: "center" }}>−</button>
<span style={{ fontSize: 13, fontWeight: 700, minWidth: 20, textAlign: "center", color: t.text }}>{item.qty}</span>
<button onClick={() => addItem({ id: item.id, name: item.name, price: item.price, tax: item.tax })} style={{ width: 24, height: 24, borderRadius: 6, border: "1px solid #875BF7", background: "#EDE9FE", cursor: "pointer", fontSize: 14, fontWeight: 700, color: "#875BF7", display: "flex", alignItems: "center", justifyContent: "center" }}>+</button>
</div>
<div style={{ fontSize: 13, fontWeight: 700, color: "#875BF7", minWidth: 55, textAlign: "right" }}>{fmt(item.price * item.qty)}</div>
</div>
))}
{posState.items.length > 0 && (
<textarea value={posState.note} onChange={e => setPosState(s => ({ ...s, note: e.target.value }))} placeholder="Special instructions..." style={{ width: "100%", marginTop: 12, padding: "8px 10px", border: `1px solid ${t.border}`, borderRadius: 8, fontSize: 12, fontFamily: "inherit", color: t.text, background: t.surface2, resize: "none", height: 56, outline: "none" }} />
)}
</div>
{posState.items.length > 0 && (
<div style={{ borderTop: `1px solid ${t.border}`, padding: "14px 20px" }}>
{[["Subtotal", fmt(subtotal)], [`GST (${db.settings.gstRate}%)`, fmt(taxAmt)], [`Service (${db.settings.serviceCharge}%)`, fmt(svcAmt)]].map(([k, v]) => (
<div key={k} style={{ display: "flex", justifyContent: "space-between", fontSize: 13, color: t.text2, marginBottom: 6 }}>
<span>{k}</span><span>{v}</span>
</div>
))}
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 17, fontWeight: 800, color: t.text, marginTop: 8, paddingTop: 10, borderTop: `1px solid ${t.border}` }}>
<span>TOTAL</span><span style={{ color: "#875BF7" }}>{fmt(total)}</span>
</div>
</div>
)}
<div style={{ padding: "12px 16px", display: "flex", gap: 8 }}>
<Btn onClick={() => setPosState({ items: [], table: null, note: "" })} variant="secondary" style={{ flex: 1 }} disabled={!posState.items.length}>Clear</Btn>
<Btn onClick={() => notify("KOT sent to kitchen 🍳", "success")} variant="ghost" style={{ flex: 1 }} disabled={!posState.items.length}>KOT</Btn>
<Btn onClick={placeOrder} variant="primary" style={{ flex: 1 }} disabled={!posState.items.length}>Place →</Btn>
</div>
</div>
</div>
);
}
// ── TABLES ────────────────────────────────────────────────────
function TablesPage({ db, mutate, notify, t }) {
const sections = [...new Set(db.tables.map(t => t.section))];
const toggle = async (num) => {
const tbl = db.tables.find(x => x.num === num);
const next = tbl.status === "free" ? "occupied" : tbl.status === "occupied" ? "reserved" : "free";
await mutate(db => ({ ...db, tables: db.tables.map(x => x.num === num ? { ...x, status: next } : x) }));
notify(`Table ${num}${next}`, "info");
};
const sColors = { free: { bg: "#D1FAE5", border: "#6EE7B7", text: "#065F46" }, occupied: { bg: "#FEF3C7", border: "#FCD34D", text: "#92400E" }, reserved: { bg: "#EDE9FE", border: "#C4B5FD", text: "#5B21B6" } };
return (
<div style={{ padding: 24 }}>
<PageHeader title="Table Map" subtitle="Click a table to cycle status: Free → Occupied → Reserved" t={t}
actions={<><Btn variant="secondary" size="sm">+ Add Table</Btn><Btn variant="primary" size="sm">Floor Plan View</Btn></>} />
<div style={{ display: "flex", gap: 12, marginBottom: 20 }}>
{Object.entries(sColors).map(([s, c]) => (
<div key={s} style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12, fontWeight: 600, color: c.text }}>
<div style={{ width: 10, height: 10, borderRadius: 2, background: c.bg, border: `1.5px solid ${c.border}` }} /> {s} ({db.tables.filter(x => x.status === s).length})
</div>
))}
</div>
{sections.map(sec => (
<div key={sec} style={{ marginBottom: 28 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: t.text2, marginBottom: 12, textTransform: "uppercase", letterSpacing: "0.5px" }}>{sec}</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
{db.tables.filter(tbl => tbl.section === sec).map(tbl => {
const c = sColors[tbl.status];
return (
<div key={tbl.num} onClick={() => toggle(tbl.num)} className="table-tile"
style={{ width: 96, height: 96, borderRadius: 12, border: `2px solid ${c.border}`, background: c.bg, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", cursor: "pointer", transition: ".2s", position: "relative" }}>
<div style={{ position: "absolute", top: 5, right: 7, fontSize: 9, color: c.text }}>👥{tbl.pax}</div>
<div style={{ fontSize: 24, fontWeight: 800, color: c.text }}>{tbl.num}</div>
<div style={{ fontSize: 9, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.5px", color: c.text, marginTop: 4 }}>{tbl.status}</div>
</div>
);
})}
</div>
</div>
))}
</div>
);
}
// ── KITCHEN ───────────────────────────────────────────────────
function KitchenPage({ db, mutate, notify, t }) {
const active = db.orders.filter(o => !["billed", "cancelled"].includes(o.status));
const update = async (id, status) => {
await mutate(db => ({ ...db, orders: db.orders.map(o => o.id === id ? { ...o, status } : o) }));
notify(`Order ${id}${status}`, "info");
};
return (
<div style={{ padding: 24 }}>
<PageHeader title="Kitchen Display System — KOT" subtitle={`${active.length} active orders · Auto-refreshes every 3s`} t={t}
actions={<Btn variant="secondary" size="sm">🖨 Print All KOT</Btn>} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))", gap: 14 }}>
{active.map(o => {
const elapsed = Math.round((Date.now() - o.time) / 60000);
return (
<div key={o.id} style={{ background: t.card, border: `1.5px solid ${statusColor[o.status]}40`, borderTop: `4px solid ${statusColor[o.status]}`, borderRadius: 12, overflow: "hidden" }}>
<div style={{ padding: "12px 14px", background: statusBg[o.status], display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div>
<div style={{ fontSize: 14, fontWeight: 800, color: statusColor[o.status] }}>{o.id}</div>
<div style={{ fontSize: 11, color: "#888" }}>Table {o.table} · {o.waiterName}</div>
</div>
<div style={{ textAlign: "right" }}>
<span style={{ display: "inline-flex", padding: "2px 8px", borderRadius: 99, fontSize: 11, fontWeight: 700, background: statusColor[o.status], color: "#fff" }}>{o.status.toUpperCase()}</span>
<div style={{ fontSize: 11, color: elapsed > 15 ? "#EF4444" : "#888", marginTop: 3, fontWeight: elapsed > 15 ? 700 : 400 }}>⏱ {elapsed}m ago</div>
</div>
</div>
<div style={{ padding: "12px 14px" }}>
{o.items.map((item, i) => (
<div key={i} style={{ display: "flex", gap: 10, padding: "6px 0", borderBottom: `1px solid ${t.border}`, alignItems: "center" }}>
<span style={{ fontWeight: 800, color: "#875BF7", fontSize: 15, minWidth: 24 }}>{item.qty}×</span>
<span style={{ fontSize: 13, color: t.text, flex: 1 }}>{item.name}</span>
</div>
))}
{o.note && <div style={{ marginTop: 8, padding: "6px 10px", background: "#FFFBEB", borderRadius: 6, fontSize: 12, color: "#92400E" }}>📝 {o.note}</div>}
</div>
<div style={{ padding: "10px 14px", borderTop: `1px solid ${t.border}`, display: "flex", gap: 6 }}>
{o.status === "new" && <Btn onClick={() => update(o.id, "preparing")} variant="primary" size="sm" style={{ flex: 1 }}>Start Cooking</Btn>}
{o.status === "preparing" && <Btn onClick={() => update(o.id, "served")} variant="success" size="sm" style={{ flex: 1 }}>Mark Ready ✓</Btn>}
{o.status === "served" && <div style={{ flex: 1, textAlign: "center", fontSize: 12, fontWeight: 700, color: "#039855" }}>✅ Ready to Serve</div>}
<Btn onClick={() => update(o.id, "cancelled")} variant="danger" size="sm">✕</Btn>
</div>
</div>
);
})}
{active.length === 0 && <div style={{ gridColumn: "1/-1", textAlign: "center", padding: "80px 20px", color: t.text3 }}><div style={{ fontSize: 48, marginBottom: 12 }}>🎉</div><div style={{ fontSize: 14 }}>No active kitchen orders</div></div>}
</div>
</div>
);
}
// ── ORDERS ────────────────────────────────────────────────────
function OrdersPage({ db, mutate, notify, t, setModal }) {
const [filter, setFilter] = useState("all");
const filtered = filter === "all" ? db.orders : db.orders.filter(o => o.status === filter);
return (
<div style={{ padding: 24 }}>
<PageHeader title="All Orders" subtitle={`${db.orders.length} total orders`} t={t} />
<div style={{ display: "flex", gap: 6, marginBottom: 20 }}>
{["all","new","preparing","served","billed","cancelled"].map(s => (
<button key={s} onClick={() => setFilter(s)} style={{ padding: "6px 14px", borderRadius: 99, border: `1.5px solid ${filter === s ? "#875BF7" : t.border}`, background: filter === s ? "#875BF7" : t.card, color: filter === s ? "#fff" : t.text2, fontSize: 12, fontWeight: 600, cursor: "pointer" }}>
{s === "all" ? "All" : s} {s !== "all" ? `(${db.orders.filter(o => o.status === s).length})` : ""}
</button>
))}
</div>
<Card t={t} style={{ padding: 0 }}>
<Table t={t} cols={["Order #","Table","Items","Amount","Waiter","Status","Time","Actions"]}
rows={filtered.map(o => [
<strong style={{ color: "#875BF7" }}>{o.id}</strong>,
`Table ${o.table}`,
o.items.map(i => `${i.qty}× ${i.name}`).join(", ").slice(0, 40) + (o.items.map(i => `${i.qty}×${i.name}`).join(", ").length > 40 ? "…" : ""),
<strong style={{ color: "#875BF7" }}>{fmt(o.total)}</strong>,
o.waiterName,
<span style={{ padding: "2px 9px", borderRadius: 99, fontSize: 11, fontWeight: 600, background: statusBg[o.status], color: statusColor[o.status] }}>{o.status}</span>,
fmtTime(o.time),
<div style={{ display: "flex", gap: 4 }}>
{o.status === "served" && <Btn onClick={() => setModal({ type: "bill", order: o })} variant="primary" size="sm">Bill</Btn>}
{o.status !== "billed" && o.status !== "cancelled" && <Btn onClick={() => mutate(db => ({ ...db, orders: db.orders.map(x => x.id === o.id ? { ...x, status: "cancelled" } : x) }))} variant="danger" size="sm">✕</Btn>}
</div>,
])} />
</Card>
</div>
);
}
// ── BILLING ───────────────────────────────────────────────────
function BillingPage({ db, mutate, notify, t, setModal }) {
const pending = db.orders.filter(o => o.status === "served");
const billed = db.orders.filter(o => o.status === "billed");
const settle = async (orderId, method) => {
await mutate(db => ({
...db,
orders: db.orders.map(o => o.id === orderId ? { ...o, status: "billed", paymentMethod: method } : o),
tables: db.tables.map(t => {
const o = db.orders.find(x => x.id === orderId);
return o && t.num === o.table ? { ...t, status: "free", currentOrder: null } : t;
}),
}));
notify(`Payment settled via ${method} ✅`, "success");
};
return (
<div style={{ padding: 24 }}>
<PageHeader title="Billing" subtitle="Process payments and generate invoices" t={t} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 14, marginBottom: 24 }}>
<StatCard icon="⏳" value={pending.length} label="Pending Bills" t={t} color="#F59E0B" />
<StatCard icon="✅" value={billed.length} label="Settled Today" t={t} color="#039855" />
<StatCard icon="💰" value={fmt(billed.reduce((a, o) => a + o.total, 0))} label="Revenue Collected" t={t} color="#875BF7" />
</div>
<div style={{ marginBottom: 20, fontSize: 15, fontWeight: 700, color: t.text }}>Pending Payment</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px,1fr))", gap: 14, marginBottom: 24 }}>
{pending.map(o => (
<Card key={o.id} t={t} style={{ border: "1.5px solid #FCD34D" }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 12 }}>
<div>
<div style={{ fontWeight: 800, color: t.text }}>{o.id}</div>
<div style={{ fontSize: 12, color: t.text2 }}>Table {o.table} · {o.waiterName}</div>
</div>
<div style={{ fontSize: 20, fontWeight: 800, color: "#875BF7" }}>{fmt(o.total)}</div>
</div>
<div style={{ display: "flex", gap: 6, marginTop: 12 }}>
{["Cash","Card","UPI"].map(m => (
<Btn key={m} onClick={() => settle(o.id, m)} variant={m === "UPI" ? "primary" : "secondary"} size="sm" style={{ flex: 1 }}>{m}</Btn>
))}
<Btn onClick={() => setModal({ type: "bill", order: o })} variant="ghost" size="sm">Invoice</Btn>
</div>
</Card>
))}
{pending.length === 0 && <div style={{ color: t.text3, fontSize: 13, padding: 20 }}>No pending bills</div>}
</div>
<div style={{ fontSize: 15, fontWeight: 700, color: t.text, marginBottom: 12 }}>Settled Bills</div>
<Card t={t} style={{ padding: 0 }}>
<Table t={t} cols={["Order #","Table","Amount","Method","Time","Invoice"]}
rows={billed.map(o => [
<strong>{o.id}</strong>, `Table ${o.table}`,
<strong style={{ color: "#875BF7" }}>{fmt(o.total)}</strong>,
<Badge label={o.paymentMethod || "—"} color="green" t={t} />,
fmtTime(o.time),
<Btn onClick={() => setModal({ type: "bill", order: o })} variant="ghost" size="sm">View</Btn>,
])} />
</Card>
</div>
);
}
// ── MENU ──────────────────────────────────────────────────────
function MenuPage({ db, mutate, notify, t, setModal }) {
const [search, setSearch] = useState("");
const [cat, setCat] = useState("all");
const items = db.menuItems.filter(m => (cat === "all" || m.cat === cat) && (!search || m.name.toLowerCase().includes(search.toLowerCase())));
const toggle = async (id) => {
await mutate(db => ({ ...db, menuItems: db.menuItems.map(m => m.id === id ? { ...m, status: m.status === "active" ? "inactive" : "active" } : m) }));
};
const del = async (id) => {
await mutate(db => ({ ...db, menuItems: db.menuItems.filter(m => m.id !== id) }));
notify("Item deleted", "info");
};
return (
<div style={{ padding: 24 }}>
<PageHeader title="Menu Items" subtitle={`${db.menuItems.filter(m => m.status === "active").length} active items`} t={t}
actions={<><Btn variant="secondary" size="sm">Import CSV</Btn><Btn onClick={() => setModal({ type: "addMenu" })} variant="primary" size="sm">+ Add Item</Btn></>} />
<div style={{ display: "flex", gap: 10, marginBottom: 16, alignItems: "center" }}>
<div style={{ display: "flex", alignItems: "center", gap: 6, background: t.surface2, border: `1px solid ${t.border}`, borderRadius: 8, padding: "0 12px", flex: 1 }}>
<span style={{ color: t.text3, fontSize: 13 }}>🔍</span>
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search menu items..." style={{ border: "none", background: "transparent", padding: "9px 0", fontSize: 13, color: t.text, outline: "none", width: "100%" }} />
</div>
<select value={cat} onChange={e => setCat(e.target.value)} style={{ padding: "9px 12px", border: `1px solid ${t.border}`, borderRadius: 8, fontSize: 13, fontFamily: "inherit", background: t.card, color: t.text, cursor: "pointer", outline: "none" }}>
<option value="all">All Categories</option>
{db.categories.map(c => <option key={c.id} value={c.id}>{c.icon} {c.name}</option>)}
</select>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(270px,1fr))", gap: 12 }}>
{items.map(item => {
const category = db.categories.find(c => c.id === item.cat);
return (
<Card key={item.id} t={t} style={{ opacity: item.status === "inactive" ? 0.55 : 1, padding: 0, overflow: "hidden" }}>
<div style={{ padding: "14px 14px 10px", borderBottom: `1px solid ${t.border}`, display: "flex", gap: 12, alignItems: "flex-start" }}>
<div style={{ fontSize: 32, flexShrink: 0 }}>{item.emoji}</div>
<div style={{ flex: 1 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }}>
<span style={{ fontWeight: 700, fontSize: 14, color: t.text }}>{item.name}</span>
<div style={{ width: 10, height: 10, border: `2px solid ${item.type === "veg" ? "#039855" : "#EF4444"}`, borderRadius: 2, display: "flex", alignItems: "center", justifyContent: "center" }}>
<div style={{ width: 5, height: 5, borderRadius: "50%", background: item.type === "veg" ? "#039855" : "#EF4444" }} />
</div>
</div>
<div style={{ fontSize: 11, color: t.text2, marginBottom: 6 }}>{item.desc}</div>
<div style={{ display: "flex", gap: 6 }}>
{category && <span style={{ fontSize: 10, padding: "1px 7px", borderRadius: 99, background: "#EDE9FE", color: "#5B21B6", fontWeight: 600 }}>{category.icon} {category.name}</span>}
</div>
</div>
</div>
<div style={{ padding: "10px 14px", display: "flex", alignItems: "center", gap: 8 }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 800, color: "#875BF7" }}>{fmt(item.price)}</div>
<div style={{ fontSize: 11, color: t.text3 }}>Cost: {fmt(item.cost)} · Margin: {Math.round(((item.price - item.cost) / item.price) * 100)}%</div>
</div>
<Toggle checked={item.status === "active"} onChange={() => toggle(item.id)} />
<Btn onClick={() => del(item.id)} variant="danger" size="sm">🗑</Btn>
</div>
</Card>
);
})}
</div>
</div>
);
}
// ── CATEGORIES ────────────────────────────────────────────────
function CategoriesPage({ db, mutate, notify, t, setModal }) {
return (
<div style={{ padding: 24 }}>
<PageHeader title="Menu Categories" t={t} actions={<Btn variant="primary" size="sm">+ Add Category</Btn>} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(220px,1fr))", gap: 14 }}>
{db.categories.map(c => {
const count = db.menuItems.filter(m => m.cat === c.id).length;
return (
<Card key={c.id} t={t}>
<div style={{ fontSize: 32, marginBottom: 10 }}>{c.icon}</div>
<div style={{ fontWeight: 700, fontSize: 15, color: t.text }}>{c.name}</div>
<div style={{ fontSize: 13, color: t.text2, marginTop: 4 }}>{count} items</div>
<div style={{ marginTop: 12, display: "flex", gap: 6 }}>
<Btn variant="secondary" size="sm" style={{ flex: 1 }}>Edit</Btn>
<Btn variant="danger" size="sm">🗑</Btn>
</div>
</Card>
);
})}
</div>
</div>
);
}
// ── INVENTORY ─────────────────────────────────────────────────
function InventoryPage({ db, mutate, notify, t }) {
const update = async (id, delta) => {
await mutate(db => ({ ...db, inventory: db.inventory.map(i => i.id === id ? { ...i, stock: Math.max(0, i.stock + delta) } : i) }));
};
return (
<div style={{ padding: 24 }}>
<PageHeader title="Inventory" subtitle="Track stock levels and manage supplies" t={t}
actions={<><Btn variant="secondary" size="sm">Purchase Order</Btn><Btn variant="primary" size="sm">+ Add Item</Btn></>} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 14, marginBottom: 20 }}>
<StatCard icon="📦" value={db.inventory.length} label="Total Items" t={t} />
<StatCard icon="⚠️" value={db.inventory.filter(i => i.stock <= i.reorderAt).length} label="Low Stock" t={t} color="#EF4444" />
<StatCard icon="💰" value={fmt(db.inventory.reduce((a, i) => a + i.stock * i.cost, 0))} label="Inventory Value" t={t} color="#875BF7" />
</div>
<Card t={t} style={{ padding: 0 }}>
<Table t={t} cols={["Item","Category","Stock","Unit","Reorder At","Value","Status","Adjust"]}
rows={db.inventory.map(i => [
<strong>{i.name}</strong>,
i.category,
<strong style={{ color: i.stock <= i.reorderAt ? "#EF4444" : t.text }}>{i.stock}</strong>,
i.unit,
i.reorderAt,
fmt(i.stock * i.cost),
<Badge label={i.stock <= i.reorderAt ? "Low Stock" : "OK"} color={i.stock <= i.reorderAt ? "red" : "green"} t={t} />,
<div style={{ display: "flex", gap: 4 }}>
<Btn onClick={() => update(i.id, -1)} variant="secondary" size="sm">−</Btn>
<Btn onClick={() => update(i.id, 10)} variant="success" size="sm">+10</Btn>
</div>,
])} />
</Card>
</div>
);
}
// ── REPORTS ───────────────────────────────────────────────────
function ReportsPage({ db, t }) {
const billed = db.orders.filter(o => o.status === "billed");
const revenue = billed.reduce((a, o) => a + o.total, 0);
const days = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
const vals = [4200,6800,5400,7200,8900,12400,9800];
const max = Math.max(...vals);
const payMethods = ["Cash","Card","UPI"];
const payVals = [45,35,20];
const topItems = db.menuItems.map(m => {
const sold = db.orders.flatMap(o => o.items).filter(i => i.id === m.id).reduce((a, i) => a + i.qty, 0);
return { ...m, sold };
}).sort((a, b) => b.sold - a.sold).slice(0, 5);
return (
<div style={{ padding: 24 }}>
<PageHeader title="Reports & Analytics" t={t}
actions={<><select style={{ padding: "7px 12px", border: `1px solid ${t.border}`, borderRadius: 8, fontSize: 13, fontFamily: "inherit", background: t.card, color: t.text, outline: "none" }}><option>Today</option><option>This Week</option><option>This Month</option></select><Btn variant="primary" size="sm">Export PDF</Btn></>} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 14, marginBottom: 24 }}>
<StatCard icon="💰" value={fmt(revenue)} label="Total Revenue" t={t} color="#875BF7" change="+8.2%" changeUp />
<StatCard icon="🧾" value={db.orders.length} label="Total Orders" t={t} change="+12%" changeUp />
<StatCard icon="📊" value={db.orders.length ? fmt(Math.round(revenue / db.orders.length)) : "₹0"} label="Avg Order Value" t={t} color="#3B82F6" />
<StatCard icon="⭐" value={topItems[0]?.name?.split(" ")[0] || "—"} label="Top Item" t={t} color="#039855" />
</div>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16, marginBottom: 16 }}>
<Card t={t}>
<div style={{ fontWeight: 700, fontSize: 14, color: t.text, marginBottom: 16 }}>Weekly Revenue</div>
<div style={{ display: "flex", alignItems: "flex-end", gap: 8, height: 130 }}>
{days.map((d, i) => (
<div key={d} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 4 }}>
<div style={{ fontSize: 10, color: t.text3 }}>₹{(vals[i]/1000).toFixed(1)}k</div>
<div style={{ width: "100%", borderRadius: "4px 4px 0 0", background: i === 5 ? "#875BF7" : "#EDE9FE", height: (vals[i] / max) * 100, minHeight: 4, transition: ".5s" }} />
<div style={{ fontSize: 11, color: t.text2 }}>{d}</div>
</div>
))}
</div>
</Card>
<Card t={t}>
<div style={{ fontWeight: 700, fontSize: 14, color: t.text, marginBottom: 16 }}>Payment Methods</div>
{payMethods.map((m, i) => (
<div key={m} style={{ marginBottom: 14 }}>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 13, marginBottom: 5, color: t.text }}>
<span>{m}</span><span style={{ fontWeight: 700 }}>{payVals[i]}%</span>
</div>
<div style={{ height: 6, background: t.surface2, borderRadius: 99 }}>
<div style={{ height: "100%", width: `${payVals[i]}%`, background: ["#039855","#3B82F6","#875BF7"][i], borderRadius: 99, transition: ".5s" }} />
</div>
</div>
))}
</Card>
</div>
<Card t={t}>
<div style={{ fontWeight: 700, fontSize: 14, color: t.text, marginBottom: 16 }}>Top Selling Items</div>
<Table t={t} cols={["Item","Category","Sold","Revenue","Margin"]}
rows={topItems.map(item => {
const cat = db.categories.find(c => c.id === item.cat);
const rev = item.sold * item.price;
const margin = Math.round(((item.price - item.cost) / item.price) * 100);
return [
<div style={{ display: "flex", gap: 8, alignItems: "center" }}><span style={{ fontSize: 18 }}>{item.emoji}</span><strong>{item.name}</strong></div>,
cat?.name || item.cat,
item.sold,
<strong style={{ color: "#875BF7" }}>{fmt(rev)}</strong>,
<Badge label={`${margin}%`} color={margin > 50 ? "green" : margin > 30 ? "orange" : "red"} t={t} />,
];
})} />
</Card>
</div>
);
}
// ── ACCOUNTING ────────────────────────────────────────────────
function AccountingPage({ db, t }) {
const revenue = db.orders.filter(o => o.status === "billed").reduce((a, o) => a + o.total, 0);
const expenses = db.expenses?.reduce((a, e) => a + e.amount, 0) || 0;
return (
<div style={{ padding: 24 }}>
<PageHeader title="Accounting" subtitle="General ledger and financial overview" t={t}
actions={<Btn variant="primary" size="sm">Export P&L</Btn>} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 14, marginBottom: 24 }}>
<StatCard icon="📈" value={fmt(revenue)} label="Total Income" t={t} color="#039855" />
<StatCard icon="📉" value={fmt(expenses)} label="Total Expenses" t={t} color="#EF4444" />
<StatCard icon="💼" value={fmt(revenue - expenses)} label="Net Profit" t={t} color="#875BF7" />
</div>
<Card t={t}>
<div style={{ fontWeight: 700, fontSize: 14, color: t.text, marginBottom: 16 }}>Ledger Entries</div>
<Table t={t} cols={["Date","Description","Type","Amount","Balance"]}
rows={[
[fmtDate(Date.now()), "Sales Revenue", <Badge label="Credit" color="green" t={t} />, <span style={{ color: "#039855" }}>+{fmt(revenue)}</span>, fmt(revenue)],
[fmtDate(Date.now() - 86400000), "Inventory Purchase", <Badge label="Debit" color="red" t={t} />, <span style={{ color: "#EF4444" }}>−{fmt(12400)}</span>, fmt(revenue - 12400)],
[fmtDate(Date.now() - 172800000), "Salary Payment", <Badge label="Debit" color="red" t={t} />, <span style={{ color: "#EF4444" }}>−{fmt(expenses)}</span>, fmt(revenue - expenses - 12400)],
]} />
</Card>
</div>
);
}
// ── EXPENSES ──────────────────────────────────────────────────
function ExpensesPage({ db, mutate, notify, t }) {
return (
<div style={{ padding: 24 }}>
<PageHeader title="Expenses" subtitle="Track and manage restaurant expenses" t={t}
actions={<Btn variant="primary" size="sm">+ Add Expense</Btn>} />
<StatCard icon="💸" value={fmt(db.expenses?.reduce((a,e)=>a+e.amount,0)||0)} label="Total Expenses" t={t} color="#EF4444" />
<div style={{ marginTop: 16 }}>
<Card t={t} style={{ padding: 0 }}>
<Table t={t} cols={["Date","Description","Category","Amount","Status","Receipt"]}
rows={(db.expenses||[]).map(e => [
e.date, e.desc, e.category,
<strong style={{ color: "#EF4444" }}>{fmt(e.amount)}</strong>,
<Badge label={e.status} color={e.status === "paid" ? "green" : "orange"} t={t} />,
e.receipt ? "✅" : "❌",
])} />
</Card>
</div>
</div>
);
}
// ── PAYROLL ───────────────────────────────────────────────────
function PayrollPage({ db, t }) {
const staff = db.users.filter(u => u.role !== "admin");
const salaries = { waiter: 18000, cashier: 22000, manager: 35000, kitchen: 20000 };
return (
<div style={{ padding: 24 }}>
<PageHeader title="Payroll" subtitle="Staff salaries and payslip management" t={t}
actions={<><Btn variant="secondary" size="sm">Generate Payslips</Btn><Btn variant="primary" size="sm">Process Payroll</Btn></>} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 14, marginBottom: 20 }}>
<StatCard icon="👥" value={staff.length} label="Total Staff" t={t} />
<StatCard icon="💰" value={fmt(staff.reduce((a,u)=>a+(salaries[u.role]||18000),0))} label="Monthly Payroll" t={t} color="#875BF7" />
<StatCard icon="📅" value="April 1" label="Next Pay Date" t={t} color="#039855" />
</div>
<Card t={t} style={{ padding: 0 }}>
<Table t={t} cols={["Name","Role","Salary","Status","Payslip"]}
rows={staff.map(u => [
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<div style={{ width: 28, height: 28, borderRadius: "50%", background: u.color, display: "flex", alignItems: "center", justifyContent: "center", color: "#fff", fontSize: 12, fontWeight: 700 }}>{u.name[0]}</div>
<strong>{u.name}</strong>
</div>,
u.role,
<strong style={{ color: "#875BF7" }}>{fmt(salaries[u.role]||18000)}</strong>,
<Badge label="Active" color="green" t={t} />,
<Btn variant="ghost" size="sm">View</Btn>,
])} />
</Card>
</div>
);
}
// ── CRM ───────────────────────────────────────────────────────
function CRMPage({ db, mutate, notify, t }) {
const tagColors = { VIP: "purple", Regular: "blue", New: "gray" };
return (
<div style={{ padding: 24 }}>
<PageHeader title="CRM — Customers" subtitle={`${db.customers.length} customers`} t={t}
actions={<Btn variant="primary" size="sm">+ Add Customer</Btn>} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 14, marginBottom: 20 }}>
<StatCard icon="👤" value={db.customers.length} label="Total Customers" t={t} />
<StatCard icon="⭐" value={db.customers.filter(c=>c.tag==="VIP").length} label="VIP Customers" t={t} color="#875BF7" />
<StatCard icon="💰" value={fmt(db.customers.reduce((a,c)=>a+c.totalSpent,0))} label="Total Spent" t={t} color="#039855" />
</div>
<Card t={t} style={{ padding: 0 }}>
<Table t={t} cols={["Name","Phone","Visits","Total Spent","Last Visit","Loyalty Pts","Tag"]}
rows={db.customers.map(c => [
<strong>{c.name}</strong>,
c.phone,
c.visits,
<strong style={{ color: "#875BF7" }}>{fmt(c.totalSpent)}</strong>,
c.lastVisit,
<span style={{ fontWeight: 700, color: "#875BF7" }}>{c.loyaltyPts} pts</span>,
<Badge label={c.tag} color={tagColors[c.tag]||"gray"} t={t} />,
])} />
</Card>
</div>
);
}
// ── EMPLOYEES ─────────────────────────────────────────────────
function EmployeesPage({ db, t }) {
const staff = db.users.filter(u => u.role !== "admin");
return (
<div style={{ padding: 24 }}>
<PageHeader title="Employees" subtitle="HR management and employee records" t={t}
actions={<><Btn variant="secondary" size="sm">Import</Btn><Btn variant="primary" size="sm">+ Add Employee</Btn></>} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(240px,1fr))", gap: 14 }}>
{staff.map(u => (
<Card key={u.id} t={t}>
<div style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 14 }}>
<div style={{ width: 44, height: 44, borderRadius: "50%", background: u.color, display: "flex", alignItems: "center", justifyContent: "center", color: "#fff", fontSize: 18, fontWeight: 700 }}>{u.name[0]}</div>
<div>
<div style={{ fontWeight: 700, fontSize: 14, color: t.text }}>{u.name}</div>
<div style={{ fontSize: 12, color: t.text2 }}>@{u.id} · {u.role}</div>
</div>
</div>
<div style={{ fontSize: 12, color: t.text2 }}>{u.email}</div>
<div style={{ fontSize: 12, color: t.text2, marginTop: 3 }}>{u.phone}</div>
<div style={{ marginTop: 10, display: "flex", gap: 6 }}>
<Badge label={u.active ? "Active" : "Inactive"} color={u.active ? "green" : "red"} t={t} />
<Badge label={u.role} color="purple" t={t} />
</div>
</Card>
))}
</div>
</div>
);
}
// ── USERS (Admin) ─────────────────────────────────────────────
function UsersPage({ db, mutate, notify, t, setModal, currentUser }) {
const nonAdmin = db.users.filter(u => u.role !== "admin");
const toggleActive = async (id) => {
await mutate(db => ({ ...db, users: db.users.map(u => u.id === id ? { ...u, active: !u.active } : u) }));
const u = db.users.find(x => x.id === id);
notify(`${u?.name} ${u?.active ? "access revoked" : "restored"}`, "info");
};
const copyLink = (u) => {
const link = `${window.location.href.split("?")[0]}?user=${u.id}`;
navigator.clipboard?.writeText(link).then(() => notify("Share link copied! 📋", "success")).catch(() => notify("Link: " + link, "info"));
};
return (
<div style={{ padding: 24 }}>
<PageHeader title="Users & Staff" subtitle="Manage user accounts and access" t={t}
actions={<Btn onClick={() => setModal({ type: "addUser" })} variant="primary" size="sm">+ Add User</Btn>} />
{nonAdmin.map(u => (
<Card key={u.id} t={t} style={{ marginBottom: 14 }}>
<div style={{ display: "flex", gap: 14, alignItems: "flex-start" }}>
<div style={{ width: 48, height: 48, borderRadius: "50%", background: u.color, display: "flex", alignItems: "center", justifyContent: "center", color: "#fff", fontSize: 20, fontWeight: 700, flexShrink: 0 }}>{u.name[0]}</div>
<div style={{ flex: 1 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 3 }}>
<span style={{ fontWeight: 700, fontSize: 15, color: t.text }}>{u.name}</span>
<Badge label={u.active ? "Active" : "Inactive"} color={u.active ? "green" : "red"} t={t} />
<Badge label={u.role} color="purple" t={t} />
</div>
<div style={{ fontSize: 12, color: t.text2, marginBottom: 6 }}>@{u.id} · {u.email} · {u.phone}</div>
<div onClick={() => copyLink(u)} style={{ fontSize: 11, color: "#3B82F6", cursor: "pointer", fontFamily: "monospace", background: "#EFF6FF", display: "inline-block", padding: "3px 10px", borderRadius: 6 }}>
🔗 Share login link (click to copy)
</div>
<div style={{ marginTop: 10, display: "flex", flexWrap: "wrap", gap: 6 }}>
{u.permissions.map(p => {
const m = MODULES.find(x => x.id === p);
return m ? <span key={p} style={{ fontSize: 10, padding: "1px 7px", borderRadius: 99, background: "#EDE9FE", color: "#5B21B6", fontWeight: 600 }}>{m.icon} {m.label}</span> : null;
})}
</div>
</div>
<div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
<Btn onClick={() => setModal({ type: "editPerms", userId: u.id })} variant="ghost" size="sm">🔐 Permissions</Btn>
<Btn onClick={() => toggleActive(u.id)} variant={u.active ? "danger" : "success"} size="sm">{u.active ? "Revoke" : "Restore"}</Btn>
</div>
</div>
</Card>
))}
</div>
);
}
// ── ACCESS CONTROL (Admin) ────────────────────────────────────
function AccessPage({ db, mutate, notify, t }) {
const nonAdmin = db.users.filter(u => u.role !== "admin");
const toggle = async (userId, modId, val) => {
await mutate(db => ({
...db,
users: db.users.map(u => u.id !== userId ? u : {
...u,
permissions: val ? [...new Set([...u.permissions, modId])] : u.permissions.filter(p => p !== modId)
})
}));
const u = db.users.find(x => x.id === userId);
const m = MODULES.find(x => x.id === modId);
notify(`${u?.name}: ${m?.label} ${val ? "granted ✅" : "revoked ❌"}`, "info");
};
const grantAll = async (userId) => {
await mutate(db => ({ ...db, users: db.users.map(u => u.id === userId ? { ...u, permissions: MODULES.map(m => m.id) } : u) }));
notify("All modules granted", "success");
};
const revokeAll = async (userId) => {
await mutate(db => ({ ...db, users: db.users.map(u => u.id === userId ? { ...u, permissions: [] } : u) }));
notify("All access revoked", "info");
};
const groups = [...new Set(MODULES.map(m => m.group))];
return (
<div style={{ padding: 24 }}>
<PageHeader title="Access Control" subtitle="Grant or revoke module permissions per user" t={t} />
{nonAdmin.map(u => (
<Card key={u.id} t={t} style={{ marginBottom: 16 }}>
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16 }}>
<div style={{ width: 40, height: 40, borderRadius: "50%", background: u.color, display: "flex", alignItems: "center", justifyContent: "center", color: "#fff", fontSize: 16, fontWeight: 700 }}>{u.name[0]}</div>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 700, color: t.text }}>{u.name}</div>
<div style={{ fontSize: 12, color: t.text2 }}>@{u.id} · {u.role} · {u.permissions.length}/{MODULES.length} modules</div>
</div>
<Btn onClick={() => grantAll(u.id)} variant="success" size="sm">Grant All</Btn>
<Btn onClick={() => revokeAll(u.id)} variant="danger" size="sm">Revoke All</Btn>
</div>
{groups.map(g => {
const mods = MODULES.filter(m => m.group === g);
return (
<div key={g} style={{ marginBottom: 12 }}>
<div style={{ fontSize: 11, fontWeight: 700, color: t.text3, textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 6 }}>{g}</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
{mods.map(m => {
const has = u.permissions.includes(m.id);
return (
<div key={m.id} onClick={() => toggle(u.id, m.id, !has)}
style={{ display: "flex", alignItems: "center", gap: 6, padding: "5px 10px", borderRadius: 99, border: `1.5px solid ${has ? "#875BF7" : t.border}`, background: has ? "#EDE9FE" : t.surface2, cursor: "pointer", fontSize: 12, fontWeight: 600, color: has ? "#5B21B6" : t.text2, transition: ".15s" }}>
<span style={{ fontSize: 12 }}>{m.icon}</span> {m.label}
{has && <span style={{ fontSize: 10, color: "#875BF7" }}></span>}
</div>
);
})}
</div>
</div>
);
})}
</Card>
))}
</div>
);
}
// ── SETTINGS ──────────────────────────────────────────────────
function SettingsPage({ db, mutate, notify, t }) {
const [s, setS] = useState(db.settings);
const save = async () => {
await mutate(db => ({ ...db, settings: s }));
notify("Settings saved ✅", "success");
};
return (
<div style={{ padding: 24 }}>
<PageHeader title="Settings" subtitle="Restaurant configuration and preferences" t={t}
actions={<Btn onClick={save} variant="primary">Save Changes</Btn>} />
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, maxWidth: 800 }}>
<Card t={t}>
<div style={{ fontWeight: 700, fontSize: 14, color: t.text, marginBottom: 16 }}>Restaurant Info</div>
<Input label="Restaurant Name" value={s.restaurantName} onChange={v => setS(x => ({ ...x, restaurantName: v }))} />
<Input label="Address" value={s.address} onChange={v => setS(x => ({ ...x, address: v }))} />
<Input label="Phone" value={s.phone} onChange={v => setS(x => ({ ...x, phone: v }))} />
<Input label="GST Number" value={s.gst} onChange={v => setS(x => ({ ...x, gst: v }))} />
<Input label="FSSAI License" value={s.fssai} onChange={v => setS(x => ({ ...x, fssai: v }))} />
</Card>
<Card t={t}>
<div style={{ fontWeight: 700, fontSize: 14, color: t.text, marginBottom: 16 }}>Tax & Charges</div>
<Input label="GST Rate (%)" type="number" value={String(s.gstRate)} onChange={v => setS(x => ({ ...x, gstRate: Number(v) }))} />
<Input label="Service Charge (%)" type="number" value={String(s.serviceCharge)} onChange={v => setS(x => ({ ...x, serviceCharge: Number(v) }))} />
<div style={{ marginBottom: 14 }}>
<label style={{ display: "block", fontSize: 12, fontWeight: 600, color: "#374151", marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.4px" }}>Theme</label>
<div style={{ display: "flex", gap: 8 }}>
{["light","dark"].map(th => (
<button key={th} onClick={() => setS(x => ({ ...x, theme: th }))}
style={{ flex: 1, padding: "10px", borderRadius: 8, border: `2px solid ${s.theme === th ? "#875BF7" : t.border}`, background: s.theme === th ? "#EDE9FE" : t.surface2, color: s.theme === th ? "#875BF7" : t.text2, fontWeight: 600, fontSize: 13, cursor: "pointer" }}>
{th === "light" ? "☀️ Light" : "🌙 Dark"}
</button>
))}
</div>
</div>
<Input label="Logo Emoji" value={s.logo} onChange={v => setS(x => ({ ...x, logo: v }))} />
</Card>
</div>
</div>
);
}
// ── GENERIC PAGE ──────────────────────────────────────────────
function GenericPage({ title, icon, theme: t }) {
return (
<div style={{ padding: 24 }}>
<PageHeader title={`${icon} ${title}`} subtitle="Module coming soon" t={t} />
<Card t={t} style={{ textAlign: "center", padding: "60px 40px" }}>
<div style={{ fontSize: 56, marginBottom: 14 }}>{icon}</div>
<div style={{ fontSize: 18, fontWeight: 700, color: t.text }}>{title}</div>
<div style={{ fontSize: 13, color: t.text2, marginTop: 6 }}>This module is available and can be fully configured.</div>
<Btn variant="primary" style={{ marginTop: 20 }}>Configure Module</Btn>
</Card>
</div>
);
}
// ── MODAL LAYER ───────────────────────────────────────────────
function ModalLayer({ modal, setModal, db, mutate, notify, theme: t, currentUser }) {
const close = () => setModal(null);
if (modal.type === "bill") {
const o = modal.order;
return (
<ModalShell title={`Invoice — ${o.id}`} onClose={close} t={t} width={440}>
<div style={{ textAlign: "center", marginBottom: 20, padding: "0 0 16px", borderBottom: `1px dashed ${t.border}` }}>
<div style={{ fontSize: 28 }}>{db.settings.logo}</div>
<div style={{ fontWeight: 800, fontSize: 18, color: t.text }}>{db.settings.restaurantName}</div>
<div style={{ fontSize: 12, color: t.text2 }}>{db.settings.address}</div>
<div style={{ fontSize: 12, color: t.text2 }}>GST: {db.settings.gst}</div>
<div style={{ marginTop: 10, fontSize: 13, color: t.text2 }}>Table {o.table} · {fmtTime(o.time)} · {o.waiterName}</div>
<div style={{ fontWeight: 700, color: "#875BF7" }}>{o.id}</div>
</div>
{o.items.map((item, i) => (
<div key={i} style={{ display: "flex", justifyContent: "space-between", fontSize: 13, padding: "6px 0", color: t.text }}>
<span>{item.qty}× {item.name}</span><span style={{ fontWeight: 600 }}>{fmt(item.price * item.qty)}</span>
</div>
))}
<div style={{ marginTop: 12, paddingTop: 12, borderTop: `1px dashed ${t.border}` }}>
{[["Subtotal", fmt(o.subtotal)], [`GST (${db.settings.gstRate}%)`, fmt(o.taxAmount)], [`Service (${db.settings.serviceCharge}%)`, fmt(Math.round(o.subtotal * db.settings.serviceCharge / 100))]].map(([k, v]) => (
<div key={k} style={{ display: "flex", justifyContent: "space-between", fontSize: 12, color: t.text2, marginBottom: 4 }}><span>{k}</span><span>{v}</span></div>
))}
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 18, fontWeight: 800, marginTop: 10, color: t.text }}>
<span>TOTAL</span><span style={{ color: "#875BF7" }}>{fmt(o.total)}</span>
</div>
</div>
<div style={{ textAlign: "center", marginTop: 16, fontSize: 12, color: t.text3, paddingTop: 12, borderTop: `1px dashed ${t.border}` }}>Thank you for dining with us! 🙏</div>
<div style={{ marginTop: 16, display: "flex", gap: 8 }}>
<Btn onClick={close} variant="secondary" style={{ flex: 1 }}>Close</Btn>
<Btn onClick={() => { window.print(); }} variant="primary" style={{ flex: 1 }}>🖨️ Print</Btn>
</div>
</ModalShell>
);
}
if (modal.type === "addUser") {
const [form, setForm] = useState({ name: "", id: "", pass: "", email: "", phone: "", role: "waiter", permissions: [...INITIAL_PERMS] });
const togglePerm = (p) => setForm(f => ({ ...f, permissions: f.permissions.includes(p) ? f.permissions.filter(x => x !== p) : [...f.permissions, p] }));
const save = async () => {
if (!form.name || !form.id || !form.pass) { notify("Fill all required fields", "error"); return; }
if (db.users.find(u => u.id === form.id)) { notify("Login ID already exists", "error"); return; }
await mutate(db => ({ ...db, users: [...db.users, { ...form, color: COLORS[Math.floor(Math.random() * COLORS.length)], active: true, pin: "0000" }] }));
notify(`${form.name} created successfully ✅`, "success");
close();
};
return (
<ModalShell title="Add New User" onClose={close} t={t}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Input label="Full Name *" value={form.name} onChange={v => setForm(f => ({ ...f, name: v }))} placeholder="Ravi Kumar" />
<Input label="Login ID *" value={form.id} onChange={v => setForm(f => ({ ...f, id: v }))} placeholder="waiter3" />
<Input label="Password *" type="password" value={form.pass} onChange={v => setForm(f => ({ ...f, pass: v }))} placeholder="••••••" />
<div>
<label style={{ display: "block", fontSize: 12, fontWeight: 600, color: "#374151", marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.4px" }}>Role</label>
<select value={form.role} onChange={e => setForm(f => ({ ...f, role: e.target.value }))} style={{ width: "100%", padding: "10px 12px", border: "1.5px solid #D1D5DB", borderRadius: 8, fontSize: 13, fontFamily: "inherit", outline: "none" }}>
{["waiter","cashier","manager","kitchen"].map(r => <option key={r} value={r}>{r}</option>)}
</select>
</div>
<Input label="Email" value={form.email} onChange={v => setForm(f => ({ ...f, email: v }))} placeholder="email@restaurant.com" />
<Input label="Phone" value={form.phone} onChange={v => setForm(f => ({ ...f, phone: v }))} placeholder="+91 98765 43210" />
</div>
<div style={{ marginTop: 8 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: "#374151", marginBottom: 10, textTransform: "uppercase", letterSpacing: "0.4px" }}>Module Permissions</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6, maxHeight: 200, overflowY: "auto" }}>
{MODULES.map(m => {
const has = form.permissions.includes(m.id);
return <div key={m.id} onClick={() => togglePerm(m.id)} style={{ padding: "4px 10px", borderRadius: 99, border: `1.5px solid ${has ? "#875BF7" : "#E5E7EB"}`, background: has ? "#EDE9FE" : "#F9FAFB", color: has ? "#5B21B6" : "#6B7280", fontSize: 12, fontWeight: 600, cursor: "pointer" }}>{m.icon} {m.label}</div>;
})}
</div>
</div>
<div style={{ display: "flex", gap: 8, marginTop: 16 }}>
<Btn onClick={close} variant="secondary" style={{ flex: 1 }}>Cancel</Btn>
<Btn onClick={save} variant="primary" style={{ flex: 1 }}>Create User</Btn>
</div>
</ModalShell>
);
}
if (modal.type === "editPerms") {
const user = db.users.find(u => u.id === modal.userId);
if (!user) return null;
const [perms, setPerms] = useState([...user.permissions]);
const togglePerm = (p) => setPerms(ps => ps.includes(p) ? ps.filter(x => x !== p) : [...ps, p]);
const save = async () => {
await mutate(db => ({ ...db, users: db.users.map(u => u.id === modal.userId ? { ...u, permissions: perms } : u) }));
notify(`Permissions updated for ${user.name} ✅`, "success");
close();
};
const groups = [...new Set(MODULES.map(m => m.group))];
return (
<ModalShell title={`Permissions — ${user.name}`} onClose={close} t={t} width={640}>
<div style={{ display: "flex", gap: 8, marginBottom: 14 }}>
<Btn onClick={() => setPerms(MODULES.map(m => m.id))} variant="success" size="sm">Grant All</Btn>
<Btn onClick={() => setPerms([])} variant="danger" size="sm">Revoke All</Btn>
<span style={{ marginLeft: "auto", fontSize: 13, color: t.text2 }}>{perms.length}/{MODULES.length} modules</span>
</div>
<div style={{ maxHeight: 360, overflowY: "auto" }}>
{groups.map(g => (
<div key={g} style={{ marginBottom: 12 }}>
<div style={{ fontSize: 11, fontWeight: 700, color: t.text3, textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 6 }}>{g}</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
{MODULES.filter(m => m.group === g).map(m => {
const has = perms.includes(m.id);
return <div key={m.id} onClick={() => togglePerm(m.id)} style={{ padding: "4px 10px", borderRadius: 99, border: `1.5px solid ${has ? "#875BF7" : "#E5E7EB"}`, background: has ? "#EDE9FE" : "#F9FAFB", color: has ? "#5B21B6" : "#6B7280", fontSize: 12, fontWeight: 600, cursor: "pointer" }}>{m.icon} {m.label} {has ? "✓" : ""}</div>;
})}
</div>
</div>
))}
</div>
<div style={{ marginTop: 14, padding: 12, background: "#EFF6FF", borderRadius: 8 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: "#1E40AF", marginBottom: 4 }}>Shareable Login Link</div>
<div style={{ fontFamily: "monospace", fontSize: 11, color: "#3B82F6", cursor: "pointer", wordBreak: "break-all" }} onClick={() => { navigator.clipboard?.writeText(`${window.location.href.split("?")[0]}?user=${user.id}`); notify("Link copied! 📋", "success"); }}>
{window.location.href.split("?")[0]}?user={user.id} (click to copy)
</div>
</div>
<div style={{ display: "flex", gap: 8, marginTop: 14 }}>
<Btn onClick={close} variant="secondary" style={{ flex: 1 }}>Cancel</Btn>
<Btn onClick={save} variant="primary" style={{ flex: 1 }}>Save Permissions</Btn>
</div>
</ModalShell>
);
}
if (modal.type === "addMenu") {
const [form, setForm] = useState({ name: "", cat: db.categories[0]?.id || "", price: "", cost: "", type: "veg", emoji: "🍽", desc: "", status: "active", tax: 5 });
const save = async () => {
if (!form.name || !form.price) { notify("Enter name and price", "error"); return; }
await mutate(db => ({ ...db, menuItems: [...db.menuItems, { ...form, id: Date.now(), price: Number(form.price), cost: Number(form.cost), tax: Number(form.tax), stock: 999 }] }));
notify(`${form.name} added to menu ✅`, "success");
close();
};
return (
<ModalShell title="Add Menu Item" onClose={close} t={t}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Input label="Item Name *" value={form.name} onChange={v => setForm(f => ({ ...f, name: v }))} placeholder="Chicken Biryani" />
<div>
<label style={{ display: "block", fontSize: 12, fontWeight: 600, color: "#374151", marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.4px" }}>Category</label>
<select value={form.cat} onChange={e => setForm(f => ({ ...f, cat: e.target.value }))} style={{ width: "100%", padding: "10px 12px", border: "1.5px solid #D1D5DB", borderRadius: 8, fontSize: 13, fontFamily: "inherit", outline: "none" }}>
{db.categories.map(c => <option key={c.id} value={c.id}>{c.icon} {c.name}</option>)}
</select>
</div>
<Input label="Price (₹) *" type="number" value={form.price} onChange={v => setForm(f => ({ ...f, price: v }))} placeholder="320" />
<Input label="Cost (₹)" type="number" value={form.cost} onChange={v => setForm(f => ({ ...f, cost: v }))} placeholder="130" />
<Input label="Emoji" value={form.emoji} onChange={v => setForm(f => ({ ...f, emoji: v }))} placeholder="🍚" />
<div>
<label style={{ display: "block", fontSize: 12, fontWeight: 600, color: "#374151", marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.4px" }}>Type</label>
<select value={form.type} onChange={e => setForm(f => ({ ...f, type: e.target.value }))} style={{ width: "100%", padding: "10px 12px", border: "1.5px solid #D1D5DB", borderRadius: 8, fontSize: 13, fontFamily: "inherit", outline: "none" }}>
<option value="veg">🟢 Vegetarian</option>
<option value="nonveg">🔴 Non-Vegetarian</option>
</select>
</div>
</div>
<div style={{ marginTop: 4 }}>
<label style={{ display: "block", fontSize: 12, fontWeight: 600, color: "#374151", marginBottom: 5, textTransform: "uppercase", letterSpacing: "0.4px" }}>Description</label>
<textarea value={form.desc} onChange={e => setForm(f => ({ ...f, desc: e.target.value }))} placeholder="Brief description of the dish..." style={{ width: "100%", padding: "10px 12px", border: "1.5px solid #D1D5DB", borderRadius: 8, fontSize: 13, fontFamily: "inherit", resize: "none", height: 70, outline: "none" }} />
</div>
<div style={{ display: "flex", gap: 8, marginTop: 12 }}>
<Btn onClick={close} variant="secondary" style={{ flex: 1 }}>Cancel</Btn>
<Btn onClick={save} variant="primary" style={{ flex: 1 }}>Add Item</Btn>
</div>
</ModalShell>
);
}
return null;
}
function ModalShell({ title, onClose, children, t, width = 540 }) {
return (
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", backdropFilter: "blur(4px)", zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center", padding: 20 }} onClick={onClose}>
<div style={{ background: t?.card || "#fff", border: `1px solid ${t?.cardBorder || "#E5E7EB"}`, borderRadius: 16, width, maxWidth: "100%", maxHeight: "88vh", overflow: "auto", boxShadow: "0 24px 60px rgba(0,0,0,0.18)", animation: "fadeIn 0.2s ease" }} onClick={e => e.stopPropagation()}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "16px 20px", borderBottom: `1px solid ${t?.border || "#E5E7EB"}` }}>
<h3 style={{ fontSize: 15, fontWeight: 800, color: t?.text || "#111" }}>{title}</h3>
<button onClick={onClose} style={{ background: "none", border: "none", cursor: "pointer", fontSize: 18, color: t?.text2 || "#888", lineHeight: 1 }}></button>
</div>
<div style={{ padding: 20 }}>{children}</div>
</div>
</div>
);
}