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

View File

@@ -0,0 +1,217 @@
// Main ops dashboard layout
const { Avatar, Card, Btn, Icon } = window.OpsUI;
const { KpiCard, ProgressBar, TablesOverview, HourlyRevenueCard, ReservationsCard } = window.OpsCards;
const { ShiftsCard, MessagesCard, ComposeModal, EndDayModal } = window.OpsCards2;
const { OPS_DATA } = window;
function TopBar({ onEndDay }) {
const b = OPS_DATA.business;
return (
<div style={{
padding: '20px 28px',
background: 'white',
borderBottom: '1px solid var(--ink-100)',
display: 'flex', alignItems: 'center', gap: 20,
}}>
<div style={{
width: 44, height: 44, borderRadius: 12,
background: 'var(--ink-900)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'white', fontWeight: 700, fontSize: 18,
fontFamily: "'Geist Mono', monospace", flexShrink: 0,
}}>TS</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--ink-900)' }}>{b.name}</div>
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '4px 10px', borderRadius: 999,
background: 'var(--open-50)', color: 'var(--open-700)',
fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.6,
}}>
<span style={{ width: 6, height: 6, borderRadius: 3, background: 'var(--open-500)' }} />
Day open
</span>
</div>
<div style={{ fontSize: 13, color: 'var(--ink-500)', marginTop: 2 }}>
{b.date} · started at {b.dayStartedAt} · {Math.floor(b.dayDurationMins/60)}h {b.dayDurationMins%60}m running
</div>
</div>
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
<Btn variant="secondary" size="md"><Icon name="bell" size={16} /></Btn>
<Btn variant="secondary" size="md">Reports</Btn>
<Btn variant="danger" size="md" onClick={onEndDay}><Icon name="stop" size={14} color="white" /> End day</Btn>
</div>
</div>
);
}
// Compact tablet top bar
function TabletTopBar({ onEndDay }) {
const b = OPS_DATA.business;
return (
<div style={{
padding: '16px 20px',
background: 'white',
borderBottom: '1px solid var(--ink-100)',
display: 'flex', alignItems: 'center', gap: 14,
}}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ fontSize: 17, fontWeight: 700, color: 'var(--ink-900)' }}>{b.name}</div>
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
padding: '3px 8px', borderRadius: 999,
background: 'var(--open-50)', color: 'var(--open-700)',
fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5,
}}>
<span style={{ width: 5, height: 5, borderRadius: 3, background: 'var(--open-500)' }} />
Open
</span>
</div>
<div style={{ fontSize: 12, color: 'var(--ink-500)', marginTop: 2 }}>
{b.date} · {b.dayStartedAt} · {Math.floor(b.dayDurationMins/60)}h {b.dayDurationMins%60}m
</div>
</div>
<Btn variant="secondary" size="sm"><Icon name="bell" size={14} /></Btn>
<Btn variant="danger" size="sm" onClick={onEndDay}><Icon name="stop" size={12} color="white" /> End day</Btn>
</div>
);
}
// KPI strip — 3 cards
function KpiStrip({ compact }) {
const k = OPS_DATA.kpis;
const revPct = (k.revenue / k.revenueGoal) * 100;
const covPct = (k.covers / k.coversGoal) * 100;
const tablesOpenPct = (k.tablesOpen / k.tablesTotal) * 100;
const revDelta = ((k.revenue - k.revenueLastWeek) / k.revenueLastWeek) * 100;
const covDelta = ((k.covers - k.coversLastWeek) / k.coversLastWeek) * 100;
return (
<div style={{
display: 'grid',
gridTemplateColumns: compact ? '1fr 1fr 1fr' : '1fr 1fr 1fr',
gap: compact ? 12 : 20,
}}>
<KpiCard
label="Revenue today"
value={'€' + OPS_DATA.kpis.revenue.toLocaleString('en', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
sub={`Goal €${k.revenueGoal.toLocaleString()} · ${revPct.toFixed(0)}%`}
delta={revDelta}
>
<ProgressBar pct={revPct} color="var(--brand-500)" />
</KpiCard>
<KpiCard
label="Covers"
value={k.covers.toString()}
sub={`Avg €${k.avgTicket.toFixed(2)} per cover · ${covPct.toFixed(0)}%`}
delta={covDelta}
>
<ProgressBar pct={covPct} color="var(--occ-500)" />
</KpiCard>
<KpiCard
label="Tables"
value={`${k.tablesOpen} / ${k.tablesTotal}`}
sub={`${k.tablesTotal - k.tablesOpen} closed · ${tablesOpenPct.toFixed(0)}% in use`}
>
<ProgressBar pct={tablesOpenPct} color="var(--res-500)" />
</KpiCard>
</div>
);
}
// ---------------------------------------------------------------- DESKTOP LAYOUT
function DesktopDashboard() {
const [composeOpen, setComposeOpen] = React.useState(false);
const [composeText, setComposeText] = React.useState('');
const [composeTo, setComposeTo] = React.useState('Everyone');
const [endDayOpen, setEndDayOpen] = React.useState(false);
const openCompose = (preset, to) => {
setComposeText(preset || '');
setComposeTo(to || 'Everyone');
setComposeOpen(true);
};
return (
<div style={{
width: '100%', height: '100%',
background: 'var(--bg)',
display: 'flex', flexDirection: 'column',
position: 'relative',
overflow: 'hidden',
}}>
<TopBar onEndDay={() => setEndDayOpen(true)} />
<div style={{ flex: 1, overflowY: 'auto', padding: 28 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 18 }}>
<div style={{ fontSize: 26, fontWeight: 700, color: 'var(--ink-900)' }}>Today at a glance</div>
<div style={{ fontSize: 13, color: 'var(--ink-500)' }}>Updated 19:08 · auto-refresh</div>
</div>
<KpiStrip />
<div style={{
marginTop: 20,
display: 'grid',
gridTemplateColumns: '1.2fr 1fr',
gap: 20,
}}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<TablesOverview />
<ShiftsCard onMessage={(s) => openCompose('', s.name)} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<HourlyRevenueCard />
<MessagesCard openCompose={openCompose} />
<ReservationsCard />
</div>
</div>
</div>
<ComposeModal open={composeOpen} prefilled={composeText} prefilledTo={composeTo} onClose={() => setComposeOpen(false)} />
<EndDayModal open={endDayOpen} onClose={() => setEndDayOpen(false)} onConfirm={() => setEndDayOpen(false)} />
</div>
);
}
// ---------------------------------------------------------------- TABLET LAYOUT
function TabletDashboard() {
const [composeOpen, setComposeOpen] = React.useState(false);
const [composeText, setComposeText] = React.useState('');
const [composeTo, setComposeTo] = React.useState('Everyone');
const [endDayOpen, setEndDayOpen] = React.useState(false);
const openCompose = (preset, to) => {
setComposeText(preset || '');
setComposeTo(to || 'Everyone');
setComposeOpen(true);
};
return (
<div style={{
width: '100%', height: '100%',
background: 'var(--bg)',
display: 'flex', flexDirection: 'column',
position: 'relative',
overflow: 'hidden',
}}>
<TabletTopBar onEndDay={() => setEndDayOpen(true)} />
<div style={{ flex: 1, overflowY: 'auto', padding: 18 }}>
<KpiStrip compact />
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, marginTop: 14 }}>
<TablesOverview />
<HourlyRevenueCard />
<ShiftsCard onMessage={(s) => openCompose('', s.name)} />
<MessagesCard openCompose={openCompose} />
<ReservationsCard />
</div>
</div>
<ComposeModal open={composeOpen} prefilled={composeText} prefilledTo={composeTo} onClose={() => setComposeOpen(false)} />
<EndDayModal open={endDayOpen} onClose={() => setEndDayOpen(false)} onConfirm={() => setEndDayOpen(false)} />
</div>
);
}
window.OpsLayouts = { DesktopDashboard, TabletDashboard };