From ee51e52acfc7dbd4464ff5837079e45333dd4785 Mon Sep 17 00:00:00 2001 From: bonamin Date: Fri, 24 Apr 2026 17:35:22 +0300 Subject: [PATCH] =?UTF-8?q?Waiter=20PWA:=20UX=20polish=20=E2=80=94=20table?= =?UTF-8?q?=20names,=20category=20colours,=20print=20ack,=20PIN=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TableCard: show table.label (display name) instead of internal number - TableListPage: zone filter rows 50% taller; table cards capped at 132px max-height so single-table zones don't stretch; grid aligns to top - ProductPicker: category tabs use their configured colour (inactive=35% opacity); new View All button opens a full-screen category tile modal - AddItemsPage: show per-printer print acknowledgement after sending order; print failures keep items as drafts and show a clear error screen - PinPad: reduced to 4 dots/digits with auto-submit on 4th digit Co-Authored-By: Claude Sonnet 4.6 --- waiter_pwa/src/components/PinPad.jsx | 7 +- waiter_pwa/src/components/ProductPicker.jsx | 85 ++++++++++++++++-- waiter_pwa/src/components/TableCard.jsx | 5 +- waiter_pwa/src/index.css | 74 +++++++++++++++- waiter_pwa/src/pages/AddItemsPage.jsx | 95 ++++++++++++++++++++- waiter_pwa/src/pages/TableListPage.jsx | 10 +-- 6 files changed, 253 insertions(+), 23 deletions(-) diff --git a/waiter_pwa/src/components/PinPad.jsx b/waiter_pwa/src/components/PinPad.jsx index 009075a..0597446 100644 --- a/waiter_pwa/src/components/PinPad.jsx +++ b/waiter_pwa/src/components/PinPad.jsx @@ -4,7 +4,10 @@ export default function PinPad({ onSubmit, loading }) { const [pin, setPin] = useState('') function press(digit) { - if (pin.length < 8) setPin(p => p + digit) + if (pin.length >= 4) return + const next = pin + digit + setPin(next) + if (next.length === 4 && !loading) onSubmit(next) } function backspace() { @@ -15,7 +18,7 @@ export default function PinPad({ onSubmit, loading }) { if (pin.length > 0 && !loading) onSubmit(pin) } - const dots = Array.from({ length: 8 }, (_, i) => ( + const dots = Array.from({ length: 4 }, (_, i) => ( )) diff --git a/waiter_pwa/src/components/ProductPicker.jsx b/waiter_pwa/src/components/ProductPicker.jsx index ee17e80..16f2157 100644 --- a/waiter_pwa/src/components/ProductPicker.jsx +++ b/waiter_pwa/src/components/ProductPicker.jsx @@ -1,24 +1,58 @@ import { useState } from 'react' import ItemOptionsModal from './ItemOptionsModal' +function hexToRgba(hex, alpha) { + if (!hex) return null + const h = hex.replace('#', '') + const r = parseInt(h.substring(0, 2), 16) + const g = parseInt(h.substring(2, 4), 16) + const b = parseInt(h.substring(4, 6), 16) + return `rgba(${r},${g},${b},${alpha})` +} + export default function ProductPicker({ categories, products, onAdd }) { const [activeCat, setActiveCat] = useState(categories[0]?.id ?? null) const [selectedProduct, setSelectedProduct] = useState(null) + const [viewAllOpen, setViewAllOpen] = useState(false) const filtered = products.filter(p => p.category_id === activeCat) + function selectCategory(id) { + setActiveCat(id) + setViewAllOpen(false) + } + return (
- {categories.map(cat => ( - - ))} + {/* View All button — always first */} + + + {categories.map(cat => { + const isActive = activeCat === cat.id + const bg = cat.color + ? isActive ? cat.color : hexToRgba(cat.color, 0.35) + : isActive ? 'var(--accent)' : 'var(--bg3)' + const color = cat.color + ? isActive ? '#fff' : 'rgba(255,255,255,0.65)' + : isActive ? '#1c1400' : 'var(--muted)' + return ( + + ) + })}
@@ -35,6 +69,39 @@ export default function ProductPicker({ categories, products, onAdd }) { )}
+ {/* View All modal */} + {viewAllOpen && ( +
setViewAllOpen(false)}> +
e.stopPropagation()} + > +
+ Κατηγορίες + +
+
+ {categories.map(cat => { + const isActive = activeCat === cat.id + const bg = cat.color || 'var(--bg3)' + const overlay = isActive ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.35)' + return ( + + ) + })} +
+
+
+ )} + {selectedProduct && ( - {table.number} - {table.name && {table.name}} + {displayName} {statusLabel} ) diff --git a/waiter_pwa/src/index.css b/waiter_pwa/src/index.css index 40154cb..5073256 100644 --- a/waiter_pwa/src/index.css +++ b/waiter_pwa/src/index.css @@ -169,7 +169,7 @@ body { background: var(--bg); } grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; padding: 16px; - flex: 1; + align-content: start; } .table-card { display: flex; @@ -177,7 +177,8 @@ body { background: var(--bg); } align-items: center; justify-content: center; gap: 4px; - min-height: 110px; + min-height: 132px; + max-height: 132px; border-radius: 14px; border: 2px solid transparent; cursor: pointer; @@ -239,15 +240,82 @@ body { background: var(--bg); } flex-shrink: 0; padding: 8px 16px; border-radius: 20px; - border: none; + border: 2px solid transparent; background: var(--bg3); color: var(--muted); font-size: 14px; font-weight: 600; cursor: pointer; white-space: nowrap; + transition: filter 0.12s; } .cat-tab--active { background: var(--accent); color: #1c1400; } +.cat-tab--viewall { + background: var(--bg3); + color: var(--text); + font-size: 18px; + padding: 4px 12px; + border-radius: 10px; +} + +/* ── Category All Modal ──────────────────────────────────── */ +.cat-all-modal { + position: fixed; + inset: 20px; + background: var(--bg2); + border-radius: 20px; + display: flex; + flex-direction: column; + z-index: 200; + overflow: hidden; +} +.cat-all-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + border-bottom: 1px solid var(--border); +} +.cat-all-modal__title { + font-size: 18px; + font-weight: 700; + color: var(--text); +} +.cat-all-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 12px; + padding: 16px; + overflow-y: auto; + flex: 1; +} +.cat-all-tile { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 90px; + border-radius: 14px; + border: none; + cursor: pointer; + overflow: hidden; + padding: 8px; +} +.cat-all-tile--active { outline: 3px solid #fff; } +.cat-all-tile__overlay { + position: absolute; + inset: 0; + pointer-events: none; +} +.cat-all-tile__name { + position: relative; + font-size: 14px; + font-weight: 700; + color: #fff; + text-align: center; + text-shadow: 0 1px 4px rgba(0,0,0,0.6); + line-height: 1.3; +} /* ── Product Grid ────────────────────────────────────────── */ .product-picker { display: flex; flex-direction: column; flex: 1; } diff --git a/waiter_pwa/src/pages/AddItemsPage.jsx b/waiter_pwa/src/pages/AddItemsPage.jsx index d5c9fb6..3e87a27 100644 --- a/waiter_pwa/src/pages/AddItemsPage.jsx +++ b/waiter_pwa/src/pages/AddItemsPage.jsx @@ -13,6 +13,8 @@ export default function AddItemsPage() { const [orderId, setOrderId] = useState(null) const [sending, setSending] = useState(false) const [error, setError] = useState('') + // null = not yet sent, { allOk, results } = sent + const [printAck, setPrintAck] = useState(null) useEffect(() => { async function load() { @@ -40,11 +42,19 @@ export default function AddItemsPage() { if (cart.length === 0 || !orderId) return setSending(true) setError('') + setPrintAck(null) try { - await client.post(`/api/orders/${orderId}/items`, { + const res = await client.post(`/api/orders/${orderId}/items`, { items: cart.map(({ _key, ...item }) => item), }) - navigate(`/tables/${tableId}`) + const printResults = res.data.print_results ?? [] + const allOk = printResults.length === 0 || printResults.every(r => r.success) + setPrintAck({ allOk, results: printResults }) + if (allOk) { + // All printed fine — navigate back after a short moment + setTimeout(() => navigate(`/tables/${tableId}`), 1200) + } + // If there were print failures, stay on page — waiter sees the ack panel } catch (err) { setError(err.response?.data?.detail || 'Σφάλμα αποστολής — η παραγγελία δεν στάλθηκε') } finally { @@ -56,6 +66,76 @@ export default function AddItemsPage() { return products.find(p => p.id === id)?.name || `#${id}` } + // If we have a print ack with failures, show the ack overlay instead of the normal UI + if (printAck && !printAck.allOk) { + return ( +
+
+ + Αποτέλεσμα εκτύπωσης +
+ +
+
+

+ ⚠ Πρόβλημα εκτύπωσης +

+

+ Η παραγγελία αποθηκεύτηκε αλλά ένας ή περισσότεροι εκτυπωτές δεν ανταποκρίθηκαν. + Τα αντικείμενα παραμένουν ως "σχέδιο" — δεν έχουν σταλεί στην κουζίνα/μπαρ. +

+
+ + {printAck.results.map((r, i) => ( +
+ {r.success ? '✓' : '✗'} +
+

+ {r.printer_name} +

+ {r.error && ( +

{r.error}

+ )} +
+
+ ))} + +
+ + +
+
+
+ ) + } + return (
@@ -91,6 +171,17 @@ export default function AddItemsPage() { {error &&

{error}

} + {/* Success flash when all printers OK */} + {printAck?.allOk && ( +
+ ✓ Εκτυπώθηκε επιτυχώς — μεταφορά… +
+ )} + ))} @@ -142,7 +142,7 @@ export default function TableListPage() { onClick={() => toggleZone('none')} style={{ display: 'block', width: '100%', textAlign: 'left', - padding: '8px 12px', borderRadius: 8, fontSize: 14, + padding: '12px 14px', borderRadius: 8, fontSize: 15, color: selectedZones.has('none') ? '#fff' : '#374151', background: selectedZones.has('none') ? '#4f46e5' : 'transparent', border: 'none', cursor: 'pointer',