Frontend overhaul: manager dashboard restructure, waiter PWA rework, new order drawer and components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 12:12:23 +03:00
parent defc49f84f
commit bb39088464
78 changed files with 24370 additions and 1358 deletions

199
CLAUDE_DESIGN/ops-cards.jsx Normal file
View File

@@ -0,0 +1,199 @@
// Dashboard cards — KPIs, tables overview, hourly chart, reservations
const { Avatar, Card, StatPill, Btn, Icon } = window.OpsUI;
const { OPS_DATA } = window;
// ---------------------------------------------------------------- KPI big card
function KpiCard({ label, value, sub, delta, accent = 'var(--brand-500)', tone, children }) {
return (
<div style={{
background: tone || 'white',
border: '1px solid var(--ink-100)',
borderRadius: 16,
padding: 22,
boxShadow: '0 1px 2px rgba(16,20,24,0.04)',
display: 'flex', flexDirection: 'column', gap: 8,
minHeight: 148,
position: 'relative',
overflow: 'hidden',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{
fontSize: 13, fontWeight: 700, color: 'var(--ink-500)',
textTransform: 'uppercase', letterSpacing: 0.6,
}}>{label}</div>
<StatPill delta={delta} />
</div>
<div style={{
fontSize: 38, fontWeight: 700,
fontFamily: "'Geist Mono', monospace",
color: 'var(--ink-900)',
letterSpacing: -1,
lineHeight: 1.1,
}}>{value}</div>
{sub && <div style={{ fontSize: 13, color: 'var(--ink-500)' }}>{sub}</div>}
{children}
</div>
);
}
// Progress bar used inside KPI cards
function ProgressBar({ pct, color = 'var(--brand-500)' }) {
return (
<div style={{
marginTop: 'auto', height: 8, borderRadius: 4,
background: 'var(--ink-100)', overflow: 'hidden',
}}>
<div style={{
width: `${Math.min(100, pct)}%`, height: '100%',
background: color, borderRadius: 4,
transition: 'width 240ms ease',
}} />
</div>
);
}
// ---------------------------------------------------------------- Tables
function TablesOverview() {
// Build a synthetic 18-table layout
const states = {
'A1': 'occupied', 'A2': 'occupied', 'A3': 'open', 'A4': 'alert',
'B1': 'reserved','B2': 'occupied', 'B3': 'open', 'B4': 'occupied',
'C1': 'open', 'C2': 'reserved','C3': 'open', 'C4': 'open',
'D1': 'occupied','D2': 'dirty', 'D3': 'open', 'D4': 'open',
'T1': 'occupied','T2': 'open',
};
const colors = {
occupied: { bg: 'var(--occ-100)', fg: 'var(--occ-700)', accent: 'var(--occ-500)' },
open: { bg: 'var(--open-50)', fg: 'var(--open-700)', accent: 'var(--open-500)' },
reserved: { bg: 'var(--res-100)', fg: 'var(--res-700)', accent: 'var(--res-500)' },
alert: { bg: 'var(--alert-100)', fg: 'var(--alert-700)', accent: 'var(--alert-500)' },
dirty: { bg: 'var(--dirty-100)', fg: 'var(--dirty-700)', accent: 'var(--dirty-500)' },
};
const counts = Object.values(states).reduce((acc, s) => (acc[s] = (acc[s] || 0) + 1, acc), {});
return (
<Card title="Tables right now" action={<Btn size="sm" variant="ghost">View floor</Btn>}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(6, 1fr)',
gap: 8,
marginBottom: 16,
}}>
{Object.entries(states).map(([name, status]) => {
const c = colors[status];
return (
<div key={name} style={{
aspectRatio: '1',
minHeight: 44,
borderRadius: 10,
background: c.bg,
color: c.fg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 14, fontWeight: 700,
fontFamily: "'Geist Mono', monospace",
border: '1px solid ' + c.accent + '33',
}}>{name}</div>
);
})}
</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{[
['occupied', 'Occupied'],
['open', 'Open'],
['reserved', 'Reserved'],
['alert', 'Alert'],
['dirty', 'Cleaning'],
].map(([k, label]) => (
<div key={k} style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '5px 10px',
borderRadius: 999,
background: 'var(--ink-100)',
fontSize: 12, fontWeight: 600,
color: 'var(--ink-700)',
}}>
<span style={{ width: 8, height: 8, borderRadius: 4, background: colors[k].accent }} />
{label}
<span style={{ fontFamily: "'Geist Mono', monospace", color: 'var(--ink-900)' }}>{counts[k] || 0}</span>
</div>
))}
</div>
</Card>
);
}
// ---------------------------------------------------------------- Hourly chart
function HourlyRevenueCard() {
const data = OPS_DATA.hourly;
const max = Math.max(...data.map(d => d.revenue), 800);
const currentHour = '19';
return (
<Card title="Revenue by hour" action={<div style={{ fontSize: 13, color: 'var(--ink-500)', fontWeight: 500 }}>Today</div>}>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height: 140, padding: '8px 0 0' }}>
{data.map(d => {
const h = max > 0 ? (d.revenue / max) * 100 : 0;
const isCurrent = d.hour === currentHour;
const isFuture = parseInt(d.hour) > parseInt(currentHour);
return (
<div key={d.hour} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, height: '100%' }}>
<div style={{ flex: 1, width: '100%', display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
<div style={{
width: '100%', height: `${h}%`,
borderRadius: 6,
minHeight: d.revenue > 0 ? 4 : 0,
background: isCurrent
? 'var(--brand-500)'
: isFuture
? 'var(--ink-100)'
: 'var(--brand-200)',
}} />
</div>
<div style={{
fontSize: 11,
fontFamily: "'Geist Mono', monospace",
color: isCurrent ? 'var(--brand-700)' : 'var(--ink-500)',
fontWeight: isCurrent ? 700 : 500,
}}>{d.hour}</div>
</div>
);
})}
</div>
</Card>
);
}
// ---------------------------------------------------------------- Reservations
function ReservationsCard() {
return (
<Card title="Reservations today" action={<Btn size="sm" variant="ghost">+ Add</Btn>}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 280, overflowY: 'auto' }}>
{OPS_DATA.reservations.map(r => (
<div key={r.id} style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '10px 12px',
borderRadius: 10,
border: '1px solid var(--ink-100)',
}}>
<div style={{
width: 56, textAlign: 'center',
fontSize: 16, fontWeight: 700,
fontFamily: "'Geist Mono', monospace",
color: 'var(--ink-900)',
flexShrink: 0,
}}>{r.time}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink-900)' }}>{r.name}</div>
<div style={{ fontSize: 12, color: 'var(--ink-500)', marginTop: 1 }}>
{r.guests} guests · Table {r.table}
{r.notes && <> · <span style={{ color: 'var(--brand-700)', fontWeight: 600 }}>{r.notes}</span></>}
</div>
</div>
</div>
))}
</div>
</Card>
);
}
window.OpsCards = { KpiCard, ProgressBar, TablesOverview, HourlyRevenueCard, ReservationsCard };