import { useEffect, useState } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import ProductPicker from '../components/ProductPicker' import OrderDrawer from '../components/OrderDrawer' import client from '../api/client' export default function AddItemsPage() { const { tableId } = useParams() const [searchParams] = useSearchParams() const isNewTable = searchParams.get('new') === '1' const navigate = useNavigate() const [categories, setCategories] = useState([]) const [products, setProducts] = useState([]) const [cart, setCart] = useState([]) const [orderId, setOrderId] = useState(null) const [sending, setSending] = useState(false) const [retrying, setRetrying] = useState(false) const [error, setError] = useState('') const [printAck, setPrintAck] = useState(null) const [cartOpen, setCartOpen] = useState(false) const [editItem, setEditItem] = useState(null) // { cartKey, product, drawerState } const [viewAllOpen, setViewAllOpen] = useState(false) const [searchOpen, setSearchOpen] = useState(false) const [searchQuery, setSearchQuery] = useState('') useEffect(() => { async function load() { const [catRes, prodRes, statusRes] = await Promise.all([ client.get('/api/products/categories'), client.get('/api/products/'), client.get(`/api/tables/${tableId}/status`), ]) setCategories(catRes.data) setProducts(prodRes.data) setOrderId(statusRes.data.active_order_id) // Pre-populate cart from "order again" if present const stored = sessionStorage.getItem('orderAgainItems') if (stored) { sessionStorage.removeItem('orderAgainItems') try { const items = JSON.parse(stored) const initialCart = items.map(it => ({ ...it, _key: Date.now() + Math.random(), })) setCart(initialCart) } catch {} } } load() }, [tableId]) // Back button: if this was a new table and nothing was added, leave the table FREE function handleBack() { if (isNewTable && cart.length === 0) { navigate('/tables', { replace: true }) } else { navigate(`/tables/${tableId}`) } } function addToCart(item) { setCart(prev => { // Try to find an identical item already in the cart to stack onto. // Two items are identical when every meaningful field matches exactly. const { _key: _k, _drawerState: _ds, ...newCore } = item const matchIdx = prev.findIndex(existing => { const { _key, _drawerState, ...existCore } = existing return JSON.stringify(existCore) === JSON.stringify(newCore) }) if (matchIdx !== -1) { const next = [...prev] next[matchIdx] = { ...next[matchIdx], quantity: next[matchIdx].quantity + (item.quantity ?? 1) } return next } return [...prev, { ...item, _key: Date.now() + Math.random() }] }) } function removeFromCart(key) { setCart(prev => prev.filter(i => i._key !== key)) } function changeCartQty(key, newQty) { if (newQty <= 0) { removeFromCart(key) } else { setCart(prev => prev.map(i => i._key === key ? { ...i, quantity: newQty } : i)) } } function openEditDrawer(cartItem) { const product = products.find(p => p.id === cartItem.product_id) if (!product) return setCartOpen(false) setEditItem({ cartKey: cartItem._key, product, drawerState: cartItem._drawerState }) } function handleEditSave(updatedItem) { setCart(prev => prev.map(i => i._key === editItem.cartKey ? { ...updatedItem, _key: i._key } : i )) setEditItem(null) } async function sendOrder() { if (cart.length === 0) return setSending(true) setError('') setPrintAck(null) setCartOpen(false) try { // For new (free) tables, open the order now — lazily let activeOrderId = orderId if (!activeOrderId) { const { data: newOrder } = await client.post('/api/orders/', { table_id: Number(tableId) }) activeOrderId = newOrder.id setOrderId(activeOrderId) } const res = await client.post(`/api/orders/${activeOrderId}/items`, { items: cart.map(({ _key, _drawerState, ...item }) => item), }) const printResults = res.data.print_results ?? [] const allOk = printResults.length === 0 || printResults.every(r => r.success) setPrintAck({ allOk, results: printResults }) if (allOk) setTimeout(() => navigate('/tables'), 1200) } catch (err) { setError(err.response?.data?.detail || 'Σφάλμα αποστολής — η παραγγελία δεν στάλθηκε') } finally { setSending(false) } } async function retryNow() { if (!orderId) return setRetrying(true) try { const res = await client.post(`/api/orders/${orderId}/retry-print`) const printResults = res.data.print_results ?? [] const allOk = printResults.length === 0 || printResults.every(r => r.success) setPrintAck({ allOk, results: printResults }) if (allOk) setTimeout(() => navigate('/tables'), 1200) } catch { } finally { setRetrying(false) } } function saveAsDraft() { navigate(`/tables/${tableId}`, { replace: true }) } function leaveAndContinue() { navigate(`/tables/${tableId}`, { replace: true }) } function getProduct(id) { return products.find(p => p.id === id) } // Returns structured sections for the expanded cart view function buildItemSections(item, product) { const sections = [] if (item.selected_options?.length) { const prefIds = new Set( (product?.preference_sets || []).flatMap(ps => ps.choices.map(c => c.id)) ) // Build a map: prefChoiceId → preference set name const prefSetByChoiceId = {} ;(product?.preference_sets || []).forEach(ps => { ps.choices.forEach(c => { prefSetByChoiceId[c.id] = ps.name }) }) const quickNames = new Set((product?.quick_options || []).map(o => o.name)) const extraIds = new Set((product?.options || []).map(o => o.id)) // Group prefs: { prefSetName, choiceName, subName } const prefGroups = [] // Group extras: { name, subName, qty } — one entry per unique (id) const extraGroups = [] const quickLines = [] let i = 0 const opts = item.selected_options while (i < opts.length) { const o = opts[i] if (prefIds.has(o.id)) { // Collect sub immediately following (id === null) const setName = prefSetByChoiceId[o.id] ?? '' let subName = null if (i + 1 < opts.length && opts[i + 1].id == null) { subName = opts[i + 1].name i += 2 } else { i++ } // Merge into existing prefGroup for same setName, or create new const existing = prefGroups.find(g => g.setName === setName) if (existing) { // multiple choices from same set (shouldn't normally happen, but handle gracefully) existing.values.push(subName ? `${o.name} · ${subName}` : o.name) } else { prefGroups.push({ setName, values: [subName ? `${o.name} · ${subName}` : o.name] }) } } else if (o.id != null && extraIds.has(o.id)) { // Collect sub immediately following let subName = null if (i + 1 < opts.length && opts[i + 1].id == null) { subName = opts[i + 1].name i += 2 } else { i++ } // Merge duplicates const existing = extraGroups.find(g => g.id === o.id && g.subName === subName) if (existing) existing.qty++ else extraGroups.push({ id: o.id, name: o.name, subName, qty: 1 }) } else if (quickNames.has(o.name)) { quickLines.push(o) i++ } else { i++ } } // Deduplicate quick lines: multiple entries of same name → single entry with qty const quickDeduped = [] quickLines.forEach(o => { const existing = quickDeduped.find(x => x.name === o.name) if (existing) existing._qty = (existing._qty || 1) + 1 else quickDeduped.push({ ...o, _qty: 1 }) }) if (prefGroups.length > 0) sections.push({ type: 'prefs', lines: prefGroups }) if (quickDeduped.length > 0) sections.push({ type: 'quick', lines: quickDeduped }) if (extraGroups.length > 0) sections.push({ type: 'extras', lines: extraGroups }) } if (item.removed_ingredients?.length) { sections.push({ type: 'removed', lines: item.removed_ingredients.map(n => ({ name: n })) }) } if (item.notes) { sections.push({ type: 'note', lines: [{ name: item.notes }] }) } return sections } // Simple flat summary for the collapsed one-liner function buildItemSummary(item) { const lines = [] if (item.selected_options?.length) { item.selected_options.forEach(o => { if (o.price_delta && o.price_delta !== 0) lines.push(`${o.name} (${o.price_delta > 0 ? '+' : ''}${o.price_delta.toFixed(2)} €)`) else lines.push(o.name) }) } if (item.removed_ingredients?.length) lines.push(`Χωρίς: ${item.removed_ingredients.join(', ')}`) if (item.notes) lines.push(item.notes) return lines } // Print-failure dialog if (printAck && !printAck.allOk) { return (
Πρόβλημα εκτύπωσης

⚠ Η παραγγελία αποθηκεύτηκε

Ένας ή περισσότεροι εκτυπωτές δεν ανταποκρίθηκαν.

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

{r.printer_name}

{!r.success &&

Εκτυπωτής μη προσβάσιμος

}
))}

Επιλέξτε πώς να συνεχίσετε:

) } // Compact names for the strip preview (max 3 items shown) const stripItems = cart.slice(-3).reverse() const hiddenCount = cart.length > 3 ? cart.length - 3 : 0 return (
{isNewTable ? 'Νέα Παραγγελία' : 'Προσθήκη'}
{/* Search button */} {/* Categories button */} {/* Cart button with badge */}
{/* Product picker takes all remaining space */} {categories.length > 0 && ( )} {/* ── Bottom bar: floating mini-cart + full-width ΑΠΟΣΤΟΛΗ ─────────────── */}
{/* Floating compact cart — shown only when there are items */} {cart.length > 0 && (
setCartOpen(true)} style={{ background: 'var(--bg3)', border: '1px solid var(--border)', borderRadius: 12, padding: '8px 12px', marginBottom: 10, cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 4, }} > {stripItems.map(item => { const p = getProduct(item.product_id) return (
{p?.name ?? `#${item.product_id}`} ×{item.quantity}
) })} {hiddenCount > 0 && (
+{hiddenCount} ακόμα — δείτε όλα →
)}
)} {/* Full-width send button */} {error &&

{error}

}
{/* ── Cart side drawer ────────────────────────────────────────────────── */} <> {/* Backdrop */}
setCartOpen(false)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', opacity: cartOpen ? 1 : 0, pointerEvents: cartOpen ? 'auto' : 'none', transition: 'opacity 240ms ease', zIndex: 50, }} /> {/* Panel */}
{/* Drawer header */}
Παραγγελία
{cart.length} {cart.length === 1 ? 'προϊόν' : 'προϊόντα'}
{/* Item list */}
{cart.length === 0 ? (

Η παραγγελία είναι κενή.

) : (
{cart.map(item => { const product = getProduct(item.product_id) const summaryLines = buildItemSummary(item) const sections = buildItemSections(item, product) return ( openEditDrawer(item)} onRemove={() => removeFromCart(item._key)} onChangeQty={qty => changeCartQty(item._key, qty)} /> ) })}
)}
{/* Drawer footer */}
{/* Edit drawer */} {editItem && ( setEditItem(null)} onAdd={handleEditSave} initialState={editItem.drawerState} /> )} {/* ── Search modal ─────────────────────────────────────────────────────── */} {searchOpen && ( setSearchOpen(false)} onAdd={item => { addToCart(item); setSearchOpen(false) }} /> )} {/* Full-screen success overlay — blocks all interaction while navigating */} {printAck?.allOk && (
Εκτυπώθηκε Επιτυχώς
)}
) } // ── Cart Item (used in the side drawer) ─────────────────────────────────────── const SECTION_META = { prefs: { icon: '◉', label: null }, quick: { icon: '>', label: null }, extras: { icon: '+', label: null }, removed: { icon: '−', label: null }, note: { icon: 'i', label: null }, } function SectionIcon({ type }) { const icons = { prefs: , quick: , extras: , removed: , note: , } return {icons[type] ?? null} } function CartItem({ item, product, summaryLines, sections, onEdit, onRemove, onChangeQty }) { const [expanded, setExpanded] = useState(false) const hasDetails = sections.length > 0 return (
{/* Whole header row is always clickable to expand (qty stepper is always available) */}
setExpanded(e => !e)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 12px', cursor: 'pointer' }} > {/* Chevron — always shown */} {/* Name */}
{product?.name ?? `#${item.product_id}`}
{!expanded && hasDetails && (
{summaryLines[0]}{summaryLines.length > 1 ? ` +${summaryLines.length - 1}` : ''}
)}
{/* Quantity on the right */} ×{item.quantity} {/* Edit — stop propagation so it doesn't toggle expand */} {/* Remove */}
{expanded && (
{sections.map((sec, si) => (
{sec.type === 'prefs' && sec.lines.map((line, li) => (
{line.setName} {line.values.join(' · ')}
))} {sec.type === 'quick' && sec.lines.map((line, li) => (
{line.name} {line._qty > 1 && ×{line._qty}}
))} {sec.type === 'extras' && sec.lines.map((line, li) => (
{line.name} {line.subName && · {line.subName}} {line.qty > 1 && ×{line.qty}}
))} {sec.type === 'removed' && sec.lines.map((line, li) => (
Χωρίς {line.name}
))} {sec.type === 'note' && sec.lines.map((line, li) => (
{line.name}
))}
))} {/* ── Quick qty row ── */}
{item.quantity}
)}
) } // ── Search Modal ────────────────────────────────────────────────────────────── function SearchModal({ products, query, setQuery, onClose, onAdd }) { const [drawerProduct, setDrawerProduct] = useState(null) const activeProducts = products.filter(p => p.lifecycle_status !== 'archived') const results = query.trim().length === 0 ? [] : activeProducts.filter(p => p.name.toLowerCase().includes(query.trim().toLowerCase()) ) function openProduct(p) { // Blur the input first so the keyboard dismisses, then open the drawer document.activeElement?.blur() setDrawerProduct(p) } // The modal is position:fixed anchored to bottom:0. // When the soft keyboard opens on mobile the browser shrinks the visual // viewport and fixed elements reposition automatically — the panel sits // right on top of the keyboard without any JS measurement needed. return ( <> {/* Dim backdrop — tap to close */}
{/* Panel: fixed to bottom, grows upward, capped at 60vh so results don't push the input off screen on short viewports */}
{/* Results scroll area — flex:1 so it takes space above the input */}
{query.trim().length === 0 ? (

Πληκτρολογήστε για αναζήτηση…

) : results.length === 0 ? (

Δεν βρέθηκαν προϊόντα για «{query}»

) : results.map(p => { const initials = p.name.trim().split(/\s+/).slice(0, 2).map(w => w[0]).join('').toUpperCase() return ( ) })}
{/* Search input — pinned at the bottom of the panel, above the keyboard */}
setQuery(e.target.value)} placeholder="Αναζήτηση προϊόντος…" style={{ flex: 1, height: 44, background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 12, padding: '0 12px', fontSize: 16, color: 'var(--text)', fontFamily: 'inherit', outline: 'none', }} />
{/* Product drawer — closes search modal when item is added */} {drawerProduct && ( setDrawerProduct(null)} onAdd={item => { onAdd(item); setDrawerProduct(null); onClose() }} /> )} ) }