/* 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" ? (
) : (
{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();