// 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 (
{title}
{subtitle &&
{subtitle}
}
{action}
{children}
); } function StatCard({ label, value, sub, accent = 'var(--accent)', icon, trend }) { return (
{icon && {icon}} {label}
{value}
{(sub || trend !== undefined) && (
{trend !== undefined && trend !== null && ( = 0 ? 'var(--green-bg)' : '#FEF2F2', color: trend >= 0 ? 'var(--green)' : 'var(--red)', }}> {trend >= 0 ? '▲' : '▼'} {Math.abs(Math.round(trend))}% )} {sub && {sub}}
)}
); } function FilterPill({ active, onClick, children }) { return ( ); } function EmptyState({ icon = '📊', message, sub }) { return (
{icon}
{message}
{sub &&
{sub}
}
); } function ConfirmDialog({ title, message, confirmLabel = 'Onayla', danger, onConfirm, onCancel }) { return (
e.stopPropagation()}>
⚠️
{title}
{message}
); } // ============================================================================ // Charts // ============================================================================ function BarChart({ data, height = 200, format = (v) => v, color = 'var(--accent)' }) { const max = Math.max(...data.map(d => d.value), 1); const [hover, setHover] = useState(null); 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 (
{data.map((d, i) => { const len = (d.value / sum) * C; const dash = `${len} ${C - len}`; const elem = ( ); offset += len; return elem; })}
{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 (
Genel Bakış
{dateStr}
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 ? (
setCatInput(e.target.value)} placeholder="Kategori adı" style={{ marginBottom: 6, fontSize: 13 }} autoFocus onKeyDown={e => e.key === 'Enter' && handleAddCat()} />
) : ( )}
{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 }} />
+90
{ const d = e.target.value.replace(/\D/g, '').slice(0, 10); setPhoneDigits(d); setError(null); }} style={{ flex: 1, padding: '12px 14px', border: 'none', outline: 'none', fontFamily: 'inherit', fontSize: 16, letterSpacing: '0.04em' }} />
{ 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 */}
Yönetim
{/* 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 });