// -- تحويل للنشر بدون أدوات بناء (Vite/Webpack): بنستخدم المتغيرات العامة يلي محمّلة من CDN بملف index.html -- const { useState, useMemo, useEffect, useRef } = React; const TABLES = Array.from({ length: 10 }, (_, i) => i + 1); const LOW_STOCK_THRESHOLD = 5; const PERM_LIST = [ { id: "editMenu", label: "تعديل القائمة (إضافة/تعديل/حذف صنف)" }, { id: "deleteOrder", label: "حذف أو إلغاء طلب" }, { id: "applyDiscount", label: "تطبيق خصم على الفاتورة" }, { id: "viewReports", label: "عرض التقارير" }, { id: "manageInventory", label: "إدارة المخزون" }, { id: "addToOpenOrder", label: "إضافة أصناف لطلب طاولة مفتوح" }, { id: "removeFromOpenOrder", label: "حذف/إنقاص أصناف من طلب طاولة مفتوح" }, ]; const DEFAULT_STAFF = [ { id: "s1", name: "أحمد", type: "نادل", assignedZone: "ground", username: "ahmad", password: "1111", permissions: { editMenu: false, deleteOrder: false, applyDiscount: false, viewReports: false, manageInventory: false, addToOpenOrder: true, removeFromOpenOrder: true } }, { id: "s2", name: "ليلى", type: "نادل", assignedZone: "roof", username: "laila", password: "1111", permissions: { editMenu: false, deleteOrder: false, applyDiscount: true, viewReports: false, manageInventory: false, addToOpenOrder: false, removeFromOpenOrder: false } }, { id: "s3", name: "سارة", type: "كاشير", assignedZone: "roof", username: "sara", password: "2222", permissions: { editMenu: true, deleteOrder: true, applyDiscount: true, viewReports: false, manageInventory: true, addToOpenOrder: true, removeFromOpenOrder: true } }, { id: "s4", name: "خالد", type: "كاشير", assignedZone: "ground", username: "khaled", password: "2222", permissions: { editMenu: false, deleteOrder: true, applyDiscount: false, viewReports: false, manageInventory: false, addToOpenOrder: false, removeFromOpenOrder: false } }, ]; const DEFAULT_ZONES = [ { id: "ground", name: "الطابق الأرضي", tables: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] }, { id: "roof", name: "الروف", tables: ["1", "2", "3", "4", "5", "6"] }, ]; const CATEGORY_STYLE = { hot: { icon: "☕", grad: "linear-gradient(135deg,#fff4e8,#ffe7cc)" }, cold: { icon: "🧊", grad: "linear-gradient(135deg,#e8f7fb,#d4eef6)" }, hot_drinks: { icon: "🍵", grad: "linear-gradient(135deg,#eef8ea,#dcf0d1)" }, cold_drinks: { icon: "🥤", grad: "linear-gradient(135deg,#e6f2ff,#d2e6ff)" }, juices: { icon: "🧃", grad: "linear-gradient(135deg,#fff8e0,#ffedb0)" }, mojito: { icon: "🍹", grad: "linear-gradient(135deg,#f6edff,#ecdcff)" }, hookah: { icon: "💨", grad: "linear-gradient(135deg,#eef1f6,#dde2ec)" }, snacks: { icon: "🍿", grad: "linear-gradient(135deg,#fff1e0,#ffe1bd)" }, addons: { icon: "➕", grad: "linear-gradient(135deg,#fdeaf1,#fbd7e5)" }, }; const CATEGORY_STATION = { hot: "bar", cold: "bar", hot_drinks: "bar", cold_drinks: "bar", juices: "bar", mojito: "bar", addons: "bar", hookah: "hookah", snacks: "kitchen", }; const STATION_LABEL = { bar: "☕ الباريستا", kitchen: "🍽️ المطبخ", cashier: "💰 الكاشير", hookah: "💨 الأراجيل" }; const DEFAULT_PRINTERS = [ { id: "p1", name: "طابعة الكاشير - الأرضي", role: "cashier", zoneId: "ground", ip: "192.168.1.50" }, { id: "p2", name: "طابعة الباريستا - الأرضي", role: "bar", zoneId: "ground", ip: "192.168.1.51" }, { id: "p3", name: "طابعة المطبخ", role: "kitchen", zoneId: "all", ip: "192.168.1.52" }, { id: "p4", name: "طابعة الأراجيل - الروف", role: "hookah", zoneId: "roof", ip: "192.168.1.53" }, ]; const ICE_GROUP = { id: "ice", name: "الثلج", type: "single", options: ["عادي", "قليل", "بدون ثلج"] }; const SUGAR_GROUP = { id: "sugar", name: "درجة السكر", type: "single", options: ["سادة", "وسط", "حلوة"] }; const MOD_PRESETS = [ { key: "sugar", label: "🍬 درجة السكر", group: { name: "درجة السكر", type: "single", options: ["سادة", "وسط", "حلوة"] } }, { key: "ice", label: "🧊 الثلج", group: { name: "الثلج", type: "single", options: ["عادي", "قليل", "بدون ثلج"] } }, { key: "size", label: "📏 الحجم", group: { name: "الحجم", type: "single", options: ["صغير", "وسط", "كبير"] } }, { key: "coffeeFlavor", label: "☕ نكهة قهوة", group: { name: "النكهة", type: "single", options: ["قهوة", "شوكولاتة", "كراميل"] } }, { key: "juiceFlavor", label: "🍓 نكهة عصير", group: { name: "النكهة", type: "single", options: ["توت", "عنب", "برتقال", "مانجا", "فراولة"] } }, { key: "milk", label: "🥛 نوع الحليب", group: { name: "نوع الحليب", type: "single", options: ["عادي", "لايت", "بدون حليب"] } }, { key: "addons", label: "➕ إضافات", group: { name: "إضافات", type: "multi", options: ["زيتون", "جبنة إضافية", "بدون بندورة"] } }, { key: "nosugar", label: "🚫 بدون سكر", group: { name: "إضافات", type: "multi", options: ["بدون سكر"] } }, ]; const DEFAULT_MENU = [ { catId: "hot", catName: "قهوة ساخنة", items: [ { id: "esp_double", name: "إسبريسو دبل", price: 1.0, stock: 40, icon: "☕", modifierGroups: [SUGAR_GROUP] }, { id: "amc", name: "أمريكانو", price: 1.0, stock: 40, icon: "☕", modifierGroups: [SUGAR_GROUP] }, { id: "flat_white", name: "فلات وايت", price: 1.5, stock: 30, icon: "☕", modifierGroups: [SUGAR_GROUP] }, { id: "cap", name: "كابتشينو", price: 1.75, stock: 30, icon: "☕", modifierGroups: [SUGAR_GROUP] }, { id: "lat", name: "لاتيه", price: 1.75, stock: 30, icon: "☕", modifierGroups: [SUGAR_GROUP] }, { id: "mocha_hot", name: "موكا شوكلت", price: 1.75, stock: 25, icon: "☕", modifierGroups: [SUGAR_GROUP] }, { id: "spanish_lat", name: "سبانش لاتيه", price: 1.75, stock: 25, icon: "☕", modifierGroups: [SUGAR_GROUP] }, { id: "trk", name: "قهوة تركية (العميد)", price: 1.5, stock: 25, icon: "☕", modifierGroups: [SUGAR_GROUP] }, ]}, { catId: "cold", catName: "قهوة باردة", items: [ { id: "esp_freddo", name: "إسبريسو الفريدو", price: 1.0, stock: 20, icon: "🧊", modifierGroups: [SUGAR_GROUP] }, { id: "iced_lat", name: "لاتيه مثلج", price: 1.75, stock: 20, icon: "🧊", modifierGroups: [SUGAR_GROUP, ICE_GROUP] }, { id: "frap", name: "فرابتشينو", price: 2.0, stock: 15, icon: "🥤", modifierGroups: [SUGAR_GROUP, ICE_GROUP] }, { id: "caramel_mac", name: "كراميل مكياتو", price: 1.75, stock: 15, icon: "🧊", modifierGroups: [SUGAR_GROUP, ICE_GROUP] }, { id: "amc_cold", name: "أمريكانو بارد", price: 1.0, stock: 20, icon: "🧊", modifierGroups: [SUGAR_GROUP, ICE_GROUP] }, { id: "mocha_cold", name: "موكا مثلجة", price: 1.75, stock: 20, icon: "🧊", modifierGroups: [SUGAR_GROUP, ICE_GROUP] }, ]}, { catId: "hot_drinks", catName: "مشاريب ساخنة", items: [ { id: "hcho", name: "هوت شوكلت", price: 1.5, stock: 15, icon: "🍫", modifierGroups: [] }, { id: "tea", name: "شاي", price: 1.0, stock: 50, icon: "🍵", modifierGroups: [SUGAR_GROUP] }, { id: "tea_karak", name: "شاي كرك", price: 1.5, stock: 30, icon: "🍵", modifierGroups: [SUGAR_GROUP] }, { id: "nescafe", name: "نسكافيه", price: 1.0, stock: 25, icon: "☕", modifierGroups: [SUGAR_GROUP] }, { id: "herbs", name: "أعشاب", price: 1.0, stock: 20, icon: "🌿", modifierGroups: [] }, ]}, { catId: "cold_drinks", catName: "مشاريب باردة", items: [ { id: "milkshake", name: "ميلك شيك", price: 2.5, stock: 20, icon: "🥤", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["سنكرز", "فانيلا", "شوكلت", "أوريو", "مانجو", "فراولة", "كروكان", "تشيز كيك", "لوتس"] }] }, { id: "iced_tea", name: "شاي مثلج", price: 2.0, stock: 20, icon: "🧊", modifierGroups: [] }, { id: "soda", name: "مشاريب غازية", price: 1.0, stock: 40, icon: "🥤", modifierGroups: [{ id: "flavor", name: "النوع", type: "single", options: ["سڤن", "بيبسي", "مرندا"] }] }, { id: "smoothie", name: "سموذي", price: 2.0, stock: 15, icon: "🥤", modifierGroups: [] }, { id: "cocktail", name: "كوكتيلات", price: 2.5, stock: 15, icon: "🍹", modifierGroups: [] }, { id: "berrican", name: "ببريكان", price: 1.5, stock: 15, icon: "🥤", modifierGroups: [] }, ]}, { catId: "juices", catName: "العصائر الطبيعية", items: [ { id: "juice_mango", name: "عصير مانجو", price: 2.0, stock: 15, icon: "🧃", modifierGroups: [] }, { id: "juice_lemon_mint", name: "عصير ليمون ونعنع", price: 2.0, stock: 15, icon: "🧃", modifierGroups: [] }, { id: "juice_lemon", name: "عصير ليمون", price: 2.0, stock: 15, icon: "🧃", modifierGroups: [] }, { id: "juice_strawberry", name: "عصير فراولة", price: 2.0, stock: 15, icon: "🧃", modifierGroups: [] }, { id: "juice_guava", name: "عصير جوافة", price: 2.0, stock: 15, icon: "🧃", modifierGroups: [] }, ]}, { catId: "mojito", catName: "موهيتو", items: [ { id: "mojito_7up", name: "موهيتو سڤن", price: 1.75, stock: 20, icon: "🍹", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["كيوي", "بلوبيري", "بطيخ", "مكس بيري", "أناناس", "ستروبيري", "مانجو", "خوخ"] }] }, { id: "mojito_bm", name: "موهيتو BM", price: 2.0, stock: 20, icon: "🍹", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["كيوي", "بلوبيري", "بطيخ", "مكس بيري", "أناناس", "ستروبيري", "مانجو", "خوخ"] }] }, { id: "mojito_boom", name: "موهيتو بوم بوم", price: 2.25, stock: 20, icon: "🍹", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["كيوي", "بلوبيري", "بطيخ", "مكس بيري", "أناناس", "ستروبيري", "مانجو", "خوخ"] }] }, { id: "mojito_redbull", name: "موهيتو ريد بول", price: 2.75, stock: 20, icon: "🍹", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["كيوي", "بلوبيري", "بطيخ", "مكس بيري", "أناناس", "ستروبيري", "مانجو", "خوخ"] }] }, ]}, { catId: "hookah", catName: "الأراجيل", items: [ { id: "hookah_mazaya", name: "مزايا", price: 3.0, stock: 100, icon: "💨", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["تفاحتين", "ليمون ونعنع", "بطيخ ونعنع", "علكة ونعنع", "علكة وقرفة", "علكة", "علكة ومستكة", "عنب توت", "بلوبيري", "love", "كاندي"] }] }, { id: "hookah_nakhla", name: "نخلة", price: 4.0, stock: 100, icon: "💨", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["تفاحتين", "ليمون ونعنع", "بطيخ ونعنع", "علكة ونعنع", "علكة وقرفة", "علكة", "علكة ومستكة", "عنب توت", "بلوبيري", "love", "كاندي"] }] }, { id: "hookah_fakher", name: "فاخر", price: 4.0, stock: 100, icon: "🌬️", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["تفاحتين", "ليمون ونعنع", "بطيخ ونعنع", "علكة ونعنع", "علكة وقرفة", "علكة", "علكة ومستكة", "عنب توت", "بلوبيري", "love", "كاندي"] }] }, { id: "hookah_zaghloul", name: "زغلول", price: 3.0, stock: 100, icon: "💨", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["تفاحتين", "ليمون ونعنع", "بطيخ ونعنع", "علكة ونعنع", "علكة وقرفة", "علكة", "علكة ومستكة", "عنب توت", "بلوبيري", "love", "كاندي"] }] }, { id: "hookah_zaghloul_nakhla", name: "زغلول (نخلة)", price: 4.0, stock: 100, icon: "💨", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["تفاحتين", "ليمون ونعنع", "بطيخ ونعنع", "علكة ونعنع", "علكة وقرفة", "علكة", "علكة ومستكة", "عنب توت", "بلوبيري", "love", "كاندي"] }] }, { id: "hookah_lamma", name: "خلطة اللمة", price: 3.5, stock: 100, icon: "💨", modifierGroups: [{ id: "flavor", name: "النكهة", type: "single", options: ["تفاحتين", "ليمون ونعنع", "بطيخ ونعنع", "علكة ونعنع", "علكة وقرفة", "علكة", "علكة ومستكة", "عنب توت", "بلوبيري", "love", "كاندي"] }] }, { id: "hookah_service", name: "خدمة الأراجيل", price: 2.0, stock: 200, icon: "🧰", modifierGroups: [] }, { id: "hookah_head", name: "تجديد رأس الأرجيلة", price: 1.5, stock: 200, icon: "🔄", modifierGroups: [] }, ]}, { catId: "snacks", catName: "تسالي", items: [ { id: "popcorn", name: "بوشار", price: 1.0, stock: 20, icon: "🍿", modifierGroups: [] }, { id: "nuts", name: "مكسرات", price: 1.0, stock: 20, icon: "🥜", modifierGroups: [] }, { id: "waffle", name: "وافل", price: 1.5, stock: 15, icon: "🧇", modifierGroups: [] }, { id: "waffle_fruit", name: "وافل مع فواكه", price: 2.0, stock: 15, icon: "🧇", modifierGroups: [] }, { id: "fruit_plate", name: "صحن فواكه", price: 5.0, stock: 10, icon: "🍓", modifierGroups: [] }, ]}, { catId: "addons", catName: "إضافات", items: [ { id: "addon_flifer", name: "فليفر", price: 0.25, stock: 100, icon: "➕", modifierGroups: [] }, { id: "addon_sauce", name: "صوص", price: 0.5, stock: 100, icon: "➕", modifierGroups: [] }, { id: "addon_may", name: "مي", price: 0.35, stock: 100, icon: "➕", modifierGroups: [] }, { id: "addon_espresso_shot", name: "شوت إسبريسو", price: 0.75, stock: 100, icon: "➕", modifierGroups: [] }, ]}, ]; const CONFIG_VERSION = 6; // bump this any time DEFAULT_MENU / DEFAULT_STAFF / DEFAULT_ZONES change, so saved shared data doesn't shadow the update const money = (n) => `${n.toFixed(2)} د.أ`; const uid = () => Math.random().toString(36).slice(2, 9); function timeAgo(ts, now) { const mins = Math.max(0, Math.round((now - ts) / 60000)); if (mins < 1) return "الآن"; if (mins === 1) return "قبل دقيقة"; if (mins === 2) return "قبل دقيقتين"; if (mins < 11) return `قبل ${mins} دقائق`; return `قبل ${mins} دقيقة`; } function computeTotals(items, discountPct, vatPct) { const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0); const net = subtotal * (1 - discountPct / 100); const vatAmount = net * (vatPct / 100); const total = net + vatAmount; return { subtotal, net, vatAmount, total }; } function orderLabel(o) { return o.orderType === "dine-in" ? `${o.zoneName ? o.zoneName + " · " : ""}طاولة ${o.table}` : "Take Away"; } function getPeriodRange(period, ref = new Date(), customRange = null) { if (period === "all") return null; if (period === "custom") { if (!customRange || !customRange.start || !customRange.end) return null; const start = new Date(customRange.start); start.setHours(0, 0, 0, 0); const end = new Date(customRange.end); end.setHours(23, 59, 59, 999); return { start: start.getTime(), end: end.getTime() }; } const start = new Date(ref); start.setHours(0, 0, 0, 0); if (period === "daily") { return { start: start.getTime(), end: ref.getTime() }; } if (period === "weekly") { // custom work-week starting Saturday (JS getDay: Sun=0 ... Sat=6) const day = start.getDay(); const daysSinceSaturday = (day + 1) % 7; start.setDate(start.getDate() - daysSinceSaturday); return { start: start.getTime(), end: ref.getTime() }; } if (period === "monthly") { start.setDate(1); return { start: start.getTime(), end: ref.getTime() }; } return null; } function PeriodTabs({ period, setPeriod, customRange, setCustomRange }) { return (
{setCustomRange && ( )}
{period === "custom" && setCustomRange && (
من setCustomRange((prev) => ({ ...prev, start: e.target.value }))} /> إلى setCustomRange((prev) => ({ ...prev, end: e.target.value }))} />
)}
); } function CafePOS() { const [view, setView] = useState("owner"); // owner | waiter | cashier const [staff, setStaff] = useState(DEFAULT_STAFF); const [menu, setMenu] = useState(DEFAULT_MENU); const [vatPct, setVatPct] = useState(16); const [ownerEmail, setOwnerEmail] = useState("owner@cafe.com"); const [ownerPassword, setOwnerPassword] = useState("change-me-123"); const [ownerLoggedIn, setOwnerLoggedIn] = useState(false); const [ownerLoginEmail, setOwnerLoginEmail] = useState(""); const [ownerLoginPassword, setOwnerLoginPassword] = useState(""); const [ownerLoginError, setOwnerLoginError] = useState(""); const [zones, setZones] = useState(DEFAULT_ZONES); const [printers, setPrinters] = useState(DEFAULT_PRINTERS); const [expenses, setExpenses] = useState([]); const [cashClosings, setCashClosings] = useState([]); const [reportsPeriod, setReportsPeriod] = useState("daily"); const [reportsCustomRange, setReportsCustomRange] = useState({ start: "", end: "" }); const [expensesPeriod, setExpensesPeriod] = useState("daily"); const [expensesCustomRange, setExpensesCustomRange] = useState({ start: "", end: "" }); const [showClosingModal, setShowClosingModal] = useState(false); const [actualCashInput, setActualCashInput] = useState(""); const [actualCardInput, setActualCardInput] = useState(""); const [actualClickInput, setActualClickInput] = useState(""); const [waiterStaffId, setWaiterStaffId] = useState(null); const [cashierStaffId, setCashierStaffId] = useState(null); const [loginUsername, setLoginUsername] = useState(""); const [loginPassword, setLoginPassword] = useState(""); const [loginError, setLoginError] = useState(""); const [orderType, setOrderType] = useState("dine-in"); // dine-in | takeaway const [selectedZone, setSelectedZone] = useState(DEFAULT_ZONES[0].id); const [selectedTable, setSelectedTable] = useState(null); const [newZoneName, setNewZoneName] = useState(""); const [newTableLabel, setNewTableLabel] = useState({}); const [activeCategory, setActiveCategory] = useState(DEFAULT_MENU[0].catId); const [cart, setCart] = useState([]); const [note, setNote] = useState(""); const [discountPct, setDiscountPct] = useState(0); const [orders, setOrders] = useState([]); const [orderCounter, setOrderCounter] = useState(3025); const [selectedOrderId, setSelectedOrderId] = useState(null); const [toast, setToast] = useState(null); const [now, setNow] = useState(Date.now()); const [ownerTab, setOwnerTab] = useState("staff"); const [newStaffName, setNewStaffName] = useState(""); const [newStaffType, setNewStaffType] = useState("نادل"); const [newStaffAssignedZone, setNewStaffAssignedZone] = useState("all"); const [newStaffUsername, setNewStaffUsername] = useState(""); const [newStaffPassword, setNewStaffPassword] = useState(""); const [menuEditItem, setMenuEditItem] = useState(null); const [restockAmount, setRestockAmount] = useState({}); const [preBillData, setPreBillData] = useState(null); const [printData, setPrintData] = useState(null); const [splitOrderId, setSplitOrderId] = useState(null); const [splitCash, setSplitCash] = useState(""); const [splitCard, setSplitCard] = useState(""); const [cashierSubTab, setCashierSubTab] = useState("active"); // active | all const [cashierZoneFilter, setCashierZoneFilter] = useState("all"); // all | | takeaway const [allOrdersFilter, setAllOrdersFilter] = useState("all"); // all | dine-in | takeaway const [pendingModItem, setPendingModItem] = useState(null); // item awaiting modifier selection const [modTargetOrderId, setModTargetOrderId] = useState(null); // if set, modifier confirm adds to this existing order instead of the draft cart const [cashierAddItemOrderId, setCashierAddItemOrderId] = useState(null); const [cashierPickerCategory, setCashierPickerCategory] = useState(null); const [modSelections, setModSelections] = useState({}); const [configLoaded, setConfigLoaded] = useState(false); const [ordersLoaded, setOrdersLoaded] = useState(false); const [lastSync, setLastSync] = useState(null); const [syncError, setSyncError] = useState(false); const configVersionRef = useRef(0); const ordersVersionRef = useRef(0); const savingRef = useRef(false); const sessionIdRef = useRef(uid()); const [activeSessions, setActiveSessions] = useState({}); // sessionId -> { staffId, staffName, role, zoneId, zoneName, lastSeen } const booksVersionRef = useRef(0); const [booksLoaded, setBooksLoaded] = useState(false); // ---- Shared sync: load config (staff/menu/vat/printers) + orders from shared storage, then keep polling ---- useEffect(() => { let cancelled = false; async function loadConfig() { try { const res = await window.storage.get("pos-config", true); if (res && !cancelled) { const data = JSON.parse(res.value); const incomingTs = data.updatedAt || 0; // only accept this data if it's actually newer than what we already have locally — // otherwise a slow/stale poll from another device could silently wipe out a fresh edit (e.g. new staff login) if (data.version === CONFIG_VERSION && incomingTs >= configVersionRef.current) { configVersionRef.current = incomingTs; if (data.staff) setStaff(data.staff); if (data.menu) setMenu(data.menu); if (typeof data.vatPct === "number") setVatPct(data.vatPct); if (data.zones) setZones(data.zones); if (data.printers) setPrinters(data.printers); if (data.ownerEmail) setOwnerEmail(data.ownerEmail); if (data.ownerPassword) setOwnerPassword(data.ownerPassword); } } if (!cancelled) setSyncError(false); } catch (e) { // key not created yet on first-ever run — that's fine, defaults will be saved } finally { if (!cancelled) { setConfigLoaded(true); setLastSync(Date.now()); } } } async function loadOrders() { try { const res = await window.storage.get("pos-orders", true); if (res && !cancelled) { const data = JSON.parse(res.value); const incomingTs = data.updatedAt || 0; if (incomingTs >= ordersVersionRef.current) { ordersVersionRef.current = incomingTs; if (data.orders) setOrders(data.orders); if (typeof data.orderCounter === "number") setOrderCounter(data.orderCounter); } } if (!cancelled) setSyncError(false); } catch (e) { // no shared orders yet } finally { if (!cancelled) { setOrdersLoaded(true); setLastSync(Date.now()); } } } async function loadBooks() { try { const res = await window.storage.get("pos-books", true); if (res && !cancelled) { const data = JSON.parse(res.value); const incomingTs = data.updatedAt || 0; if (incomingTs >= booksVersionRef.current) { booksVersionRef.current = incomingTs; if (data.expenses) setExpenses(data.expenses); if (data.cashClosings) setCashClosings(data.cashClosings); } } } catch (e) { // no shared books yet } finally { if (!cancelled) setBooksLoaded(true); } } loadConfig(); loadOrders(); loadBooks(); const ordersPoll = setInterval(loadOrders, 4000); const configPoll = setInterval(loadConfig, 8000); const booksPoll = setInterval(loadBooks, 8000); return () => { cancelled = true; clearInterval(ordersPoll); clearInterval(configPoll); clearInterval(booksPoll); }; }, []); // save config (staff/menu/vat/printers) to shared storage whenever it changes locally — only after initial load completes useEffect(() => { if (!configLoaded) return; const ts = Date.now(); configVersionRef.current = ts; // this device's own edit is now the newest known version — polls older than this are ignored window.storage.set("pos-config", JSON.stringify({ version: CONFIG_VERSION, updatedAt: ts, staff, menu, vatPct, zones, printers, ownerEmail, ownerPassword }), true).catch(() => setSyncError(true)); }, [staff, menu, vatPct, zones, printers, ownerEmail, ownerPassword, configLoaded]); // save orders to shared storage whenever they change locally useEffect(() => { if (!ordersLoaded) return; const ts = Date.now(); ordersVersionRef.current = ts; window.storage.set("pos-orders", JSON.stringify({ updatedAt: ts, orders, orderCounter }), true).catch(() => setSyncError(true)); }, [orders, orderCounter, ordersLoaded]); // save expenses/cash-closings ("the books") to shared storage whenever they change locally useEffect(() => { if (!booksLoaded) return; const ts = Date.now(); booksVersionRef.current = ts; window.storage.set("pos-books", JSON.stringify({ updatedAt: ts, expenses, cashClosings }), true).catch(() => setSyncError(true)); }, [expenses, cashClosings, booksLoaded]); useEffect(() => { const t = setInterval(() => setNow(Date.now()), 15000); return () => clearInterval(t); }, []); useEffect(() => { if (!toast) return; const t = setTimeout(() => setToast(null), 3000); return () => clearTimeout(t); }, [toast]); const waiterStaff = staff.find((s) => s.id === waiterStaffId) || null; const cashierStaff = staff.find((s) => s.id === cashierStaffId) || null; function hasPermission(staffObj, permId) { return !!staffObj?.permissions?.[permId]; } useEffect(() => { if (waiterStaff && waiterStaff.assignedZone && waiterStaff.assignedZone !== "all") { setSelectedZone(waiterStaff.assignedZone); } }, [waiterStaff]); // ---- Presence heartbeat: let the owner see who is actually logged in right now, on any device ---- useEffect(() => { let cancelled = false; async function publishPresence() { try { const mySessionId = sessionIdRef.current; const res = await window.storage.get("pos-presence", true).catch(() => null); const all = res ? JSON.parse(res.value) : {}; const now = Date.now(); // drop sessions that haven't reported in over 25s (device closed / logged out) Object.keys(all).forEach((sid) => { if (now - all[sid].lastSeen > 25000) delete all[sid]; }); if (waiterStaff) { all[mySessionId + "-w"] = { staffId: waiterStaff.id, staffName: waiterStaff.name, role: "نادل", zoneId: waiterStaff.assignedZone !== "all" ? waiterStaff.assignedZone : selectedZone, zoneName: waiterStaff.assignedZone !== "all" ? zones.find((z) => z.id === waiterStaff.assignedZone)?.name : "كل المناطق", lastSeen: now, }; } else delete all[mySessionId + "-w"]; if (cashierStaff) { all[mySessionId + "-c"] = { staffId: cashierStaff.id, staffName: cashierStaff.name, role: "كاشير", zoneId: cashierStaff.assignedZone !== "all" ? cashierStaff.assignedZone : null, zoneName: cashierStaff.assignedZone !== "all" ? zones.find((z) => z.id === cashierStaff.assignedZone)?.name : "كل المناطق", lastSeen: now, }; } else delete all[mySessionId + "-c"]; if (!cancelled) { setActiveSessions(all); await window.storage.set("pos-presence", JSON.stringify(all), true); } } catch (e) { // presence is best-effort, ignore failures } } publishPresence(); const t = setInterval(publishPresence, 6000); return () => { cancelled = true; clearInterval(t); }; }, [waiterStaff, cashierStaff, selectedZone, zones]); function attemptLogin(allowedTypes, setter) { const uname = loginUsername.trim().toLowerCase(); const match = staff.find((s) => (s.username || "").toLowerCase() === uname && s.password === loginPassword); if (!uname || !loginPassword) { setLoginError("لازم تدخل اسم المستخدم وكلمة السر"); return; } if (!match) { setLoginError("اسم المستخدم أو كلمة السر غير صحيحة"); return; } if (!allowedTypes.includes(match.type)) { setLoginError(`حساب "${match.name}" نوعه "${match.type}" وما إله صلاحية دخول هون`); return; } setter(match.id); setLoginUsername(""); setLoginPassword(""); setLoginError(""); } function logoutWaiter() { setWaiterStaffId(null); setLoginError(""); } function logoutCashier() { setCashierStaffId(null); setLoginError(""); } function attemptOwnerLogin() { const email = ownerLoginEmail.trim().toLowerCase(); if (!email || !ownerLoginPassword) { setOwnerLoginError("لازم تدخل الإيميل وكلمة السر"); return; } if (email !== ownerEmail.trim().toLowerCase() || ownerLoginPassword !== ownerPassword) { setOwnerLoginError("الإيميل أو كلمة السر غير صحيحة"); return; } setOwnerLoggedIn(true); setOwnerLoginEmail(""); setOwnerLoginPassword(""); setOwnerLoginError(""); } function logoutOwner() { setOwnerLoggedIn(false); setOwnerLoginError(""); } function addZone() { if (!newZoneName.trim()) return; setZones((prev) => [...prev, { id: uid(), name: newZoneName.trim(), tables: [] }]); setNewZoneName(""); setToast("تمت إضافة المنطقة"); } function removeZone(zoneId) { setZones((prev) => prev.filter((z) => z.id !== zoneId)); setToast("تم حذف المنطقة"); } function addTableToZone(zoneId) { const label = (newTableLabel[zoneId] || "").trim(); if (!label) return; setZones((prev) => prev.map((z) => (z.id === zoneId ? { ...z, tables: [...z.tables, label] } : z))); setNewTableLabel((prev) => ({ ...prev, [zoneId]: "" })); } function removeTableFromZone(zoneId, label) { setZones((prev) => prev.map((z) => (z.id === zoneId ? { ...z, tables: z.tables.filter((t) => t !== label) } : z))); } // ---- Printers ---- function addPrinter(name, ip, role, zoneId) { if (!name.trim() || !ip.trim()) return; setPrinters((prev) => [...prev, { id: uid(), name: name.trim(), ip: ip.trim(), role, zoneId: zoneId || "all" }]); setToast("تمت إضافة الطابعة"); } function updatePrinter(id, field, value) { setPrinters((prev) => prev.map((p) => (p.id === id ? { ...p, [field]: value } : p))); } function removePrinter(id) { setPrinters((prev) => prev.filter((p) => p.id !== id)); setToast("تم حذف الطابعة"); } // ---- Expenses / withdrawals (owner only) ---- function addExpense(type, description, amount) { if (!description.trim() || !amount || Number(amount) <= 0) return; setExpenses((prev) => [...prev, { id: uid(), type, description: description.trim(), amount: Number(amount), createdAt: Date.now() }]); setToast(`تمت إضافة ${type === "purchase" ? "المشترى" : "السحب"}`); } function removeExpense(id) { setExpenses((prev) => prev.filter((e) => e.id !== id)); setToast("تم حذف السجل"); } // ---- Daily cashier closing ---- function closeCashierShift(cashierName, zoneName, expectedCash, expectedCard, actualCash, actualCard, clickCount, clickAmount) { const variance = Number(actualCash) - expectedCash; const cardVariance = Number(actualCard) - expectedCard; setCashClosings((prev) => [ ...prev, { id: uid(), cashierName, zoneName, expectedCash, expectedCard, actualCash: Number(actualCash), actualCard: Number(actualCard), variance, cardVariance, clickCount: clickCount || 0, clickAmount: Number(clickAmount) || 0, closedAt: Date.now() }, ]); const grandTotal = Number(actualCash) + Number(actualCard) + (Number(clickAmount) || 0); setToast(`تم إغلاق الشفت — الإجمالي ${money(grandTotal)} 💰`); } const totals = computeTotals(cart, discountPct, vatPct); function qtyInCartForId(itemId) { return cart.filter((c) => c.id === itemId).reduce((s, c) => s + c.qty, 0); } function addToCart(item) { const usedQty = qtyInCartForId(item.id); if (item.stock - usedQty <= 0) { setToast(`عذراً، "${item.name}" خلص من المخزون`); return; } if (item.modifierGroups && item.modifierGroups.length > 0) { const defaults = {}; item.modifierGroups.forEach((g) => { defaults[g.id] = g.type === "multi" ? [] : g.options[0]; }); setModSelections(defaults); setPendingModItem(item); return; } addLineToCart(item, {}, ""); } function addLineToCart(item, mods, modsLabel) { const cartKey = `${item.id}::${JSON.stringify(mods)}`; const station = item.station || CATEGORY_STATION[item.catId] || "bar"; setCart((prev) => { const existing = prev.find((i) => i.cartKey === cartKey); if (existing) return prev.map((i) => (i.cartKey === cartKey ? { ...i, qty: i.qty + 1 } : i)); return [...prev, { id: item.id, name: item.name, price: item.price, icon: item.icon, qty: 1, cartKey, mods, modsLabel, station }]; }); } function confirmModifierSelection() { const item = pendingModItem; if (!item) return; const labelParts = []; item.modifierGroups.forEach((g) => { const val = modSelections[g.id]; if (g.type === "multi") { if (val && val.length) labelParts.push(val.join("، ")); } else if (val) { labelParts.push(val); } }); const label = labelParts.join(" · "); if (modTargetOrderId) { const targetOrder = orders.find((o) => o.id === modTargetOrderId); if (targetOrder) addNewItemToExistingOrder(targetOrder, item, modSelections, label, cashierStaff?.name || "الكاشير"); setModTargetOrderId(null); } else { addLineToCart(item, modSelections, label); } setPendingModItem(null); setModSelections({}); } function toggleModOption(group, option) { setModSelections((prev) => { if (group.type === "single") return { ...prev, [group.id]: option }; const current = prev[group.id] || []; const next = current.includes(option) ? current.filter((o) => o !== option) : [...current, option]; return { ...prev, [group.id]: next }; }); } function changeQty(cartKey, delta) { setCart((prev) => prev.map((i) => (i.cartKey === cartKey ? { ...i, qty: i.qty + delta } : i)).filter((i) => i.qty > 0)); } function deductStockForCart(cartItems) { const qtyById = {}; cartItems.forEach((c) => { qtyById[c.id] = (qtyById[c.id] || 0) + c.qty; }); setMenu((prev) => prev.map((cat) => ({ ...cat, items: cat.items.map((it) => (qtyById[it.id] ? { ...it, stock: Math.max(0, it.stock - qtyById[it.id]) } : it)) }))); } function startOverDraft() { setCart([]); setNote(""); setDiscountPct(0); setSelectedTable(null); setOrderType("dine-in"); setToast("تم البدء من جديد"); } function voidAllDraft() { if (cart.length === 0) return; setCart([]); setToast("تم إلغاء كل الأصناف من الطلب الحالي"); } function preBillDraft() { if (cart.length === 0) { setToast("الطلب فارغ، ضيف أصناف أول"); return; } const zoneName = zones.find((z) => z.id === selectedZone)?.name || ""; setPreBillData({ label: orderType === "dine-in" ? (selectedTable ? `${zoneName} · طاولة ${selectedTable}` : "بدون طاولة") : "Take Away", items: cart, ...totals }); } function getOpenOrderForTable(zoneId, tableLabel) { return orders.find((o) => o.orderType === "dine-in" && o.zoneId === zoneId && o.table === tableLabel && o.status === "pending"); } function routingToastFor(items) { const hasBar = items.some((i) => i.station === "bar"); const hasKitchen = items.some((i) => i.station === "kitchen"); const hasHookah = items.some((i) => i.station === "hookah"); const parts = []; if (hasBar) parts.push("☕ الباريستا"); if (hasKitchen) parts.push("🍽️ المطبخ"); if (hasHookah) parts.push("💨 الأراجيل"); return parts.length ? ` — رايح لـ ${parts.join(" و ")}` : ""; } function submitOrder() { if ((orderType === "dine-in" && !selectedTable) || cart.length === 0) return; const existing = orderType === "dine-in" ? getOpenOrderForTable(selectedZone, selectedTable) : null; if (existing) { const mergedItems = [...existing.items]; cart.forEach((c) => { const found = mergedItems.find((i) => i.cartKey === c.cartKey); if (found) found.qty += c.qty; else mergedItems.push({ ...c }); }); const mergedTotals = computeTotals(mergedItems, discountPct || existing.discountPct, vatPct); const mergedWaiters = Array.from(new Set([...(existing.waiters || [existing.staffName]), waiterStaff.name])); setOrders((prev) => prev.map((o) => o.id === existing.id ? { ...o, items: mergedItems, waiters: mergedWaiters, note: [o.note, note].filter(Boolean).join(" · "), discountPct: discountPct || o.discountPct, vatPct, ...mergedTotals, printed: false, barPrinted: false, kitchenPrinted: false, hookahPrinted: false, updatedAt: Date.now() } : o ) ); deductStockForCart(cart); startOverDraft(); setToast(`تمت إضافة الأصناف لطلب طاولة ${selectedTable} (#${existing.id})${routingToastFor(cart)} ✅`); return; } const newOrder = { id: orderCounter, orderType, zoneId: orderType === "dine-in" ? selectedZone : null, zoneName: orderType === "dine-in" ? zones.find((z) => z.id === selectedZone)?.name : null, table: orderType === "dine-in" ? selectedTable : null, staffName: waiterStaff.name, waiters: [waiterStaff.name], items: cart, note, discountPct, vatPct, ...totals, status: "pending", printed: false, barPrinted: false, hookahPrinted: false, kitchenPrinted: false, createdAt: Date.now(), updatedAt: Date.now(), }; setOrders((prev) => [...prev, newOrder]); setOrderCounter((c) => c + 1); deductStockForCart(cart); startOverDraft(); setToast(`تم إرسال الطلب #${newOrder.id} إلى الكاشير${routingToastFor(cart)} ✅`); } function adjustStock(itemId, delta) { setMenu((prev) => prev.map((cat) => ({ ...cat, items: cat.items.map((it) => (it.id === itemId ? { ...it, stock: Math.max(0, it.stock + delta) } : it)) }))); } function editExistingOrderItemQty(order, cartKey, delta, actorName) { const target = order.items.find((i) => i.cartKey === cartKey); const updatedItems = order.items.map((i) => (i.cartKey === cartKey ? { ...i, qty: i.qty + delta } : i)).filter((i) => i.qty > 0); const updatedTotals = computeTotals(updatedItems, order.discountPct, order.vatPct); const waiters = Array.from(new Set([...(order.waiters || [order.staffName]), actorName])); setOrders((prev) => prev.map((o) => (o.id === order.id ? { ...o, items: updatedItems, waiters, ...updatedTotals, printed: false, updatedAt: Date.now() } : o))); if (target) adjustStock(target.id, -delta); } function removeExistingOrderItem(order, cartKey, actorName) { const target = order.items.find((i) => i.cartKey === cartKey); const updatedItems = order.items.filter((i) => i.cartKey !== cartKey); const updatedTotals = computeTotals(updatedItems, order.discountPct, order.vatPct); const waiters = Array.from(new Set([...(order.waiters || [order.staffName]), actorName])); setOrders((prev) => prev.map((o) => (o.id === order.id ? { ...o, items: updatedItems, waiters, ...updatedTotals, printed: false, updatedAt: Date.now() } : o))); if (target) adjustStock(target.id, target.qty); } function addNewItemToExistingOrder(order, item, mods, modsLabel, actorName) { const station = item.station || CATEGORY_STATION[item.catId] || "bar"; const cartKey = `${item.id}::${JSON.stringify(mods)}`; const existingLine = order.items.find((i) => i.cartKey === cartKey); const updatedItems = existingLine ? order.items.map((i) => (i.cartKey === cartKey ? { ...i, qty: i.qty + 1 } : i)) : [...order.items, { id: item.id, name: item.name, price: item.price, icon: item.icon, qty: 1, cartKey, mods, modsLabel, station }]; const updatedTotals = computeTotals(updatedItems, order.discountPct, order.vatPct); const waiters = Array.from(new Set([...(order.waiters || [order.staffName]), actorName])); setOrders((prev) => prev.map((o) => (o.id === order.id ? { ...o, items: updatedItems, waiters, ...updatedTotals, printed: false, barPrinted: false, kitchenPrinted: false, hookahPrinted: false, updatedAt: Date.now() } : o))); adjustStock(item.id, -1); setToast(`تمت إضافة ${item.name} للطلب #${order.id}`); } function markPaid(orderId, method) { setOrders((prev) => prev.map((o) => (o.id === orderId ? { ...o, status: "paid", paymentMethod: method, cashierName: cashierStaff?.name, paidAt: Date.now(), printed: true } : o))); setSelectedOrderId(null); setToast(`تم تحصيل الطلب #${orderId} (${method}) 💰`); } function markPaidSplit(orderId, cashAmount, cardAmount) { setOrders((prev) => prev.map((o) => o.id === orderId ? { ...o, status: "paid", paymentMethod: "مقسّم", cashAmount, cardAmount, cashierName: cashierStaff?.name, paidAt: Date.now(), printed: true } : o ) ); setSelectedOrderId(null); setSplitOrderId(null); setToast(`تم تحصيل الطلب #${orderId} (نقدي ${money(cashAmount)} + بطاقة ${money(cardAmount)}) 💰`); } function deleteOrder(orderId) { setOrders((prev) => prev.filter((o) => o.id !== orderId)); setSelectedOrderId(null); setToast(`تم إلغاء الطلب #${orderId}`); } function holdOrder(orderId) { setOrders((prev) => prev.map((o) => (o.id === orderId ? { ...o, status: "held", heldAt: Date.now() } : o))); setSelectedOrderId(null); setToast(`تم تعليق الفاتورة #${orderId}`); } function reopenOrder(orderId) { setOrders((prev) => prev.map((o) => (o.id === orderId ? { ...o, status: "pending" } : o))); setToast(`تم إعادة فتح الفاتورة #${orderId}`); } function preBillOrder(order) { setOrders((prev) => prev.map((o) => (o.id === order.id ? { ...o, printed: true } : o))); setPreBillData({ label: `#${order.id} · ${orderLabel(order)}`, items: order.items, subtotal: order.subtotal, net: order.net, vatAmount: order.vatAmount, total: order.total }); } function openDrawer() { setToast("🔓 تم فتح درج الكاش"); } const STATION_FLAG_FIELD = { bar: "barPrinted", kitchen: "kitchenPrinted", hookah: "hookahPrinted" }; function printStationTicket(order, station) { const items = order.items.filter((i) => i.station === station); if (items.length === 0) return; setOrders((prev) => prev.map((o) => (o.id === order.id ? { ...o, [STATION_FLAG_FIELD[station]]: true } : o))); setPrintData({ orderId: order.id, label: `${STATION_LABEL[station]} · ${orderLabel(order)}`, staffName: order.staffName, items, subtotal: items.reduce((s, i) => s + i.price * i.qty, 0), net: null, vatAmount: null, total: null, stationTicket: true, date: new Date().toLocaleString("ar"), }); setTimeout(() => { window.print(); setPrintData(null); }, 150); } function printReceipt(order) { setOrders((prev) => prev.map((o) => (o.id === order.id ? { ...o, printed: true } : o))); setPrintData({ orderId: order.id, label: orderLabel(order), staffName: order.staffName, paymentMethod: order.paymentMethod, cashAmount: order.cashAmount, cardAmount: order.cardAmount, items: order.items, subtotal: order.subtotal, net: order.net, vatAmount: order.vatAmount, vatPct: order.vatPct, total: order.total, date: new Date(order.paidAt || order.createdAt).toLocaleString("ar"), }); setTimeout(() => { window.print(); setPrintData(null); }, 150); } function resetAll() { setOrders([]); setCart([]); setSelectedTable(null); setSelectedOrderId(null); setOrderCounter(3025); setMenu(DEFAULT_MENU); setVatPct(16); setZones(DEFAULT_ZONES); setStaff(DEFAULT_STAFF); setCurrentStaffId(null); setToast("تم تصفير كل البيانات"); } // ---- Owner: staff management ---- function addStaff() { if (!newStaffName.trim() || !newStaffUsername.trim() || !newStaffPassword.trim()) { setToast("لازم تعبي الاسم واسم المستخدم وكلمة السر"); return; } if (staff.some((s) => (s.username || "").toLowerCase() === newStaffUsername.trim().toLowerCase())) { setToast("اسم المستخدم هذا مستخدم لموظف تاني، اختار اسم مختلف"); return; } const basePerms = { editMenu: false, deleteOrder: false, applyDiscount: false, viewReports: false, manageInventory: false, addToOpenOrder: false, removeFromOpenOrder: false }; setStaff((prev) => [...prev, { id: uid(), name: newStaffName.trim(), type: newStaffType, assignedZone: newStaffAssignedZone, username: newStaffUsername.trim(), password: newStaffPassword.trim(), permissions: basePerms }]); setNewStaffName(""); setNewStaffUsername(""); setNewStaffPassword(""); setToast(`تمت إضافة الموظف ${newStaffName.trim()}`); } function removeStaff(id) { setStaff((prev) => prev.filter((s) => s.id !== id)); if (waiterStaffId === id) setWaiterStaffId(null); if (cashierStaffId === id) setCashierStaffId(null); setToast("تم حذف الموظف"); } function toggleStaffPerm(id, permId) { setStaff((prev) => prev.map((s) => (s.id === id ? { ...s, permissions: { ...s.permissions, [permId]: !s.permissions[permId] } } : s))); } function setStaffType(id, type) { setStaff((prev) => prev.map((s) => (s.id === id ? { ...s, type } : s))); } function setStaffZone(id, assignedZone) { setStaff((prev) => prev.map((s) => (s.id === id ? { ...s, assignedZone } : s))); } function updateStaffCredential(id, field, value) { setStaff((prev) => prev.map((s) => (s.id === id ? { ...s, [field]: value } : s))); } // ---- Menu management ---- function saveMenuItem(catId, itemDraft) { setMenu((prev) => prev.map((cat) => { if (cat.catId !== catId) return cat; const exists = cat.items.some((i) => i.id === itemDraft.id); return { ...cat, items: exists ? cat.items.map((i) => (i.id === itemDraft.id ? itemDraft : i)) : [...cat.items, itemDraft] }; }) ); setMenuEditItem(null); setToast("تم حفظ الصنف"); } function deleteMenuItem(catId, itemId) { setMenu((prev) => prev.map((cat) => (cat.catId === catId ? { ...cat, items: cat.items.filter((i) => i.id !== itemId) } : cat))); setToast("تم حذف الصنف"); } function applyGroupsToCategory(catId, modifierGroups) { setMenu((prev) => prev.map((cat) => (cat.catId === catId ? { ...cat, items: cat.items.map((i) => ({ ...i, modifierGroups: modifierGroups.map((g) => ({ ...g, id: `${g.id}_${i.id}` })) })) } : cat))); setToast("تم تطبيق نفس الخيارات على كل أصناف هذا القسم ✅"); } function restock(catId, itemId) { const amount = Number(restockAmount[itemId] || 0); if (!amount) return; setMenu((prev) => prev.map((cat) => (cat.catId === catId ? { ...cat, items: cat.items.map((i) => (i.id === itemId ? { ...i, stock: i.stock + amount } : i)) } : cat))); setRestockAmount((prev) => ({ ...prev, [itemId]: "" })); setToast("تم تحديث المخزون"); } const allItems = menu.flatMap((c) => c.items.map((i) => ({ ...i, catId: c.catId, catName: c.catName }))); const lowStockItems = allItems.filter((i) => i.stock <= LOW_STOCK_THRESHOLD); const pendingOrders = orders.filter((o) => o.status === "pending").sort((a, b) => a.createdAt - b.createdAt); const heldOrders = orders.filter((o) => o.status === "held").sort((a, b) => b.heldAt - a.heldAt); const paidOrders = orders.filter((o) => o.status === "paid").sort((a, b) => b.paidAt - a.paidAt); const selectedOrder = orders.find((o) => o.id === selectedOrderId); const filteredAllOrders = orders .filter((o) => allOrdersFilter === "all" || o.orderType === allOrdersFilter) .sort((a, b) => b.createdAt - a.createdAt); const reports = useMemo(() => { const range = getPeriodRange(reportsPeriod, new Date(), reportsCustomRange); const scopedPaidOrders = range ? paidOrders.filter((o) => (o.paidAt || o.createdAt) >= range.start && (o.paidAt || o.createdAt) <= range.end) : paidOrders; const scopedOrders = range ? orders.filter((o) => o.createdAt >= range.start && o.createdAt <= range.end) : orders; const totalRevenue = scopedPaidOrders.reduce((s, o) => s + o.total, 0); const ordersCount = scopedPaidOrders.length; const avgTicket = ordersCount ? totalRevenue / ordersCount : 0; const itemTally = {}; scopedPaidOrders.forEach((o) => o.items.forEach((i) => { if (!itemTally[i.name]) itemTally[i.name] = { qty: 0, revenue: 0 }; itemTally[i.name].qty += i.qty; itemTally[i.name].revenue += i.qty * i.price; })); const topItems = Object.entries(itemTally).map(([name, v]) => ({ name, ...v })).sort((a, b) => b.revenue - a.revenue).slice(0, 5); const maxItemRevenue = topItems.length ? topItems[0].revenue : 1; const byMethod = { نقدي: 0, بطاقة: 0, كليك: 0 }; let clickCount = 0; scopedPaidOrders.forEach((o) => { if (o.paymentMethod === "مقسّم") { byMethod["نقدي"] += o.cashAmount || 0; byMethod["بطاقة"] += o.cardAmount || 0; } else if (o.paymentMethod) { byMethod[o.paymentMethod] += o.total; if (o.paymentMethod === "كليك") clickCount++; } }); const byType = { "dine-in": 0, takeaway: 0 }; scopedPaidOrders.forEach((o) => { byType[o.orderType] += o.total; }); const byStaff = staff.map((s) => ({ name: s.name, revenue: scopedPaidOrders.filter((o) => o.staffName === s.name).reduce((sum, o) => sum + o.total, 0) })); const waiterActivity = staff .filter((s) => s.type === "نادل") .map((s) => ({ name: s.name, count: scopedOrders.filter((o) => o.orderType === "dine-in" || o.orderType === "takeaway" ? (o.waiters || [o.staffName]).includes(s.name) : false).length, })) .sort((a, b) => b.count - a.count); const maxActivity = waiterActivity.length ? waiterActivity[0].count : 1; return { totalRevenue, ordersCount, avgTicket, topItems, maxItemRevenue, byMethod, clickCount, byType, byStaff, waiterActivity, maxActivity }; }, [paidOrders, orders, staff, reportsPeriod, reportsCustomRange]); return (
دفتر القهوجي CAFE POS · DEMO {syncError ? "⚠️ انقطع الاتصال" : configLoaded && ordersLoaded ? "🟢 متزامن" : "⏳ يتزامن..."}
{/* ---------------- OWNER ---------------- */} {view === "owner" && (
{!ownerLoggedIn ? ( ) : ( <>
مسجل دخول: {ownerEmail} 👑 صاحب الكافيه
{ownerTab === "staff" && (

الموظفين — نوع الموظف، منطقته، وصلاحياته

حدد لكل نادل/كاشير المنطقة المسؤول عنها بس (الأرضي أو الروف) — أو "كل المناطق" لو بدك يشوف الكل. الموظف ما رح يقدر يغيّرها من شاشته.
{staff.map((s) => (
{s.name}
updateStaffCredential(s.id, "username", e.target.value)} />
updateStaffCredential(s.id, "password", e.target.value)} />
{PERM_LIST.map((p) => ( ))}
))}

إضافة موظف جديد

أعطي الموظف اسم المستخدم وكلمة السر هاي عشان يسجل دخول من جهازه
setNewStaffName(e.target.value)} /> setNewStaffUsername(e.target.value)} /> setNewStaffPassword(e.target.value)} />
)} {ownerTab === "menu" && ( )} {ownerTab === "inventory" && ( )} {ownerTab === "zones" && (

المناطق والطاولات

مثال: "الطابق الأرضي" و"الروف" منطقتين منفصلتين، كل وحدة إلها طاولاتها الخاصة وطلباتها المفتوحة منفصلة عن التانية.
{zones.map((z) => (
🪑 {z.name}
{z.tables.map((t) => ( طاولة {t} ))} {z.tables.length === 0 && ما في طاولات بهاي المنطقة بعد}
setNewTableLabel((prev) => ({ ...prev, [z.id]: e.target.value }))} style={{ width: 160 }} />
))}

إضافة منطقة جديدة

setNewZoneName(e.target.value)} />
)} {ownerTab === "live" && (() => { const now2 = Date.now(); const liveSessions = Object.values(activeSessions).filter((s) => now2 - s.lastSeen < 25000); const liveWaiters = liveSessions.filter((s) => s.role === "نادل"); const liveCashiers = liveSessions.filter((s) => s.role === "كاشير"); const openOrders = orders.filter((o) => o.status === "pending" || o.status === "held"); const openValue = openOrders.reduce((s, o) => s + o.total, 0); return (
نادلين شغالين هلق
{liveWaiters.length}
كاشيرية شغالين هلق
{liveCashiers.length}
طاولات مفتوحة الآن
{openOrders.filter((o) => o.orderType === "dine-in").length}
قيمة الطلبات المفتوحة
{money(openValue)}

👥 مين شغال هلق (كل الأجهزة)

{liveSessions.length === 0 &&
ما في حدا مسجل دخول هلق
} {liveWaiters.map((s, i) => (
🧾 {s.staffName} نادل
منطقته: {s.zoneName || "—"}
🟢 متصل
))} {liveCashiers.map((s, i) => (
💰 {s.staffName} كاشير
منطقته: {s.zoneName || "—"}
🟢 متصل
))}

🪑 الطاولات المفتوحة حسب المنطقة

{zones.map((z) => { const zoneOpen = openOrders.filter((o) => o.zoneId === z.id); const zoneValue = zoneOpen.reduce((s, o) => s + o.total, 0); return (
🪑 {z.name} — {zoneOpen.length} طاولة مفتوحة {money(zoneValue)}
{zoneOpen.length === 0 &&
ما في طاولات مفتوحة هون
} {zoneOpen.map((o) => (
طاولة {o.table} · {(o.waiters || [o.staffName]).join("، ")} {money(o.total)}
))}
); })} {(() => { const takeawayOpen = openOrders.filter((o) => o.orderType === "takeaway"); if (takeawayOpen.length === 0) return null; return (
🥡 Take Away — {takeawayOpen.length} طلب {money(takeawayOpen.reduce((s, o) => s + o.total, 0))}
{takeawayOpen.map((o) => (
#{o.id} · {(o.waiters || [o.staffName]).join("، ")} {money(o.total)}
))}
); })()}
); })()} {ownerTab === "printers" && ( )} {ownerTab === "expenses" && ( )} {ownerTab === "closings" && } {ownerTab === "reports" && ( )} {ownerTab === "settings" && (

إعدادات عامة

نسبة الضريبة VAT % setVatPct(Number(e.target.value))} />
بتنطبق هاي النسبة تلقائياً على كل الفواتير الجديدة (Net + Vat = Total).

🔐 بيانات دخول صاحب الكافيه

هاي بيانات الدخول الخاصة فيك لهاي الشاشة — لا تشاركها مع الموظفين.
setOwnerEmail(e.target.value)} />
setOwnerPassword(e.target.value)} />
)} )}
)} {/* ---------------- WAITER ---------------- */} {view === "waiter" && (
{!waiterStaff || !["نادل"].includes(waiterStaff.type) ? ( attemptLogin(["نادل"], setWaiterStaffId)} /> ) : ( <>
مسجل دخول: {waiterStaff.name} {waiterStaff.type}
{orderType === "dine-in" && ( <> {waiterStaff && waiterStaff.assignedZone && waiterStaff.assignedZone !== "all" ? (
🪑 منطقتك: {zones.find((z) => z.id === waiterStaff.assignedZone)?.name || "—"}
) : (
{zones.map((z) => ( ))}
)}
{(zones.find((z) => z.id === selectedZone)?.tables || []).map((t) => { const occupied = getOpenOrderForTable(selectedZone, t); return ( ); })} {(zones.find((z) => z.id === selectedZone)?.tables || []).length === 0 && (
ما في طاولات بهاي المنطقة، ضيفها من شاشة صاحب الكافيه
)}
فاضية فيها طلب مفتوح
)}
{menu.map((c) => { const style = CATEGORY_STYLE[c.catId] || { icon: "🍽", grad: "linear-gradient(135deg,#555,#222)" }; return ( ); })}
{(menu.find((c) => c.catId === activeCategory)?.items || []).map((item) => { const style = CATEGORY_STYLE[activeCategory] || { grad: "linear-gradient(135deg,#555,#222)" }; const inCartQty = qtyInCartForId(item.id); const isOut = item.stock - inCartQty <= 0; return (
!isOut && addToCart({ ...item, catId: activeCategory })}> {item.icon}
{item.name}
{money(item.price)}
{item.stock <= LOW_STOCK_THRESHOLD && ( {isOut ? "خلص" : `متبقي ${item.stock - inCartQty}`} )}
); })}
{orderType === "dine-in" && selectedTable && getOpenOrderForTable(selectedZone, selectedTable) && (
🔔 طاولة {selectedTable} عندها طلب مفتوح #{getOpenOrderForTable(selectedZone, selectedTable).id} بقيمة {money(getOpenOrderForTable(selectedZone, selectedTable).total)} — أي صنف تضيفه هلق رح ينضاف عليه.
)} {orderType === "dine-in" && selectedTable && getOpenOrderForTable(selectedZone, selectedTable) && (hasPermission(waiterStaff, "addToOpenOrder") || hasPermission(waiterStaff, "removeFromOpenOrder")) && (
تعديل الأصناف المرسلة سابقاً
{getOpenOrderForTable(selectedZone, selectedTable).items.map((i) => (
{i.name} {i.modsLabel && ({i.modsLabel})}
{hasPermission(waiterStaff, "removeFromOpenOrder") && ( )} {i.qty} {hasPermission(waiterStaff, "addToOpenOrder") && ( )} {hasPermission(waiterStaff, "removeFromOpenOrder") && ( )}
))} {getOpenOrderForTable(selectedZone, selectedTable).items.length === 0 && (
ما بقي أصناف بهاي الفاتورة
)}
)}
{orderType === "dine-in" && selectedTable && getOpenOrderForTable(selectedZone, selectedTable) ? `إضافة لطلب #${getOpenOrderForTable(selectedZone, selectedTable).id} · طاولة ${selectedTable}` : `طلب #${orderCounter} · ${orderType === "dine-in" ? (selectedTable ? `طاولة ${selectedTable}` : "— اختر طاولة —") : "Take Away"}`}
{cart.length === 0 &&
لسا ما ضفت أصناف للطلب
} {cart.map((i) => (
{i.name} {i.modsLabel && ({i.modsLabel})}
{i.qty} {money(i.price * i.qty)}
))} {cart.length > 0 && ( <>
المجموع الفرعي{money(totals.subtotal)}
{hasPermission(waiterStaff, "applyDiscount") && (
نسبة الخصم % setDiscountPct(Math.min(100, Math.max(0, Number(e.target.value))))} />
)}
Net{money(totals.net)}
Vat ({vatPct}%){money(totals.vatAmount)}
Total{money(totals.total)}