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