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:
283
CLAUDE_DESIGN/ops-shifts.jsx
Normal file
283
CLAUDE_DESIGN/ops-shifts.jsx
Normal file
@@ -0,0 +1,283 @@
|
||||
// Shifts card + Messages card
|
||||
|
||||
const { Avatar, Card, Btn, Icon } = window.OpsUI;
|
||||
const { OPS_DATA } = window;
|
||||
|
||||
// ---------------------------------------------------------------- Shifts
|
||||
function ShiftsCard({ onMessage }) {
|
||||
return (
|
||||
<Card
|
||||
title="Shifts on now"
|
||||
action={<Btn size="sm" variant="secondary">+ Start shift</Btn>}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{OPS_DATA.shifts.map(s => (
|
||||
<div key={s.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 12,
|
||||
border: '1px solid var(--ink-100)',
|
||||
background: s.status === 'break' ? '#fbf6ec' : 'white',
|
||||
}}>
|
||||
<Avatar name={s.name} size={42} status={s.status} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--ink-900)' }}>{s.name}</div>
|
||||
{s.status === 'break' && (
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 999,
|
||||
background: 'var(--dirty-100)', color: 'var(--dirty-700)',
|
||||
fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.4,
|
||||
}}>On break</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-500)', marginTop: 2 }}>
|
||||
{s.section} · in at {s.clockIn} · {s.hoursWorked.toFixed(1)}h worked
|
||||
{s.tables.length > 0 && <> · tables {s.tables.join(', ')}</>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
|
||||
<button onClick={() => onMessage(s)} title="Message" style={{
|
||||
width: 36, height: 36, borderRadius: 18,
|
||||
background: 'white', border: '1px solid var(--ink-200)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'pointer', color: 'var(--ink-700)',
|
||||
}}><Icon name="chat" size={16} /></button>
|
||||
<button title="More" style={{
|
||||
width: 36, height: 36, borderRadius: 18,
|
||||
background: 'white', border: '1px solid var(--ink-200)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'pointer', color: 'var(--ink-700)',
|
||||
fontSize: 18, lineHeight: 1, paddingBottom: 6,
|
||||
}}>⋯</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* scheduled / not yet started */}
|
||||
<div style={{
|
||||
marginTop: 6,
|
||||
fontSize: 11, fontWeight: 700, color: 'var(--ink-400)',
|
||||
textTransform: 'uppercase', letterSpacing: 0.6,
|
||||
padding: '4px 4px 0',
|
||||
}}>Scheduled later</div>
|
||||
{OPS_DATA.scheduledShifts.map(s => (
|
||||
<div key={s.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 12,
|
||||
border: '1px dashed var(--ink-200)',
|
||||
background: 'var(--bg)',
|
||||
}}>
|
||||
<Avatar name={s.name} size={42} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--ink-700)' }}>{s.name}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-500)', marginTop: 2 }}>
|
||||
{s.section} · starts at {s.scheduledAt}
|
||||
</div>
|
||||
</div>
|
||||
<Btn size="sm" variant="primary">Start now</Btn>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Messages
|
||||
function MessagesCard({ openCompose }) {
|
||||
return (
|
||||
<Card
|
||||
title="Messages"
|
||||
action={<Btn size="sm" variant="primary" onClick={() => openCompose()}><Icon name="send" size={14} /> New</Btn>}
|
||||
>
|
||||
{/* Quick presets */}
|
||||
<div style={{
|
||||
fontSize: 11, fontWeight: 700, color: 'var(--ink-400)',
|
||||
textTransform: 'uppercase', letterSpacing: 0.6, marginBottom: 8,
|
||||
}}>Quick send</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 18 }}>
|
||||
{OPS_DATA.presets.map(p => (
|
||||
<button key={p} onClick={() => openCompose(p)} style={{
|
||||
height: 34, padding: '0 12px',
|
||||
borderRadius: 17,
|
||||
background: 'white',
|
||||
border: '1px solid var(--ink-200)',
|
||||
color: 'var(--ink-700)',
|
||||
fontSize: 13, fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
}}>+ {p}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
fontSize: 11, fontWeight: 700, color: 'var(--ink-400)',
|
||||
textTransform: 'uppercase', letterSpacing: 0.6, marginBottom: 8,
|
||||
}}>Recent</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{OPS_DATA.recentMessages.map(m => (
|
||||
<div key={m.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '8px 4px',
|
||||
}}>
|
||||
<Avatar name={m.to === 'Everyone' ? 'All' : m.to} size={28} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, color: 'var(--ink-900)' }}>
|
||||
<span style={{ fontWeight: 600 }}>{m.to}</span>
|
||||
<span style={{ color: 'var(--ink-500)' }}> · {m.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-400)', fontFamily: "'Geist Mono', monospace" }}>{m.sentAt}</div>
|
||||
{!m.read && <div style={{ width: 8, height: 8, borderRadius: 4, background: 'var(--brand-500)' }} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Compose modal
|
||||
function ComposeModal({ open, prefilled, prefilledTo, onClose }) {
|
||||
const [text, setText] = React.useState('');
|
||||
const [recipient, setRecipient] = React.useState('Everyone');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
setText(prefilled || '');
|
||||
setRecipient(prefilledTo || 'Everyone');
|
||||
}
|
||||
}, [open, prefilled, prefilledTo]);
|
||||
|
||||
if (!open) return null;
|
||||
const recipients = ['Everyone', ...OPS_DATA.shifts.map(s => s.name)];
|
||||
|
||||
return (
|
||||
<div onClick={onClose} style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'rgba(16,20,24,0.45)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
zIndex: 50,
|
||||
padding: 20,
|
||||
}}>
|
||||
<div onClick={(e) => e.stopPropagation()} style={{
|
||||
width: 'min(520px, 100%)',
|
||||
background: 'white',
|
||||
borderRadius: 18,
|
||||
padding: 24,
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.25)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 18 }}>
|
||||
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--ink-900)' }}>New message</div>
|
||||
<button onClick={onClose} style={{
|
||||
width: 36, height: 36, borderRadius: 18,
|
||||
border: 'none', background: 'var(--ink-100)', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}><Icon name="x" size={16} /></button>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-500)', textTransform: 'uppercase', letterSpacing: 0.6, marginBottom: 8 }}>To</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 18 }}>
|
||||
{recipients.map(r => (
|
||||
<button key={r} onClick={() => setRecipient(r)} style={{
|
||||
height: 36, padding: '0 14px',
|
||||
borderRadius: 18,
|
||||
background: recipient === r ? 'var(--brand-500)' : 'white',
|
||||
border: '1px solid ' + (recipient === r ? 'var(--brand-500)' : 'var(--ink-200)'),
|
||||
color: recipient === r ? 'white' : 'var(--ink-700)',
|
||||
fontSize: 14, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}>{r}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-500)', textTransform: 'uppercase', letterSpacing: 0.6, marginBottom: 8 }}>Message</div>
|
||||
<textarea value={text} onChange={(e) => setText(e.target.value)} rows={3}
|
||||
placeholder="Type a message..."
|
||||
style={{
|
||||
width: '100%', padding: 14, fontSize: 16, fontFamily: 'inherit',
|
||||
border: '1px solid var(--ink-200)', borderRadius: 12, resize: 'none',
|
||||
outline: 'none', boxSizing: 'border-box',
|
||||
}}/>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 12 }}>
|
||||
{OPS_DATA.presets.map(p => (
|
||||
<button key={p} onClick={() => setText(p)} style={{
|
||||
height: 32, padding: '0 12px',
|
||||
borderRadius: 16,
|
||||
background: 'var(--ink-100)',
|
||||
border: 'none',
|
||||
color: 'var(--ink-700)', fontSize: 13, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}>{p}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 22 }}>
|
||||
<Btn variant="secondary" onClick={onClose}>Cancel</Btn>
|
||||
<Btn variant="primary" size="lg" onClick={onClose}><Icon name="send" size={16} color="white" /> Send</Btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- End-day modal
|
||||
function EndDayModal({ open, onClose, onConfirm }) {
|
||||
if (!open) return null;
|
||||
const k = OPS_DATA.kpis;
|
||||
return (
|
||||
<div onClick={onClose} style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'rgba(16,20,24,0.5)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
zIndex: 50, padding: 20,
|
||||
}}>
|
||||
<div onClick={(e) => e.stopPropagation()} style={{
|
||||
width: 'min(560px, 100%)',
|
||||
background: 'white', borderRadius: 18, padding: 28,
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.25)',
|
||||
}}>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: 'var(--ink-900)', marginBottom: 6 }}>End business day?</div>
|
||||
<div style={{ fontSize: 14, color: 'var(--ink-500)', marginBottom: 22 }}>
|
||||
Today's session will close. Make sure all tables are paid and waiters have clocked out.
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
padding: '16px 18px',
|
||||
background: 'var(--bg)', borderRadius: 12,
|
||||
marginBottom: 22,
|
||||
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14,
|
||||
}}>
|
||||
{[
|
||||
['Revenue', '€' + k.revenue.toFixed(2)],
|
||||
['Covers', k.covers.toString()],
|
||||
['Open tables', `${k.tablesOpen} of ${k.tablesTotal}`],
|
||||
['Avg ticket', '€' + k.avgTicket.toFixed(2)],
|
||||
].map(([lbl, val]) => (
|
||||
<div key={lbl}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink-500)', textTransform: 'uppercase', letterSpacing: 0.6 }}>{lbl}</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: 'var(--ink-900)', fontFamily: "'Geist Mono', monospace", marginTop: 2 }}>{val}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{k.tablesOpen > 0 && (
|
||||
<div style={{
|
||||
padding: '12px 14px',
|
||||
background: 'var(--alert-50)', border: '1px solid var(--alert-100)',
|
||||
borderRadius: 10, marginBottom: 22,
|
||||
fontSize: 14, color: 'var(--alert-700)', fontWeight: 600,
|
||||
}}>
|
||||
⚠ {k.tablesOpen} tables are still open. They'll need to be closed manually.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
|
||||
<Btn variant="secondary" onClick={onClose}>Cancel</Btn>
|
||||
<Btn variant="danger" size="lg" onClick={onConfirm}>End day</Btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.OpsCards2 = { ShiftsCard, MessagesCard, ComposeModal, EndDayModal };
|
||||
Reference in New Issue
Block a user