import { useEffect, useState, useRef } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import OrderSummary from '../components/OrderSummary' import useAuthStore from '../store/authStore' import client from '../api/client' import { TransferIcon, MergeIcon, FlagsIcon, WaiterIcon, PrintIcon } from '../components/Icons' function fmtPrice(v) { return Number(v).toFixed(2) + ' €' } // ─── Print results modal ────────────────────────────────────────────────────── function PrintResultsModal({ results, onClose }) { return (
e.stopPropagation()}>

Αποτέλεσμα εκτύπωσης

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

{r.printer_name}

{!r.success && (

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

)}
))}
) } // ─── Split stepper modal ────────────────────────────────────────────────────── function SplitModal({ item, onConfirm, onClose }) { const [splitQty, setSplitQty] = useState(1) const max = item.quantity - 1 return (
e.stopPropagation()}>

Διαχωρισμός

{item.product?.name} ×{item.quantity}

Χώρισε σε πόσα;

{splitQty}
Νέα γραμμή: ×{splitQty} Μένει: ×{item.quantity - splitQty}
) } // ─── Actions top sheet ──────────────────────────────────────────────────────── function ActionsSheet({ order, tableId, onClose, onTransfer, onMerge, onSetFlags, onAssignWaiter, onPrintSynopsis }) { const hasOrder = !!order const actions = [ { Icon: TransferIcon, label: 'Μεταφορά Τραπεζιού', sub: 'Μεταφορά σε άλλο τραπέζι', onClick: hasOrder ? onTransfer : null, color: '#6099db', iconBg: 'rgba(96,165,250,0.15)' }, { Icon: MergeIcon, label: 'Συγχώνευση Τραπεζιού', sub: 'Συγχώνευση με άλλο τραπέζι', onClick: hasOrder ? onMerge : null, color: '#6099db', iconBg: 'rgba(96,165,250,0.15)' }, { Icon: FlagsIcon, label: 'Ενδείξεις Τραπεζιού', sub: 'Επιλογή σημαιών', onClick: onSetFlags, color: '#fac823', iconBg: 'rgba(251,191,36,0.15)' }, { Icon: WaiterIcon, label: 'Ανάθεση Σερβιτόρου', sub: 'Προσθήκη σερβιτόρου στην παραγγελία', onClick: hasOrder ? onAssignWaiter : null, color: '#39b861', iconBg: 'rgba(34,197,94,0.15)' }, { Icon: PrintIcon, label: 'Εκτύπωση Σύνοψης', sub: 'Εκτύπωση σύνοψης παραγγελίας', onClick: hasOrder ? onPrintSynopsis : null, color: '#cbd5e1', iconBg: 'rgba(148,163,184,0.15)' }, ] return (
e.stopPropagation()} style={{ gap: 0 }}>

ACTIONS

{actions.map((a, i) => ( ))}
) } // ─── Table picker (for transfer / merge / move-items) ───────────────────────── function TablePicker({ title, subtitle, tables, currentTableId, onSelect, onClose, loading }) { return (
e.stopPropagation()}>

{title}

{subtitle &&

{subtitle}

}
{loading && (

Φόρτωση…

)} {!loading && tables.length === 0 && (

Δεν υπάρχουν διαθέσιμα τραπέζια

)} {!loading && tables.map(t => ( ))}
) } // ─── Flag picker ────────────────────────────────────────────────────────────── function FlagPicker({ tableId, currentFlagIds, flagDefs, onSave, onClose, loading }) { const [selected, setSelected] = useState(currentFlagIds || []) // Sync selected when currentFlagIds loads useEffect(() => { setSelected(currentFlagIds || []) }, [currentFlagIds.join(',')]) function toggle(id) { setSelected(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]) } return (
e.stopPropagation()}>

Ενδείξεις Τραπεζιού

{loading && (

Φόρτωση…

)} {!loading && flagDefs.length === 0 && (

Δεν υπάρχουν σημαίες

)} {!loading && flagDefs.map(f => { const sel = selected.includes(f.id) return ( ) })}
) } // ─── Assign waiter picker ───────────────────────────────────────────────────── function AssignWaiterPicker({ orderId, currentWaiterIds, waiters, onAssigned, onClose, loading }) { const [saving, setSaving] = useState(false) async function assign(waiterId) { setSaving(true) try { await client.put(`/api/orders/${orderId}/assign-waiter`, { waiter_id: waiterId }) onAssigned() onClose() } catch { // already assigned or error — just close onClose() } finally { setSaving(false) } } const available = waiters.filter(w => !currentWaiterIds.includes(w.id)) return (
e.stopPropagation()}>

Ανάθεση Σερβιτόρου

{loading && (

Φόρτωση…

)} {!loading && available.length === 0 && (

Όλοι οι σερβιτόροι έχουν ήδη ανατεθεί

)} {!loading && available.map(w => ( ))}
) } // ─── Print synopsis picker ──────────────────────────────────────────────────── function PrintSynopsisPicker({ orderId, onClose }) { const [printers, setPrinters] = useState([]) const [printing, setPrinting] = useState(false) useEffect(() => { client.get('/api/system/status').then(r => setPrinters(r.data?.printers || [])).catch(() => {}) }, []) async function print(printerId) { setPrinting(true) try { await client.post(`/api/orders/${orderId}/print-synopsis`, { printer_id: printerId }) onClose() } catch { onClose() } finally { setPrinting(false) } } return (
e.stopPropagation()}>

Εκτύπωση Σύνοψης

Επιλέξτε εκτυπωτή

{printers.filter(p => p.reachable).map(p => ( ))} {printers.filter(p => !p.reachable).map(p => (
🖨️ {p.name} Offline
))}
) } // ─── Pay confirm modal ──────────────────────────────────────────────────────── function PayConfirmModal({ payAll, payIds, activeItems, onConfirm, onClose }) { const payTotal = activeItems .filter(i => payIds.includes(i.id)) .reduce((s, i) => s + i.unit_price * i.quantity, 0) return (
e.stopPropagation()}>

Επιβεβαίωση πληρωμής

{payAll ? 'Όλα τα αντικείμενα' : `${payIds.length} αντικείμενο${payIds.length !== 1 ? 'α' : ''}`}

{fmtPrice(payTotal)}

) } // ─── Main page ──────────────────────────────────────────────────────────────── export default function TableDetailPage() { const { tableId } = useParams() const [searchParams, setSearchParams] = useSearchParams() const { user } = useAuthStore() const navigate = useNavigate() const [table, setTable] = useState(null) const [order, setOrder] = useState(null) const [loading, setLoading] = useState(true) const [paying, setPaying] = useState(false) const [selectedIds, setSelectedIds] = useState([]) const [confirmClose, setConfirmClose] = useState(false) const [confirmPay, setConfirmPay] = useState(false) const [error, setError] = useState('') const [printResults, setPrintResults] = useState(null) const [retrying, setRetrying] = useState(false) // Tab: 'active' | 'paid' const [activeTab, setActiveTab] = useState('active') // Actions sheet state const [showActions, setShowActions] = useState(false) const [actionsMode, setActionsMode] = useState(null) // actionsMode: null | 'transfer' | 'merge' | 'flags' | 'assign_waiter' | 'print_synopsis' | 'split' | 'move_items' const [pendingTable, setPendingTable] = useState(null) // pendingTable: { table, mode: 'transfer'|'merge'|'move_items' } — waiting for confirmation // Data for sub-flows const [allTables, setAllTables] = useState([]) const [allOrders, setAllOrders] = useState([]) const [flagDefs, setFlagDefs] = useState([]) const [currentFlagIds, setCurrentFlagIds] = useState([]) const [allWaiters, setAllWaiters] = useState([]) const [actionDataLoading, setActionDataLoading] = useState(false) const [splitItem, setSplitItem] = useState(null) const scrollRef = useRef(null) async function load() { setLoading(true) try { const [statusRes, flagDefsRes, flagAssignRes] = await Promise.all([ client.get(`/api/tables/${tableId}/status`), client.get('/api/flags/defs'), client.get(`/api/flags/table/${tableId}`), ]) setTable(statusRes.data.table) if (statusRes.data.active_order_id) { const { data: o } = await client.get(`/api/orders/${statusRes.data.active_order_id}`) setOrder(o) } else { setOrder(null) } setFlagDefs(flagDefsRes.data) setCurrentFlagIds(flagAssignRes.data.map(a => a.flag_id)) } catch { setError('Σφάλμα φόρτωσης') } finally { setLoading(false) } } useEffect(() => { load() }, [tableId]) // Handle ?action= param from table list long-press quick actions useEffect(() => { const action = searchParams.get('action') if (!action || loading) return setSearchParams({}, { replace: true }) // Load supporting data, then jump straight to the action mode loadActionData() setActionsMode(action) }, [loading]) // Load supporting data for sub-flows (lazy, only when Actions opened) async function loadActionData() { setActionDataLoading(true) const [tablesRes, ordersRes, waitersRes] = await Promise.allSettled([ client.get('/api/tables/'), client.get('/api/orders/active'), client.get('/api/waiters/on-shift'), ]) if (tablesRes.status === 'fulfilled') setAllTables(tablesRes.value.data) if (ordersRes.status === 'fulfilled') setAllOrders(ordersRes.value.data) if (waitersRes.status === 'fulfilled') setAllWaiters(waitersRes.value.data) setActionDataLoading(false) } function openActions() { setAllTables([]) setAllOrders([]) setAllWaiters([]) loadActionData() setShowActions(true) } const activeItems = order?.items?.filter(i => i.status === 'active') || [] const paidItems = order?.items?.filter(i => i.status === 'paid') || [] const unprintedItems = activeItems.filter(i => !i.printed) const allPaid = order && activeItems.length === 0 const canInteract = !!order const allActiveSelected = activeItems.length > 0 && activeItems.every(i => selectedIds.includes(i.id)) async function sendDraftToKitchen() { if (!order || retrying) return setRetrying(true) try { const res = await client.post(`/api/orders/${order.id}/retry-print`) setPrintResults(res.data.print_results ?? []) await load() } catch { setError('Σφάλμα κατά την εκτύπωση') } finally { setRetrying(false) } } async function openOrder() { try { await client.post('/api/orders/', { table_id: Number(tableId) }) await load() } catch (err) { setError(err.response?.data?.detail || 'Σφάλμα ανοίγματος') } } async function paySelected() { setPaying(true) setConfirmPay(false) const idsToPayNow = selectedIds.length > 0 ? selectedIds : activeItems.map(i => i.id) try { await client.post(`/api/orders/${order.id}/pay`, { item_ids: idsToPayNow }) setSelectedIds([]) await load() } catch { setError('Σφάλμα πληρωμής') } finally { setPaying(false) } } async function closeOrder() { try { await client.post(`/api/orders/${order.id}/close`) setConfirmClose(false) navigate('/tables') } catch (err) { setError(err.response?.data?.detail || 'Σφάλμα κλεισίματος') } } function toggleItem(id) { setSelectedIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]) } function selectAll() { const allActive = activeItems.map(i => i.id) const allSelected = allActive.every(id => selectedIds.includes(id)) setSelectedIds(allSelected ? [] : allActive) } // Split item: called from SplitModal async function doSplit(qty) { if (!splitItem || !order) return setSplitItem(null) try { await client.post(`/api/orders/${order.id}/items/${splitItem.id}/split`, { quantity: qty }) setSelectedIds([]) await load() } catch (err) { setError(err.response?.data?.detail || 'Σφάλμα διαχωρισμού') } } // Transfer table async function doTransfer(targetTable) { if (!order) return setPendingTable(null) setActionsMode(null) try { await client.post(`/api/orders/${order.id}/transfer`, { target_table_id: targetTable.id }) navigate('/tables') } catch (err) { setError(err.response?.data?.detail || 'Σφάλμα μεταφοράς') } } // Merge table async function doMerge(targetTable) { if (!order) return const targetOrder = allOrders.find(o => o.table_id === targetTable.id) if (!targetOrder) { setError('Δεν βρέθηκε παραγγελία στο τραπέζι'); return } setPendingTable(null) setActionsMode(null) try { await client.post(`/api/orders/${order.id}/merge`, { target_order_id: targetOrder.id }) navigate('/tables') } catch (err) { setError(err.response?.data?.detail || 'Σφάλμα συγχώνευσης') } } // Move items to another table async function doMoveItems(targetTable) { if (!order || selectedIds.length === 0) return const targetOrder = allOrders.find(o => o.table_id === targetTable.id) if (!targetOrder) { setError('Δεν βρέθηκε παραγγελία στο τραπέζι'); return } setPendingTable(null) setActionsMode(null) try { await client.post(`/api/orders/${order.id}/move-items`, { item_ids: selectedIds, target_order_id: targetOrder.id, }) setSelectedIds([]) await load() } catch (err) { setError(err.response?.data?.detail || 'Σφάλμα μεταφοράς αντικειμένων') } } // Save flags async function doSaveFlags(flagIds) { try { await client.put(`/api/flags/table/${tableId}`, { flag_ids: flagIds }) setCurrentFlagIds(flagIds) setActionsMode(null) } catch { setError('Σφάλμα αποθήκευσης σημαιών') } } // Tables for transfer: active tables that are free (no active order), excluding current const transferTargets = allTables.filter(t => t.id !== Number(tableId) && t.is_active && !allOrders.some(o => o.table_id === t.id) ) // Tables for merge: tables with active orders, excluding current const mergeTargets = allTables .filter(t => t.id !== Number(tableId) && t.is_active) .map(t => { const o = allOrders.find(ord => ord.table_id === t.id) return o ? { ...t, orderStatus: o.status } : null }) .filter(Boolean) // Tables for move-items: tables with open/partially_paid orders, excluding current const moveItemsTargets = allTables .filter(t => t.id !== Number(tableId) && t.is_active) .map(t => { const o = allOrders.find(ord => ord.table_id === t.id) return o && (o.status === 'open' || o.status === 'partially_paid') ? { ...t, orderStatus: o.status } : null }) .filter(Boolean) const tableName = table ? (table.label || `T${table.number}`) : '—' if (loading) return

Φόρτωση…

return (
{/* Top bar */}
{tableName}
{error &&

{error}

} {/* Active flag cards */} {currentFlagIds.length > 0 && flagDefs.length > 0 && (
{currentFlagIds.map(fid => { const def = flagDefs.find(d => d.id === fid) if (!def) return null return ( ) })}
)} {!order && (

Δεν υπάρχει ενεργή παραγγελία

)} {order && (
{/* Unprinted items warning */} {unprintedItems.length > 0 && (

{unprintedItems.length} αντικείμενο{unprintedItems.length !== 1 ? 'α' : ''} δεν εκτυπώθηκε{unprintedItems.length !== 1 ? 'αν' : ''}

Δεν έχουν σταλεί στην κουζίνα/μπαρ

)} {/* ── Tabs ── */}
{activeTab === 'active' && ( <> { setSplitItem(item) }} /> {/* Floating controls row — only visible when items are selected */} {canInteract && activeItems.length > 0 && selectedIds.length > 0 && (
{/* Clear selection */} {/* Select all — hidden once everything is already selected */} {!allActiveSelected && ( )} {/* Transfer items */}
)} )} {activeTab === 'paid' && (
{paidItems.length === 0 ? (

Δεν υπάρχουν πληρωμένα αντικείμενα

) : ( {}} /> )}
)}
)} {/* Action bar — sticky at bottom, only when there's an active order on the active tab */} {order && canInteract && activeTab === 'active' && (
)} {/* Split stepper modal */} {splitItem && ( setSplitItem(null)} /> )} {/* Pay confirmation */} {confirmPay && ( i.id) : selectedIds} activeItems={activeItems} onConfirm={paySelected} onClose={() => setConfirmPay(false)} /> )} {/* Close confirmation */} {confirmClose && (
setConfirmClose(false)}>
e.stopPropagation()}>

Κλείσιμο παραγγελίας;

Αυτή η ενέργεια δεν αναιρείται.

)} {/* Print retry results */} {printResults && ( setPrintResults(null)} /> )} {/* Actions top sheet */} {showActions && actionsMode === null && ( setShowActions(false)} onTransfer={() => { setShowActions(false); setActionsMode('transfer') }} onMerge={() => { setShowActions(false); setActionsMode('merge') }} onSetFlags={() => { setShowActions(false); setActionsMode('flags') }} onAssignWaiter={() => { setShowActions(false); setActionsMode('assign_waiter') }} onPrintSynopsis={() => { setShowActions(false); setActionsMode('print_synopsis') }} /> )} {/* Transfer picker */} {actionsMode === 'transfer' && !pendingTable && ( setPendingTable({ table: t, mode: 'transfer' })} onClose={() => setActionsMode(null)} loading={actionDataLoading} /> )} {/* Merge picker */} {actionsMode === 'merge' && !pendingTable && ( setPendingTable({ table: t, mode: 'merge' })} onClose={() => setActionsMode(null)} loading={actionDataLoading} /> )} {/* Move items picker */} {actionsMode === 'move_items' && !pendingTable && ( setPendingTable({ table: t, mode: 'move_items' })} onClose={() => setActionsMode(null)} loading={actionDataLoading} /> )} {/* Transfer / Merge / Move-items confirmation */} {pendingTable && (
setPendingTable(null)}>
e.stopPropagation()}>

{pendingTable.mode === 'transfer' ? 'Επιβεβαίωση Μεταφοράς' : pendingTable.mode === 'merge' ? 'Επιβεβαίωση Συγχώνευσης' : 'Επιβεβαίωση Μεταφοράς Αντικειμένων'}

{pendingTable.mode === 'transfer' ? 'Μεταφορά παραγγελίας στο τραπέζι' : pendingTable.mode === 'merge' ? 'Συγχώνευση παραγγελίας με το τραπέζι' : `Μεταφορά ${selectedIds.length} αντικειμένου${selectedIds.length !== 1 ? 'ων' : ''} στο τραπέζι`}

{pendingTable.table.label || `T${pendingTable.table.number}`}

)} {/* Flag picker */} {actionsMode === 'flags' && ( setActionsMode(null)} loading={actionDataLoading} /> )} {/* Assign waiter */} {actionsMode === 'assign_waiter' && order && ( w.waiter_id)} waiters={allWaiters} onAssigned={load} onClose={() => setActionsMode(null)} loading={actionDataLoading} /> )} {/* Print synopsis */} {actionsMode === 'print_synopsis' && order && ( setActionsMode(null)} /> )}
) }