// Order drawer — add/customize a product before sending to the kitchen.
// Mobile-portrait, ~90% height bottom sheet with horizontal tabs.
const { DIAVOLA } = window;
// --- utils -----------------------------------------------------------------
const fmt = (n) => {
const sign = n < 0 ? '−' : '';
return sign + '€' + Math.abs(n).toFixed(2);
};
const fmtSigned = (n) => (n >= 0 ? '+' : '−') + '€' + Math.abs(n).toFixed(2);
// --- stepper (big touch) ---------------------------------------------------
function Stepper({ value, onChange, min = 0, max = 99, size = 'md' }) {
const sizes = {
md: { btn: 40, font: 18, w: 108 },
lg: { btn: 48, font: 22, w: 132 },
};
const s = sizes[size];
return (
e.stopPropagation()}>
{value}
);
}
// --- checkmark icon --------------------------------------------------------
function Check({ size = 18 }) {
return (
);
}
function Chevron({ open, size = 16 }) {
return (
);
}
// --- Row primitive (shared look across tabs) -------------------------------
function Row({ selected, onClick, left, right, children }) {
return (
{left &&
{left}
}
{children}
{right &&
{right}
}
);
}
// --- Checkbox circle (selected / not) --------------------------------------
function CheckCircle({ selected, size = 26 }) {
return (
{selected && }
);
}
// --- Radio dot -------------------------------------------------------------
function RadioDot({ selected, size = 22 }) {
return (
);
}
// ===========================================================================
// QUICK OPTIONS TAB
// ===========================================================================
function QuickOptionsTab({ options, state, setState }) {
return (
{options.map(opt => {
const qty = state[opt.id] || 0;
const selected = qty > 0;
return (
setState({ ...state, [opt.id]: selected ? 0 : 1 })}
left={!opt.multi && }
right={
opt.multi ? (
selected ? (
setState({ ...state, [opt.id]: v })} />
) : (
)
) : null
}
>
{opt.label}
{opt.price > 0 && (
+€{opt.price.toFixed(2)} {opt.multi ? 'each' : ''}
)}
);
})}
);
}
// ===========================================================================
// EXTRAS TAB — each row expands inline to pick a sub-option
// ===========================================================================
function ExtrasTab({ extras, state, setState, expandedId, setExpandedId }) {
return (
{extras.map(ex => {
const selection = state[ex.id]; // { qty, subId } | undefined
const selected = !!selection;
const open = expandedId === ex.id;
const subLabel = selection
? ex.subOptions.find(s => s.id === selection.subId)?.label
: null;
const toggle = () => {
if (selected) {
setState({ ...state, [ex.id]: undefined });
if (open) setExpandedId(null);
} else {
// default to first sub-option, auto-expand so user can change it
setState({ ...state, [ex.id]: { qty: 1, subId: ex.subOptions[0].id } });
setExpandedId(ex.id);
}
};
return (
|
}
right={
selected ? (
e.stopPropagation()}>
{ex.multi && (
setState({ ...state, [ex.id]: v === 0 ? undefined : { ...selection, qty: v } })}
/>
)}
) : null
}
>
{ex.label}
{ex.price > 0 ? `+€${ex.price.toFixed(2)}${ex.multi ? ' each' : ''}` : 'Included'}
{subLabel && · {subLabel}}
{/* Inline sub-option picker */}
{selected && open && (
{ex.subLabel}
{ex.subOptions.map(sub => {
const isSel = selection.subId === sub.id;
return (
setState({ ...state, [ex.id]: { ...selection, subId: sub.id } })}
left={}
>
{sub.label}
{sub.price > 0 && (
+€{sub.price.toFixed(2)}
)}
);
})}
)}
);
})}
);
}
// ===========================================================================
// INGREDIENTS TAB — remove only
// ===========================================================================
function IngredientsTab({ ingredients, state, setState }) {
return (
Tap to remove ingredients from this item.
{ingredients.map(ing => {
const removed = !!state[ing.id];
return (
setState({ ...state, [ing.id]: !removed })}
right={
{removed ? 'Removed' : 'Remove'}
}
>
{ing.label}
);
})}
);
}
// ===========================================================================
// PREFERENCES TAB — radio groups
// ===========================================================================
function PreferencesTab({ preferences, state, setState }) {
return (
{preferences.map(pref => {
const selected = state[pref.id];
return (
{pref.label}
{pref.required && (
Required
)}
{pref.subOptions.map(sub => {
const isSel = selected === sub.id;
return (
setState({ ...state, [pref.id]: sub.id })}
left={}
right={
sub.price !== 0 ? (
{fmtSigned(sub.price)}
) : null
}
>
{sub.label}
);
})}
);
})}
);
}
// ===========================================================================
// NOTES TAB
// ===========================================================================
const QUICK_NOTES = ['No eye contact 😅', 'Table is in a hurry', 'Customer is allergic', 'Cut in smaller pieces', 'Leave box open'];
function NotesTab({ note, setNote }) {
return (
Anything specific for the kitchen. Short and clear works best.
);
}
// ===========================================================================
// SUMMARY TAB — final config review
// ===========================================================================
function SummaryTab({ product, config, lines, onJumpTab }) {
const sectionStyle = { marginBottom: 22 };
const headerStyle = {
fontSize: 12, fontWeight: 700, color: 'var(--ink-500)',
textTransform: 'uppercase', letterSpacing: 0.8,
padding: '0 2px 8px',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
};
const editBtn = (tab) => (
);
// Group summary lines
const groups = {
pref: lines.filter(l => l.group === 'pref'),
quick: lines.filter(l => l.group === 'quick'),
extra: lines.filter(l => l.group === 'extra'),
removed: lines.filter(l => l.group === 'removed'),
};
const LineItem = ({ l }) => (
{l.qty > 1 && {l.qty}×}
{l.label}
{l.detail && (
{l.detail}
)}
{l.price !== 0 && (
{fmtSigned(l.price)}
)}
);
const isEmpty = lines.length === 0 && !config.note;
return (
{/* Product summary header */}
{product.emoji}
{product.name}
Base €{product.price.toFixed(2)}
{isEmpty && (
No customization yet. Switch to other tabs to add options.
)}
{groups.pref.length > 0 && (
Preferences
{editBtn('preferences')}
{groups.pref.map((l, i) => )}
)}
{groups.quick.length > 0 && (
Quick options
{editBtn('quick')}
{groups.quick.map((l, i) => )}
)}
{groups.extra.length > 0 && (
Extras
{editBtn('extras')}
{groups.extra.map((l, i) => )}
)}
{groups.removed.length > 0 && (
Removed
{editBtn('ingredients')}
{groups.removed.map((l, i) => )}
)}
{config.note && (
Note
{editBtn('notes')}
{config.note}
)}
);
}
// ===========================================================================
// MAIN DRAWER
// ===========================================================================
function OrderDrawer({ product, isOpen, onClose, onAddToOrder }) {
// Tabs available for this product (hidden if empty)
const tabs = React.useMemo(() => {
const list = [];
if (product.quickOptions?.length) list.push({ id: 'quick', label: 'Quick' });
if (product.extras?.length) list.push({ id: 'extras', label: 'Extras' });
if (product.ingredients?.length) list.push({ id: 'ingredients', label: 'Ingredients' });
if (product.preferences?.length) list.push({ id: 'preferences', label: 'Preferences' });
list.push({ id: 'notes', label: 'Notes' });
list.push({ id: 'summary', label: 'Summary' });
return list;
}, [product]);
// --- STATE ---
const initialPrefs = () => {
const p = {};
(product.preferences || []).forEach(pref => {
const def = pref.subOptions.find(s => s.default);
if (def) p[pref.id] = def.id;
});
return p;
};
const [activeTab, setActiveTab] = React.useState(tabs[0].id);
const [quick, setQuick] = React.useState({}); // { id: qty }
const [extras, setExtras] = React.useState({}); // { id: {qty, subId} }
const [extraExpanded, setExtraExpanded] = React.useState(null);
const [removed, setRemoved] = React.useState({}); // { id: true }
const [prefs, setPrefs] = React.useState(initialPrefs());
const [note, setNote] = React.useState('');
const [qty, setQty] = React.useState(1);
// Reset when drawer opens with a (possibly different) product
React.useEffect(() => {
if (isOpen) {
setActiveTab(tabs[0].id);
setQuick({});
setExtras({});
setExtraExpanded(null);
setRemoved({});
setPrefs(initialPrefs());
setNote('');
setQty(1);
}
}, [isOpen, product.id]);
const config = { quick, extras, removed, prefs, note };
// --- Derived: summary lines -------------------------------------------
const { lines, itemPrice } = React.useMemo(() => {
const L = [];
let p = product.price;
// Preferences
(product.preferences || []).forEach(pref => {
const selId = prefs[pref.id];
if (!selId) return;
const sub = pref.subOptions.find(s => s.id === selId);
if (!sub) return;
if (!sub.default || sub.price !== 0) {
L.push({
group: 'pref',
label: `${pref.label}: ${sub.label}`,
qty: 1,
price: sub.price,
});
}
p += sub.price;
});
// Quick
(product.quickOptions || []).forEach(opt => {
const q = quick[opt.id] || 0;
if (q === 0) return;
const linePrice = opt.price * q;
L.push({
group: 'quick',
label: opt.label,
qty: q,
price: linePrice,
});
p += linePrice;
});
// Extras
(product.extras || []).forEach(ex => {
const sel = extras[ex.id];
if (!sel) return;
const sub = ex.subOptions.find(s => s.id === sel.subId);
const linePrice = (ex.price + (sub?.price || 0)) * sel.qty;
L.push({
group: 'extra',
label: ex.label,
detail: sub?.label,
qty: sel.qty,
price: linePrice,
});
p += linePrice;
});
// Removed ingredients
(product.ingredients || []).forEach(ing => {
if (removed[ing.id]) {
L.push({
group: 'removed',
label: `No ${ing.label.toLowerCase()}`,
qty: 1,
price: 0,
});
}
});
return { lines: L, itemPrice: p };
}, [prefs, quick, extras, removed, product]);
const total = itemPrice * qty;
const customizationCount = lines.length + (note ? 1 : 0);
// Backdrop click to close
const handleBackdrop = (e) => {
if (e.target === e.currentTarget) onClose();
};
// --- Render ---
return (
<>
{/* Backdrop */}
{/* Sheet */}
{/* Grab handle */}
{/* Header */}
{product.emoji}
{product.name}
{product.desc}
{/* Tabs */}
{tabs.map(t => {
const active = activeTab === t.id;
const badge = (
t.id === 'quick' ? Object.values(quick).filter(v => v > 0).length :
t.id === 'extras' ? Object.values(extras).filter(Boolean).length :
t.id === 'ingredients' ? Object.values(removed).filter(Boolean).length :
t.id === 'notes' ? (note ? 1 : 0) :
t.id === 'summary' ? customizationCount :
0
);
return (
);
})}
{/* Scrollable content */}
{activeTab === 'quick' &&
}
{activeTab === 'extras' &&
}
{activeTab === 'ingredients' &&
}
{activeTab === 'preferences' &&
}
{activeTab === 'notes' &&
}
{activeTab === 'summary' &&
}
{/* Footer — qty stepper + Add to Order */}
>
);
}
window.OrderDrawer = OrderDrawer;