218 lines
8.4 KiB
JavaScript
218 lines
8.4 KiB
JavaScript
// 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 };
|