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:
217
CLAUDE_DESIGN/ops-layouts.jsx
Normal file
217
CLAUDE_DESIGN/ops-layouts.jsx
Normal 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 };
|
||||
Reference in New Issue
Block a user