/* Relay — main app */ const { useState, useEffect, useRef, useMemo } = React; const RD = window.RelayData; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "layout": "columns", "density": "regular", "accent": "#2563EB", "showKpis": true, "feedSpeed": "normal" }/*EDITMODE-END*/; // short two-tone chime via WebAudio function makeChime() { let ctx = null; return function play() { try { ctx = ctx || new (window.AudioContext || window.webkitAudioContext)(); if (ctx.state === "suspended") ctx.resume(); const now = ctx.currentTime; [ [880, 0], [1320, 0.12] ].forEach(([f, t]) => { const o = ctx.createOscillator(), g = ctx.createGain(); o.type = "sine"; o.frequency.value = f; g.gain.setValueAtTime(0.0001, now + t); g.gain.exponentialRampToValueAtTime(0.22, now + t + 0.02); g.gain.exponentialRampToValueAtTime(0.0001, now + t + 0.32); o.connect(g).connect(ctx.destination); o.start(now + t); o.stop(now + t + 0.34); }); } catch (e) {} }; } function Notices({ notices }) { return (
{notices.map((n) => (
{n.text}
))}
); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [orders, setOrders] = useState(() => RD.seedOrders()); const [branch, setBranch] = useState(RD.BRANCHES[0]); const [soundOn, setSoundOn] = useState(true); const [typeFilter, setTypeFilter] = useState("all"); const [platFilter, setPlatFilter] = useState([]); const [statusFilter, setStatusFilter] = useState("active"); const [search, setSearch] = useState(""); const [simOn, setSimOn] = useState(true); const [selectedId, setSelectedId] = useState(null); const [toasts, setToasts] = useState([]); const [now, setNow] = useState(Date.now()); const [view, setView] = useState("dashboard"); const [history] = useState(() => RD.seedHistory(7)); const [notices, setNotices] = useState([]); const chime = useRef(makeChime()); const allOrders = useMemo(() => [...orders, ...history], [orders, history]); // global text-toast helper (used by Reports export, Integrations connect, POS) useEffect(() => { window.__relayToast = (text) => { const id = "n" + Date.now() + Math.random(); setNotices((p) => [{ id, text }, ...p].slice(0, 3)); setTimeout(() => setNotices((p) => p.filter((x) => x.id !== id)), 3200); }; return () => { delete window.__relayToast; }; }, []); const layout = t.layout; const dense = t.density === "compact"; // clock tick useEffect(() => { const iv = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(iv); }, []); // accent → CSS var useEffect(() => { document.documentElement.style.setProperty("--accent", t.accent); document.documentElement.style.setProperty("--new", t.accent); }, [t.accent]); // live simulation useEffect(() => { if (!simOn) return; const base = t.feedSpeed === "fast" ? 4500 : t.feedSpeed === "slow" ? 14000 : 8000; let timer; const schedule = () => { const jitter = base * (0.6 + Math.random() * 0.8); timer = setTimeout(() => { addIncoming(); schedule(); }, jitter); }; schedule(); return () => clearTimeout(timer); }, [simOn, t.feedSpeed]); function addIncoming() { const o = RD.buildOrder({ status: "new", minutesAgo: 0 }); o._flash = true; setOrders((prev) => [o, ...prev].slice(0, 80)); if (soundOn) chime.current(); setToasts((prev) => [{ id: o.id, order: o }, ...prev].slice(0, 4)); setTimeout(() => setToasts((prev) => prev.filter((x) => x.id !== o.id)), 5200); setTimeout(() => setOrders((prev) => prev.map((x) => x.id === o.id ? { ...x, _flash: false } : x)), 600); } // mutations const patch = (id, fields) => setOrders((prev) => prev.map((o) => o.id === id ? { ...o, ...fields } : o)); const onAccept = (o) => patch(o.id, { status: "preparing", acceptedAt: Date.now(), isNew: false }); const onReject = (o) => patch(o.id, { status: "cancelled", isNew: false }); const onAdvance = (o) => { const next = o.status === "preparing" ? "ready" : o.status === "ready" ? "completed" : o.status; patch(o.id, { status: next }); }; const onOpen = (o) => setSelectedId(o.id); // filtering const filtered = useMemo(() => { const q = search.trim().toLowerCase(); return orders.filter((o) => { if (typeFilter !== "all" && o.type !== typeFilter) return false; if (platFilter.length && !platFilter.includes(o.platformId)) return false; if (q) { const hay = (o.customer + " " + o.id + " " + o.channelRef + " " + PLATFORMS[o.platformId].name + " " + o.items.map((i) => i.name).join(" ")).toLowerCase(); if (!hay.includes(q)) return false; } return true; }); }, [orders, typeFilter, platFilter, search]); // status filter only for list/grid const listFiltered = useMemo(() => { return filtered.filter((o) => { if (statusFilter === "all") return true; if (statusFilter === "active") return ["new", "preparing", "ready"].includes(o.status); if (statusFilter === "preparing") return ["preparing", "ready"].includes(o.status); return o.status === statusFilter; }).sort((a, b) => b.placedAt - a.placedAt); }, [filtered, statusFilter]); // counts / KPIs const counts = useMemo(() => { const c = { new: 0, preparing: 0, ready: 0, completed: 0, late: 0, revenue: 0, total: 0 }; orders.forEach((o) => { if (c[o.status] !== undefined) c[o.status]++; if (o.status !== "cancelled") { c.total++; c.revenue += o.total; } const base = o.acceptedAt || o.placedAt; if (["new", "preparing", "ready"].includes(o.status) && (now - base) / 60000 >= o.prepMins) c.late++; }); return c; }, [orders, now]); const selected = allOrders.find((o) => o.id === selectedId) || null; const cardProps = { now, onOpen, onAccept, onReject, onAdvance, dense }; // place a manual (phone/POS) order into the live pipeline function placeManual(order) { order._flash = true; setOrders((prev) => [order, ...prev].slice(0, 80)); if (soundOn) chime.current(); window.__relayToast && window.__relayToast(`${order.id} nolu sipariş alındı — ${order.type === "food" ? "mutfağa" : "hazırlama birimine"} gönderildi.`); setTimeout(() => setOrders((prev) => prev.map((x) => x.id === order.id ? { ...x, _flash: false } : x)), 600); setView("dashboard"); } // ---------- layouts ---------- function renderList() { return (
PlatformRefMüşteriÜrünToplamDurumİşlem
{listFiltered.length === 0 ? emptyState() : listFiltered.map((o) => )}
); } function renderGrid() { if (listFiltered.length === 0) return
{emptyState()}
; return (
{listFiltered.map((o) => )}
); } function renderColumns() { const cols = RD.statusOrder.map((k) => ({ key: k, label: FLOW_LABELS.food[k] === FLOW_LABELS.retail[k] ? STATUS[k].label : STATUS[k].label, items: filtered.filter((o) => o.status === k).sort((a, b) => b.placedAt - a.placedAt), })); return (
{cols.map((col) => (
{STATUS[col.key].label} {col.items.length}
{col.items.length === 0 ?
Sipariş yok
: col.items.map((o) => )}
))}
); } function emptyState() { return (
Filtrelere uygun sipariş bulunamadı
Platform veya durum filtrelerini temizlemeyi deneyin.
); } return (
{view === "pos" ? (
setView("dashboard")} />
) : (
{view === "dashboard" && <> setPlatFilter((p) => p.includes(id) ? p.filter((x) => x !== id) : [...p, id]), clearPlat: () => setPlatFilter([]), statusFilter, setStatusFilter, layout, setLayout: (l) => setTweak("layout", l), simOn, setSimOn, counts, }} /> {t.showKpis && (
} sub="bekliyor" /> } sub={`${counts.ready} hazır`} /> } /> } /> } />
)}
{layout === "list" && renderList()} {layout === "grid" && renderGrid()} {layout === "columns" && renderColumns()}
} {view === "orders" && setSelectedId(o.id)} />} {view === "reports" && } {view === "catalog" && } {view === "integrations" && }
)} setSelectedId(null)} onAccept={onAccept} onReject={onReject} onAdvance={onAdvance} /> { setView("dashboard"); setSelectedId(o.id); setToasts([]); }} /> setTweak("layout", v)} /> setTweak("density", v)} /> setTweak("showKpis", v)} /> setTweak("feedSpeed", v)} /> setTweak("accent", v)} />
); } ReactDOM.createRoot(document.getElementById("root")).render();