// AdminPanel.jsx — Yönetim paneli (Genel, Analitik, Masalar, Menü, Kullanıcılar, Sistem)
const { useState, useEffect, useMemo } = React;
// ============================================================================
// Helpers
// ============================================================================
const PALETTE = ['#E8590C', '#16A34A', '#7C3AED', '#0EA5E9', '#EC4899', '#14B8A6', '#EAB308', '#F97316', '#84CC16'];
function pctChange(curr, prev) {
if (!prev || prev === 0) return curr ? 100 : null;
return ((curr - prev) / prev) * 100;
}
function ymd(d) {
const x = new Date(d);
const y = x.getFullYear();
const m = String(x.getMonth() + 1).padStart(2, '0');
const day = String(x.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
function startOfDay(d) {
const x = new Date(d);
x.setHours(0, 0, 0, 0);
return x;
}
function addDays(d, n) {
const x = new Date(d);
x.setDate(x.getDate() + n);
return x;
}
function startOfMonth(d) {
const x = new Date(d);
x.setDate(1); x.setHours(0, 0, 0, 0);
return x;
}
function dateRangeFromPreset(preset, customFrom, customTo) {
const now = new Date();
const today0 = startOfDay(now);
switch (preset) {
case 'today':
return { from: today0, to: addDays(today0, 1), label: 'Bugün', days: 1 };
case 'yesterday':
return { from: addDays(today0, -1), to: today0, label: 'Dün', days: 1 };
case '7d':
return { from: addDays(today0, -6), to: addDays(today0, 1), label: 'Son 7 Gün', days: 7 };
case '30d':
return { from: addDays(today0, -29), to: addDays(today0, 1), label: 'Son 30 Gün', days: 30 };
case 'month': {
const f = startOfMonth(now);
return { from: f, to: addDays(today0, 1), label: 'Bu Ay', days: Math.max(1, Math.round((addDays(today0, 1) - f) / 86400000)) };
}
case 'custom': {
const f = customFrom ? startOfDay(new Date(customFrom)) : today0;
const t = customTo ? addDays(startOfDay(new Date(customTo)), 1) : addDays(today0, 1);
const days = Math.max(1, Math.round((t - f) / 86400000));
const fmtD = (d) => d.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
return { from: f, to: t, label: `${fmtD(f)} → ${fmtD(addDays(t, -1))}`, days };
}
default:
return dateRangeFromPreset('today');
}
}
function previousRange(range) {
const span = range.to - range.from;
return { from: new Date(range.from.getTime() - span), to: new Date(range.to.getTime() - span) };
}
function filterOrders(orders, from, to) {
return orders.filter(o => {
const d = new Date(o.closedAt);
return d >= from && d < to;
});
}
function aggregate(orders) {
let revenue = 0, count = orders.length, items = 0, cash = 0, card = 0;
for (const o of orders) {
revenue += o.total;
if (o.paymentMethod === 'cash') cash += o.total; else card += o.total;
for (const it of o.items) items += it.qty;
}
return { revenue, count, items, cash, card, avg: count ? revenue / count : 0 };
}
function groupByDay(orders, from, to) {
const days = [];
for (let d = new Date(from); d < to; d = addDays(d, 1)) {
days.push({ date: new Date(d), key: ymd(d), orders: [] });
}
for (const o of orders) {
const k = ymd(new Date(o.closedAt));
const day = days.find(d => d.key === k);
if (day) day.orders.push(o);
}
return days.map(d => ({ ...d, ...aggregate(d.orders) }));
}
function groupByHour(orders) {
const hours = Array.from({ length: 24 }, (_, h) => ({ h, orders: [] }));
for (const o of orders) {
const h = new Date(o.closedAt).getHours();
hours[h].orders.push(o);
}
return hours.map(({ h, orders }) => ({ h, ...aggregate(orders) }));
}
function groupByWaiter(orders) {
const m = {};
for (const o of orders) {
const w = o.waiter || '—';
if (!m[w]) m[w] = { name: w, orders: [], itemCounts: {} };
m[w].orders.push(o);
for (const it of o.items) m[w].itemCounts[it.name] = (m[w].itemCounts[it.name] || 0) + it.qty;
}
return Object.values(m).map(w => {
const agg = aggregate(w.orders);
const top = Object.entries(w.itemCounts).sort((a, b) => b[1] - a[1])[0];
return { ...w, ...agg, topItem: top ? { name: top[0], qty: top[1] } : null };
}).sort((a, b) => b.revenue - a.revenue);
}
function groupByItem(orders, top = 8) {
const m = {};
for (const o of orders) for (const it of o.items) {
if (!m[it.name]) m[it.name] = { name: it.name, qty: 0, revenue: 0 };
m[it.name].qty += it.qty;
m[it.name].revenue += it.qty * it.price;
}
return Object.values(m).sort((a, b) => b.qty - a.qty).slice(0, top);
}
function groupByCategory(orders) {
const menu = Store.getMenu();
const itemCat = {};
menu.forEach(m => { itemCat[m.name] = m.cat; });
const m = {};
for (const o of orders) for (const it of o.items) {
const cat = itemCat[it.name] || 'Diğer';
if (!m[cat]) m[cat] = { name: cat, qty: 0, revenue: 0 };
m[cat].qty += it.qty;
m[cat].revenue += it.qty * it.price;
}
return Object.values(m).sort((a, b) => b.revenue - a.revenue);
}
// ============================================================================
// Generic UI components
// ============================================================================
function Section({ title, subtitle, action, children, padded = true }) {
return (
{data.map((d, i) => {
const pct = (d.value / max) * 100;
const isHover = hover === i;
return (
setHover(i)} onMouseLeave={() => setHover(null)}
onTouchStart={() => setHover(i)}
>
{isHover && (
{d.tooltip || d.label}
{format(d.value)}
)}
0 ? (isHover ? color : color + 'cc') : 'var(--border)',
borderRadius: '4px 4px 0 0',
transition: 'background 0.15s',
}} />
);
})}
{data.map((d, i) => (
{d.label}
))}
);
}
function LineChart({ data, height = 200, format = (v) => v, color = 'var(--accent)' }) {
const max = Math.max(...data.map(d => d.value), 1);
const [hover, setHover] = useState(null);
const W = 100, H = 100;
const pts = data.map((d, i) => {
const xpct = data.length > 1 ? (i / (data.length - 1)) * W : W / 2;
const ypct = (d.value / max) * H;
return { xpct, ypct, ...d };
});
const svgPts = pts.map(p => `${p.xpct},${H - p.ypct}`).join(' ');
const areaPts = `0,${H} ${svgPts} ${W},${H}`;
return (
{pts.map((p, i) => (
setHover(i)} onMouseLeave={() => setHover(null)}
onTouchStart={() => setHover(i)}
style={{
position: 'absolute', top: `${100 - p.ypct}%`, left: `${p.xpct}%`,
width: 16, height: 16, marginLeft: -8, marginTop: -8,
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
))}
{hover !== null && (() => {
const p = pts[hover];
const isRight = p.xpct > 70;
return (
{p.tooltip || p.label}
{format(p.value)}
);
})()}
{data.map((d, i) => (
{d.label}
))}
);
}
function DonutChart({ data, size = 140, centerLabel, centerValue }) {
const sum = data.reduce((s, d) => s + d.value, 0) || 1;
const r = 40, cx = 50, cy = 50;
const C = 2 * Math.PI * r;
let offset = 0;
return (
{centerLabel &&
{centerLabel}
}
{centerValue &&
{centerValue}
}
);
}
function HBarList({ items, format = (v) => v, color = 'var(--accent)', emptyMessage = 'Veri yok' }) {
if (!items.length) return
;
const max = Math.max(...items.map(i => i.value), 1);
return (
{items.map((it, i) => (
{it.label}
{format(it.value)}
))}
);
}
// ============================================================================
// Tabs
// ============================================================================
// ----- Dashboard -----
function DashboardTab({ onJumpToAnalytics }) {
const orders = Store.getOrders();
const tables = Store.getTables();
const today = dateRangeFromPreset('today');
const yesterday = dateRangeFromPreset('yesterday');
const todayOrders = filterOrders(orders, today.from, today.to);
const yOrders = filterOrders(orders, yesterday.from, yesterday.to);
const a = aggregate(todayOrders);
const yA = aggregate(yOrders);
const occupied = tables.filter(t => t.occupied).length;
const occupiedRevenue = tables.reduce((s, t) => s + (t.orders || []).reduce((ss, o) => ss + o.price * o.qty, 0), 0);
const hourly = groupByHour(todayOrders);
const hourlyData = hourly.filter(h => h.h >= 7 && h.h <= 23).map(h => ({
label: String(h.h).padStart(2, '0'),
value: h.revenue,
tooltip: `${String(h.h).padStart(2, '0')}:00 – ${String((h.h + 1) % 24).padStart(2, '0')}:00`,
}));
const topItems = groupByItem(todayOrders, 5);
const waiterPerf = groupByWaiter(todayOrders).slice(0, 5);
const dateStr = new Date().toLocaleDateString('tr-TR', { weekday: 'long', day: 'numeric', month: 'long' });
return (
0 ? `${a.count} sipariş, ${fmt(a.revenue)} toplam` : null}
action={}>
{a.count > 0
?
: }
({ label: it.name, value: it.qty, color: PALETTE[i % PALETTE.length] }))}
format={(v) => `${v} adet`}
emptyMessage="Henüz satış yok"
/>
({ label: `${w.name} · ${w.count} sip.`, value: w.revenue, color: PALETTE[i % PALETTE.length] }))}
format={fmt}
emptyMessage="Henüz garson satışı yok"
/>
);
}
// ----- Analytics -----
function AnalyticsTab() {
const orders = Store.getOrders();
const [preset, setPreset] = useState('7d');
const [customFrom, setCustomFrom] = useState(ymd(addDays(new Date(), -7)));
const [customTo, setCustomTo] = useState(ymd(new Date()));
const [waiterFilter, setWaiterFilter] = useState('all');
const [exporting, setExporting] = useState(false);
const [exportError, setExportError] = useState(null);
const range = useMemo(
() => dateRangeFromPreset(preset, customFrom, customTo),
[preset, customFrom, customTo]
);
const prev = useMemo(() => previousRange(range), [range]);
const periodOrders = filterOrders(orders, range.from, range.to);
const filteredOrders = waiterFilter === 'all' ? periodOrders : periodOrders.filter(o => o.waiter === waiterFilter);
const prevOrders = filterOrders(orders, prev.from, prev.to);
const prevFiltered = waiterFilter === 'all' ? prevOrders : prevOrders.filter(o => o.waiter === waiterFilter);
const a = aggregate(filteredOrders);
const pA = aggregate(prevFiltered);
const allWaiters = useMemo(
() => [...new Set(orders.map(o => o.waiter).filter(Boolean))].sort(),
[orders]
);
const byWaiter = groupByWaiter(periodOrders);
const byDay = groupByDay(filteredOrders, range.from, range.to);
const hourly = groupByHour(filteredOrders);
const items = groupByItem(filteredOrders, 10);
const cats = groupByCategory(filteredOrders);
const dayLabel = (d) => d.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
const dayTooltip = (d) => d.toLocaleDateString('tr-TR', { weekday: 'long', day: 'numeric', month: 'long' });
const handleExport = async () => {
setExporting(true);
setExportError(null);
try {
const fromStr = ymd(range.from);
const toStr = ymd(addDays(range.to, -1)); // server treats `to` as inclusive day
await Store.downloadXlsx({
from: fromStr, to: toStr,
waiter: waiterFilter !== 'all' ? waiterFilter : null,
});
} catch (e) {
setExportError(e.message || 'İndirme başarısız');
setTimeout(() => setExportError(null), 5000);
} finally {
setExporting(false);
}
};
return (
Analitik
{range.label} · {filteredOrders.length} sipariş
{waiterFilter !== 'all' && <> · {waiterFilter}>}
{exportError && (
{exportError}
)}
{/* Filters */}
{[
{ id: 'today', label: 'Bugün' },
{ id: 'yesterday', label: 'Dün' },
{ id: '7d', label: '7 Gün' },
{ id: '30d', label: '30 Gün' },
{ id: 'month', label: 'Bu Ay' },
{ id: 'custom', label: 'Özel' },
].map(p => (
setPreset(p.id)}>{p.label}
))}
{preset === 'custom' && (
setCustomFrom(e.target.value)}
style={{ border: 'none', background: 'transparent', fontFamily: 'inherit', fontSize: 13, fontWeight: 600, color: 'var(--text)', padding: '2px 0' }} />
→
setCustomTo(e.target.value)}
style={{ border: 'none', background: 'transparent', fontFamily: 'inherit', fontSize: 13, fontWeight: 600, color: 'var(--text)', padding: '2px 0' }} />
)}
Garson:
{/* KPIs */}
{/* Daily trend (only if range > 1 day) */}
{range.days > 1 && (
{a.count > 0
? ({
label: dayLabel(d.date),
value: d.revenue,
tooltip: `${dayTooltip(d.date)} · ${d.count} sipariş`,
}))}
format={fmt} height={220}
/>
: }
)}
{/* Hourly */}
1 ? `${range.days} günün ortalaması` : null}
>
{a.count > 0
? h.h >= 7 && h.h <= 23).map(h => ({
label: String(h.h).padStart(2, '0'),
value: range.days > 1 ? h.revenue / range.days : h.revenue,
tooltip: `${String(h.h).padStart(2, '0')}:00 – ${String((h.h + 1) % 24).padStart(2, '0')}:00`,
}))}
format={fmt} height={180}
/>
: }
{/* Per-waiter table */}
setWaiterFilter('all')}>
← Tüm garsonlar
)}
padded={false}
>
{byWaiter.length === 0 ? (
) : (
Garson
Ciro
Sipariş
Ort.
En Çok Sattığı
{byWaiter.map((w, i) => (
))}
)}
{/* Two-col bottom */}
{a.revenue > 0 ? (
{[
{ label: 'Nakit', value: a.cash, color: 'var(--green)' },
{ label: 'Kart', value: a.card, color: 'var(--accent)' },
].map(row => (
{row.label}
{fmt(row.value)}
%{Math.round((row.value / a.revenue) * 100)}
))}
) : }
({ label: it.name, value: it.qty, color: PALETTE[i % PALETTE.length] }))}
format={(v) => `${v} adet`}
emptyMessage="Veri yok"
/>
({ label: `${c.name} · ${c.qty} adet`, value: c.revenue, color: PALETTE[i % PALETTE.length] }))}
format={fmt}
emptyMessage="Veri yok"
/>
);
}
// ----- Tables -----
function TablesTab() {
const [tables, setTables] = useState(Store.getTables());
const [confirmRemove, setConfirmRemove] = useState(null);
const [_, setTick] = useState(0);
useEffect(() => {
const onUpdate = () => setTables(Store.getTables());
window.addEventListener('store-updated', onUpdate);
// tick once a minute so elapsed times refresh
const id = setInterval(() => setTick(t => t + 1), 30000);
return () => { window.removeEventListener('store-updated', onUpdate); clearInterval(id); };
}, []);
const handleAdd = async () => setTables(await Store.addTable());
const handleRemove = async (id) => {
setTables(await Store.removeTable(id));
setConfirmRemove(null);
};
const occupied = tables.filter(t => t.occupied).length;
const empty = tables.length - occupied;
const totalOpen = tables.reduce(
(s, t) => s + (t.orders || []).reduce((ss, o) => ss + o.price * o.qty, 0),
0
);
return (
Masalar
{tables.length} masa · {occupied} dolu · {empty} boş
{tables.map(t => {
const total = (t.orders || []).reduce((s, o) => s + o.price * o.qty, 0);
const itemCount = (t.orders || []).reduce((s, o) => s + o.qty, 0);
return (
{t.name}
{t.occupied ? 'DOLU' : 'BOŞ'}
{!t.occupied && (
)}
{t.occupied && (
👤 {t.waiter}
⏱ {t.openedAt ? elapsed(t.openedAt) : '—'} · {itemCount} ürün
{total > 0 && (
{fmt(total)}
)}
)}
);
})}
{confirmRemove && (
handleRemove(confirmRemove.id)}
onCancel={() => setConfirmRemove(null)}
/>
)}
);
}
// ----- Menu -----
function MenuTab() {
const [menu, setMenu] = useState(Store.getMenu());
const [cats, setCats] = useState(Store.getCategories());
const [activeCat, setActiveCat] = useState(null);
const [editId, setEditId] = useState(null);
const [editName, setEditName] = useState('');
const [editPrice, setEditPrice] = useState('');
const [newName, setNewName] = useState('');
const [newPrice, setNewPrice] = useState('');
const [addingCat, setAddingCat] = useState(false);
const [catInput, setCatInput] = useState('');
useEffect(() => {
const onUpdate = () => { setMenu(Store.getMenu()); setCats(Store.getCategories()); };
window.addEventListener('store-updated', onUpdate);
return () => window.removeEventListener('store-updated', onUpdate);
}, []);
useEffect(() => { if (!activeCat && cats.length) setActiveCat(cats[0]); }, [cats]);
const catItems = menu.filter(m => m.cat === activeCat);
const startEdit = (item) => { setEditId(item.id); setEditName(item.name); setEditPrice(String(item.price)); };
const saveEdit = async () => {
setMenu(await Store.updateMenuItem(editId, { name: editName, price: parseFloat(editPrice) || 0 }));
setEditId(null);
};
const handleDelete = async (id) => { setMenu(await Store.deleteMenuItem(id)); };
const handleAdd = async () => {
if (!newName.trim() || !newPrice || !activeCat) return;
setMenu(await Store.addMenuItem(activeCat, newName.trim(), newPrice));
setNewName(''); setNewPrice('');
};
const handleAddCat = async () => {
if (!catInput.trim()) return;
await Store.addCategory(catInput.trim());
setCats(Store.getCategories());
setActiveCat(catInput.trim());
setCatInput(''); setAddingCat(false);
};
return (
Menü
{menu.length} ürün · {cats.length} kategori
Kategoriler
{cats.map(c => {
const count = menu.filter(m => m.cat === c).length;
return (
);
})}
{addingCat ? (
) : (
)}
{activeCat || 'Kategori seçin'}
{catItems.length} ürün
{catItems.length === 0 ? (
) : catItems.map(item => (
{editId === item.id ? (
<>
setEditName(e.target.value)} style={{ flex: 2 }} />
setEditPrice(e.target.value)} type="number" style={{ width: 90 }} />
>
) : (
<>
{item.name}
{fmt(item.price)}
>
)}
))}
{activeCat && (
setNewName(e.target.value)} placeholder="Yeni ürün adı" style={{ flex: 2 }} onKeyDown={e => e.key === 'Enter' && handleAdd()} />
setNewPrice(e.target.value)} placeholder="₺" type="number" style={{ width: 100 }} onKeyDown={e => e.key === 'Enter' && handleAdd()} />
)}
);
}
// ----- Users -----
function maskPhone(phone) {
if (!phone) return '';
const m = phone.match(/^\+90(\d{3})(\d{3})(\d{2})(\d{2})$/);
if (!m) return phone;
return `+90 ${m[1][0]}•• ••• ${m[3]} ${m[4]}`;
}
function UsersTab({ currentUser }) {
const [users, setUsers] = useState(Store.getUsers());
const [showAdd, setShowAdd] = useState(false);
const [editingPinId, setEditingPinId] = useState(null);
const [editPin, setEditPin] = useState('');
const [confirmDelete, setConfirmDelete] = useState(null);
const [filter, setFilter] = useState('all');
useEffect(() => {
const onUpdate = () => setUsers(Store.getUsers());
window.addEventListener('store-updated', onUpdate);
return () => window.removeEventListener('store-updated', onUpdate);
}, []);
const counts = {
all: users.length,
admin: users.filter(u => u.role === 'admin').length,
waiter: users.filter(u => u.role === 'waiter').length,
inactive: users.filter(u => !u.active).length,
};
const filtered = users.filter(u => {
if (filter === 'admin') return u.role === 'admin';
if (filter === 'waiter') return u.role === 'waiter';
if (filter === 'inactive') return !u.active;
return true;
});
const handleToggle = async (id) => { await Store.toggleUser(id); setUsers(Store.getUsers()); };
const handleDelete = async (id) => {
try {
await Store.deleteUser(id);
setUsers(Store.getUsers());
setConfirmDelete(null);
} catch (e) { alert(e.message || 'Silinemedi'); }
};
const handleSavePin = async (id) => {
if (!/^\d{6}$/.test(editPin)) return;
await Store.setUserPin(id, editPin);
setEditingPinId(null); setEditPin('');
};
return (
Kullanıcılar
{counts.all} toplam · {counts.admin} yönetici · {counts.waiter} garson
setFilter('all')}>Tümü ({counts.all})
setFilter('admin')}>Yönetici ({counts.admin})
setFilter('waiter')}>Garson ({counts.waiter})
{counts.inactive > 0 && setFilter('inactive')}>Pasif ({counts.inactive})}
{filtered.length === 0 ? (
) : (
{filtered.map(u => {
const isSelf = currentUser && currentUser.id === u.id;
const isAdminUser = u.role === 'admin';
return (
{u.name[0]}
{u.name}
{isSelf && SEN}
{maskPhone(u.phone)}
{isAdminUser ? 'YÖNETİCİ' : 'GARSON'}
{u.active ? 'AKTİF' : 'PASİF'}
{editingPinId === u.id ? (
setEditPin(e.target.value.replace(/\D/g, '').slice(0, 6))}
style={{ letterSpacing: '0.2em', textAlign: 'center', fontSize: 14 }} autoFocus
onKeyDown={e => e.key === 'Enter' && handleSavePin(u.id)} />
) : (
)}
);
})}
)}
{showAdd &&
setShowAdd(false)} onAdded={() => { setUsers(Store.getUsers()); setShowAdd(false); }} />}
{confirmDelete && (
{confirmDelete.name} kalıcı olarak silinecek. Geçmiş satışlar etkilenmez.>}
confirmLabel="Sil" danger
onConfirm={() => handleDelete(confirmDelete.id)}
onCancel={() => setConfirmDelete(null)}
/>
)}
);
}
function AddUserModal({ onClose, onAdded }) {
const [name, setName] = useState('');
const [phoneDigits, setPhoneDigits] = useState('');
const [pin, setPin] = useState('');
const [role, setRole] = useState('waiter');
const [error, setError] = useState(null);
const [submitting, setSubmitting] = useState(false);
const phoneOk = phoneDigits.length === 10;
const pinOk = /^\d{6}$/.test(pin);
const canSubmit = name.trim() && phoneOk && pinOk && !submitting;
const submit = async () => {
if (!canSubmit) return;
setSubmitting(true);
try {
await Store.addUser(name.trim(), '+90' + phoneDigits, pin, role);
onAdded();
} catch (e) {
setError(e.message || 'Eklenemedi');
setSubmitting(false);
}
};
return (
e.stopPropagation()}>
Yeni Kullanıcı
{ setName(e.target.value); setError(null); }} autoFocus style={{ marginBottom: 12 }} />
{ setPin(e.target.value.replace(/\D/g, '').slice(0, 6)); setError(null); }}
style={{ letterSpacing: '0.2em', textAlign: 'center', marginBottom: 12 }} />
{[
{ id: 'waiter', label: '👤 Garson' },
{ id: 'admin', label: '🔒 Yönetici' },
].map(r => (
))}
{error &&
{error}
}
);
}
// ----- System -----
function SystemTab({ lang, onLangChange }) {
return (
Sistem
Cihaz oturumları ve uygulama ayarları
{[{ id: 'tr', label: '🇹🇷 Türkçe' }, { id: 'en', label: '🇬🇧 English' }].map(l => (
))}
);
}
function SessionsList() {
const [sessions, setSessions] = useState([]);
const [loading, setLoading] = useState(true);
const refresh = async () => {
setLoading(true);
try { setSessions(await Store.listSessions()); }
catch { setSessions([]); }
finally { setLoading(false); }
};
useEffect(() => { refresh(); }, []);
const handleRevoke = async (token, isCurrent) => {
if (isCurrent && !confirm('Bu mevcut oturumu kapatırsan çıkış yapacaksın. Devam?')) return;
try { await Store.revokeSession(token); await refresh(); }
catch (e) { alert(e.message || 'Hata'); }
};
const fmtAgo = (iso) => {
const m = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
if (m < 1) return 'şimdi';
if (m < 60) return `${m} dk önce`;
const h = Math.floor(m / 60);
if (h < 24) return `${h} saat önce`;
return `${Math.floor(h / 24)} gün önce`;
};
if (loading) return
Yükleniyor…
;
if (sessions.length === 0) return
;
return (
{sessions.map(s => (
{s.userName}
{s.role === 'admin' ? 'ADMİN' : 'GARSON'}
{s.isCurrent && BU CİHAZ}
{fmtAgo(s.lastSeen)} · {(s.userAgent || 'bilinmiyor').slice(0, 60)}
))}
);
}
// ============================================================================
// AdminPanel shell
// ============================================================================
function AdminPanel({ lang, onClose, onLangChange, currentUser }) {
const [tab, setTab] = useState('dashboard');
const tabs = [
{ id: 'dashboard', label: 'Genel', icon: '📊' },
{ id: 'analytics', label: 'Analitik', icon: '📈' },
{ id: 'tables', label: 'Masalar', icon: '🪑' },
{ id: 'menu', label: 'Menü', icon: '🍽' },
{ id: 'users', label: 'Kullanıcılar', icon: '👥' },
{ id: 'system', label: 'Sistem', icon: '⚙️' },
];
return (
e.stopPropagation()}>
{/* Top bar */}
{/* Mobile-only horizontal tab bar */}
{tabs.map(t => (
))}
{/* Body */}
{tabs.map(t => (
))}
{currentUser ? `👤 ${currentUser.name}` : ''}
{tab === 'dashboard' &&
setTab('analytics')} />}
{tab === 'analytics' && }
{tab === 'tables' && }
{tab === 'menu' && }
{tab === 'users' && }
{tab === 'system' && }
);
}
Object.assign(window, { AdminPanel });