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 => (
- setActiveCat(cat.id)}
- >
- {cat.name}
-
- ))}
+ {/* View All button — always first */}
+ setViewAllOpen(true)}
+ title="Εμφάνιση όλων"
+ >
+ ⊞
+
+
+ {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 (
+ setActiveCat(cat.id)}
+ >
+ {cat.name}
+
+ )
+ })}
@@ -35,6 +69,39 @@ export default function ProductPicker({ categories, products, onAdd }) {
)}
+ {/* View All modal */}
+ {viewAllOpen && (
+
setViewAllOpen(false)}>
+
e.stopPropagation()}
+ >
+
+ Κατηγορίες
+ setViewAllOpen(false)}>✕
+
+
+ {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 (
+ selectCategory(cat.id)}
+ >
+
+ {cat.name}
+
+ )
+ })}
+
+
+
+ )}
+
{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 (
+
+
+ navigate(`/tables/${tableId}`)}>←
+ Αποτέλεσμα εκτύπωσης
+
+
+
+
+
+ ⚠ Πρόβλημα εκτύπωσης
+
+
+ Η παραγγελία αποθηκεύτηκε αλλά ένας ή περισσότεροι εκτυπωτές δεν ανταποκρίθηκαν.
+ Τα αντικείμενα παραμένουν ως "σχέδιο" — δεν έχουν σταλεί στην κουζίνα/μπαρ.
+
+
+
+ {printAck.results.map((r, i) => (
+
+
{r.success ? '✓' : '✗'}
+
+
+ {r.printer_name}
+
+ {r.error && (
+
{r.error}
+ )}
+
+
+ ))}
+
+
+ navigate(`/tables/${tableId}`)}
+ >
+ Επιστροφή στο τραπέζι
+
+ {
+ setPrintAck(null)
+ setCart([])
+ navigate(`/tables/${tableId}`)
+ }}
+ >
+ Εντάξει, συνέχεια
+
+
+
+
+ )
+ }
+
return (