1222 lines
51 KiB
JavaScript
1222 lines
51 KiB
JavaScript
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 (
|
||
<div className="modal-overlay" onClick={onClose}>
|
||
<div className="modal-sheet" onClick={e => e.stopPropagation()}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title">Αποτέλεσμα εκτύπωσης</h2>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 20 }}>
|
||
{results.map((r, i) => (
|
||
<div key={i} style={{
|
||
display: 'flex', alignItems: 'center', gap: 10,
|
||
background: r.success ? '#14532d' : '#431407',
|
||
border: `1px solid ${r.success ? '#22c55e' : '#c2410c'}`,
|
||
borderRadius: 10, padding: '10px 14px',
|
||
}}>
|
||
<span style={{ fontSize: 18 }}>{r.success ? '✓' : '✗'}</span>
|
||
<div>
|
||
<p style={{ fontWeight: 600, fontSize: 14, color: r.success ? '#86efac' : '#fdba74', margin: 0 }}>
|
||
{r.printer_name}
|
||
</p>
|
||
{!r.success && (
|
||
<p style={{ fontSize: 12, color: '#fdba74', margin: 0 }}>Εκτυπωτής μη προσβάσιμος</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<button className="btn btn--secondary" style={{ width: '100%' }} onClick={onClose}>Κλείσιμο</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Split stepper modal ──────────────────────────────────────────────────────
|
||
|
||
function SplitModal({ item, onConfirm, onClose }) {
|
||
const [splitQty, setSplitQty] = useState(1)
|
||
const max = item.quantity - 1
|
||
|
||
return (
|
||
<div className="modal-overlay" onClick={onClose}>
|
||
<div className="modal-sheet" onClick={e => e.stopPropagation()}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title">Διαχωρισμός</h2>
|
||
<p style={{ textAlign: 'center', color: '#94a3b8', fontSize: 14, marginBottom: 4 }}>
|
||
{item.product?.name} ×{item.quantity}
|
||
</p>
|
||
<p style={{ textAlign: 'center', color: '#94a3b8', fontSize: 13, marginBottom: 16 }}>
|
||
Χώρισε σε πόσα;
|
||
</p>
|
||
|
||
<div className="modal-qty" style={{ marginBottom: 8 }}>
|
||
<button className="qty-btn" onClick={() => setSplitQty(q => Math.max(1, q - 1))}>−</button>
|
||
<span className="qty-value">{splitQty}</span>
|
||
<button className="qty-btn" onClick={() => setSplitQty(q => Math.min(max, q + 1))}>+</button>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'center', gap: 20, marginBottom: 20, fontSize: 13, color: '#94a3b8' }}>
|
||
<span>Νέα γραμμή: <strong style={{ color: '#f59e0b' }}>×{splitQty}</strong></span>
|
||
<span>Μένει: <strong style={{ color: '#64748b' }}>×{item.quantity - splitQty}</strong></span>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button className="btn btn--secondary" style={{ flex: 1 }} onClick={onClose}>Άκυρο</button>
|
||
<button className="btn btn--primary" style={{ flex: 1 }} onClick={() => onConfirm(splitQty)}>
|
||
Διαχωρισμός
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Item action modal (long-press) ──────────────────────────────────────────
|
||
|
||
function ItemActionModal({ target, onOrderAgain, onSplit, onClose }) {
|
||
const { items, singleStacked, multiSelect } = target
|
||
const label = multiSelect
|
||
? `${items.length} αντικείμενα επιλεγμένα`
|
||
: items[0]?.product?.name || `#${items[0]?.product_id}`
|
||
|
||
return (
|
||
<div className="modal-overlay" onClick={onClose}>
|
||
<div className="modal-sheet" onClick={e => e.stopPropagation()} style={{ gap: 0 }}>
|
||
<div className="modal-handle" />
|
||
<p style={{ textAlign: 'center', color: 'var(--muted)', fontSize: 13, margin: '0 0 16px' }}>{label}</p>
|
||
|
||
<button
|
||
onClick={onOrderAgain}
|
||
style={{
|
||
width: '100%', display: 'flex', alignItems: 'center', gap: 14,
|
||
padding: '16px 4px', background: 'none', border: 'none',
|
||
borderBottom: singleStacked && !multiSelect ? '1px solid var(--border)' : 'none',
|
||
cursor: 'pointer', textAlign: 'left',
|
||
}}
|
||
>
|
||
<span style={{
|
||
width: 38, height: 38, borderRadius: 10, flexShrink: 0,
|
||
background: 'rgba(245,158,11,0.15)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
}}>
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||
<path d="M1 4v6h6M23 20v-6h-6" stroke="#f59e0b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4-4.64 4.36A9 9 0 0 1 3.51 15" stroke="#f59e0b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||
</svg>
|
||
</span>
|
||
<div>
|
||
<div style={{ fontSize: 15, fontWeight: 600, color: '#f59e0b' }}>Παραγγελία ξανά</div>
|
||
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 1 }}>Προσθήκη στο νέο καλάθι</div>
|
||
</div>
|
||
<span style={{ marginLeft: 'auto', color: 'var(--muted)', fontSize: 18 }}>›</span>
|
||
</button>
|
||
|
||
{singleStacked && !multiSelect && (
|
||
<button
|
||
onClick={onSplit}
|
||
style={{
|
||
width: '100%', display: 'flex', alignItems: 'center', gap: 14,
|
||
padding: '16px 4px', background: 'none', border: 'none',
|
||
cursor: 'pointer', textAlign: 'left',
|
||
}}
|
||
>
|
||
<span style={{
|
||
width: 38, height: 38, borderRadius: 10, flexShrink: 0,
|
||
background: 'rgba(96,165,250,0.15)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
}}>
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||
<path d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5" stroke="#60a5fa" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||
</svg>
|
||
</span>
|
||
<div>
|
||
<div style={{ fontSize: 15, fontWeight: 600, color: '#60a5fa' }}>Διαχωρισμός</div>
|
||
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 1 }}>Χώρισμα σε δύο γραμμές</div>
|
||
</div>
|
||
<span style={{ marginLeft: 'auto', color: 'var(--muted)', fontSize: 18 }}>›</span>
|
||
</button>
|
||
)}
|
||
|
||
<button className="btn btn--secondary" style={{ width: '100%', marginTop: 12 }} onClick={onClose}>
|
||
Άκυρο
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── 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 (
|
||
<div className="modal-overlay modal-overlay--top" onClick={onClose}>
|
||
<div className="modal-sheet modal-sheet--top" onClick={e => e.stopPropagation()} style={{ gap: 0 }}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title" style={{ marginBottom: 16 }}>ACTIONS</h2>
|
||
{actions.map((a, i) => (
|
||
<button
|
||
key={i}
|
||
onClick={() => { a.onClick?.(); onClose() }}
|
||
disabled={!a.onClick}
|
||
style={{
|
||
width: '100%', display: 'flex', alignItems: 'center', gap: 16,
|
||
padding: '14px 0', background: 'none', border: 'none',
|
||
borderBottom: i < actions.length - 1 ? '1px solid var(--border)' : 'none',
|
||
cursor: a.onClick ? 'pointer' : 'not-allowed',
|
||
opacity: a.onClick ? 1 : 0.35, textAlign: 'left',
|
||
}}
|
||
>
|
||
<span style={{
|
||
width: 38, height: 38, borderRadius: 10, flexShrink: 0,
|
||
background: a.iconBg,
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
color: a.color,
|
||
}}>
|
||
<a.Icon width="20" height="20" />
|
||
</span>
|
||
<div>
|
||
<div style={{ fontSize: 15, fontWeight: 600, color: a.color }}>{a.label}</div>
|
||
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 1 }}>{a.sub}</div>
|
||
</div>
|
||
{a.onClick && <span style={{ marginLeft: 'auto', color: 'var(--muted)', fontSize: 18 }}>›</span>}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Table picker (for transfer / merge / move-items) ─────────────────────────
|
||
|
||
function TablePicker({ title, subtitle, tables, currentTableId, onSelect, onClose, loading }) {
|
||
return (
|
||
<div className="modal-overlay modal-overlay--top" onClick={onClose}>
|
||
<div className="modal-sheet modal-sheet--top" onClick={e => e.stopPropagation()}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title">{title}</h2>
|
||
{subtitle && <p style={{ textAlign: 'center', color: '#94a3b8', fontSize: 13, marginBottom: 4 }}>{subtitle}</p>}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, maxHeight: '60svh', overflowY: 'auto' }}>
|
||
{loading && (
|
||
<p style={{ textAlign: 'center', color: '#64748b', padding: 24 }}>Φόρτωση…</p>
|
||
)}
|
||
{!loading && tables.length === 0 && (
|
||
<p style={{ textAlign: 'center', color: '#64748b', padding: 24 }}>Δεν υπάρχουν διαθέσιμα τραπέζια</p>
|
||
)}
|
||
{!loading && tables.map(t => (
|
||
<button
|
||
key={t.id}
|
||
onClick={() => onSelect(t)}
|
||
style={{
|
||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||
padding: '14px 4px', background: 'none', border: 'none',
|
||
borderBottom: '1px solid var(--border)', cursor: 'pointer', textAlign: 'left',
|
||
}}
|
||
>
|
||
<div>
|
||
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>
|
||
{t.label || `T${t.number}`}
|
||
</div>
|
||
{t.orderStatus && (
|
||
<div style={{ fontSize: 12, color: '#f59e0b', marginTop: 2 }}>
|
||
{t.orderStatus === 'open' ? 'Ανοιχτό' : t.orderStatus === 'partially_paid' ? 'Μερικώς πληρωμένο' : t.orderStatus}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<span style={{ color: 'var(--muted)', fontSize: 18 }}>›</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<button className="btn btn--secondary" style={{ width: '100%', marginTop: 8 }} onClick={onClose}>Άκυρο</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── 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 (
|
||
<div className="modal-overlay modal-overlay--top" onClick={onClose}>
|
||
<div className="modal-sheet modal-sheet--top" onClick={e => e.stopPropagation()}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title">Ενδείξεις Τραπεζιού</h2>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, marginBottom: 20 }}>
|
||
{loading && (
|
||
<p style={{ textAlign: 'center', color: '#64748b', padding: 16 }}>Φόρτωση…</p>
|
||
)}
|
||
{!loading && flagDefs.length === 0 && (
|
||
<p style={{ textAlign: 'center', color: '#64748b', padding: 16 }}>Δεν υπάρχουν σημαίες</p>
|
||
)}
|
||
{!loading && flagDefs.map(f => {
|
||
const sel = selected.includes(f.id)
|
||
return (
|
||
<button
|
||
key={f.id}
|
||
onClick={() => toggle(f.id)}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 14,
|
||
padding: '14px 4px', background: 'none', border: 'none',
|
||
borderBottom: '1px solid var(--border)', cursor: 'pointer', textAlign: 'left',
|
||
opacity: sel ? 1 : 0.7,
|
||
}}
|
||
>
|
||
<div style={{
|
||
width: 36, height: 36, borderRadius: '50%', flexShrink: 0,
|
||
background: f.color + '33', border: `2px solid ${sel ? f.color : 'transparent'}`,
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18,
|
||
}}>
|
||
{f.emoji || '🏷️'}
|
||
</div>
|
||
<span style={{ flex: 1, fontSize: 15, fontWeight: sel ? 700 : 500, color: sel ? f.color : 'var(--text)' }}>
|
||
{f.name}
|
||
</span>
|
||
<span style={{ fontSize: 20, color: sel ? f.color : 'var(--muted)' }}>
|
||
{sel ? '✓' : '○'}
|
||
</span>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button className="btn btn--secondary" style={{ flex: 1 }} onClick={onClose}>Άκυρο</button>
|
||
<button className="btn btn--primary" style={{ flex: 1 }} onClick={() => onSave(selected)}>Αποθήκευση</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── 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 (
|
||
<div className="modal-overlay modal-overlay--top" onClick={onClose}>
|
||
<div className="modal-sheet modal-sheet--top" onClick={e => e.stopPropagation()}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title">Ανάθεση Σερβιτόρου</h2>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, marginBottom: 16, maxHeight: '60svh', overflowY: 'auto' }}>
|
||
{loading && (
|
||
<p style={{ textAlign: 'center', color: '#64748b', padding: 16 }}>Φόρτωση…</p>
|
||
)}
|
||
{!loading && available.length === 0 && (
|
||
<p style={{ textAlign: 'center', color: '#64748b', padding: 16 }}>Όλοι οι σερβιτόροι έχουν ήδη ανατεθεί</p>
|
||
)}
|
||
{!loading && available.map(w => (
|
||
<button
|
||
key={w.id}
|
||
onClick={() => assign(w.id)}
|
||
disabled={saving}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 14,
|
||
padding: '14px 4px', background: 'none', border: 'none',
|
||
borderBottom: '1px solid var(--border)', cursor: 'pointer', textAlign: 'left',
|
||
}}
|
||
>
|
||
<div style={{
|
||
width: 36, height: 36, borderRadius: '50%', flexShrink: 0,
|
||
background: 'var(--bg3)', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
fontSize: 16, fontWeight: 700, color: 'var(--text)',
|
||
}}>
|
||
{(w.nickname || w.username).charAt(0).toUpperCase()}
|
||
</div>
|
||
<span style={{ fontSize: 15, color: 'var(--text)' }}>{w.nickname || w.full_name || w.username}</span>
|
||
<span style={{ marginLeft: 'auto', color: 'var(--muted)', fontSize: 18 }}>›</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<button className="btn btn--secondary" style={{ width: '100%' }} onClick={onClose}>Άκυρο</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── 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 (
|
||
<div className="modal-overlay modal-overlay--top" onClick={onClose}>
|
||
<div className="modal-sheet modal-sheet--top" onClick={e => e.stopPropagation()}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title">Εκτύπωση Σύνοψης</h2>
|
||
<p style={{ textAlign: 'center', color: '#94a3b8', fontSize: 13, marginBottom: 8 }}>Επιλέξτε εκτυπωτή</p>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, marginBottom: 16 }}>
|
||
{printers.filter(p => p.reachable).map(p => (
|
||
<button
|
||
key={p.id}
|
||
onClick={() => print(p.id)}
|
||
disabled={printing}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 14,
|
||
padding: '14px 4px', background: 'none', border: 'none',
|
||
borderBottom: '1px solid var(--border)', cursor: 'pointer', textAlign: 'left',
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 20 }}>🖨️</span>
|
||
<span style={{ fontSize: 15, color: 'var(--text)' }}>{p.name}</span>
|
||
<span style={{ marginLeft: 'auto', color: 'var(--muted)', fontSize: 18 }}>›</span>
|
||
</button>
|
||
))}
|
||
{printers.filter(p => !p.reachable).map(p => (
|
||
<div key={p.id} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 4px', borderBottom: '1px solid var(--border)', opacity: 0.4 }}>
|
||
<span style={{ fontSize: 20 }}>🖨️</span>
|
||
<span style={{ fontSize: 15, color: 'var(--text)' }}>{p.name}</span>
|
||
<span style={{ marginLeft: 'auto', fontSize: 11, color: '#ef4444' }}>Offline</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<button className="btn btn--secondary" style={{ width: '100%' }} onClick={onClose}>Άκυρο</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── 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 (
|
||
<div className="modal-overlay" onClick={onClose}>
|
||
<div className="modal-sheet" onClick={e => e.stopPropagation()}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title">Επιβεβαίωση πληρωμής</h2>
|
||
<p style={{ color: '#94a3b8', textAlign: 'center', marginBottom: 4 }}>
|
||
{payAll ? 'Όλα τα αντικείμενα' : `${payIds.length} αντικείμενο${payIds.length !== 1 ? 'α' : ''}`}
|
||
</p>
|
||
<p style={{ color: '#f59e0b', textAlign: 'center', fontSize: 28, fontWeight: 700, marginBottom: 24 }}>
|
||
{fmtPrice(payTotal)}
|
||
</p>
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button className="btn btn--secondary" style={{ flex: 1 }} onClick={onClose}>
|
||
Άκυρο
|
||
</button>
|
||
<button className="btn btn--success" style={{ flex: 1 }} onClick={onConfirm}>
|
||
Πληρώθηκαν ✓
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── 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 [itemActionTarget, setItemActionTarget] = useState(null) // { items: [...], singleStacked: bool }
|
||
|
||
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 <div className="page page--centered"><p style={{ color: '#94a3b8' }}>Φόρτωση…</p></div>
|
||
|
||
return (
|
||
<div className="page">
|
||
{/* Top bar */}
|
||
<header className="top-bar">
|
||
<button className="icon-btn" onClick={() => navigate('/tables')}>←</button>
|
||
<span className="top-bar__title">{tableName}</span>
|
||
<button
|
||
className="icon-btn"
|
||
onClick={openActions}
|
||
style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 13, fontWeight: 700, color: 'var(--accent)', minWidth: 'unset', padding: '0 8px' }}
|
||
>
|
||
<span style={{ fontSize: 16 }}>⚡</span>
|
||
<span>ACTIONS</span>
|
||
</button>
|
||
</header>
|
||
|
||
{error && <p className="error-msg" style={{ margin: 12 }}>{error}</p>}
|
||
|
||
{/* Active flag cards */}
|
||
{currentFlagIds.length > 0 && flagDefs.length > 0 && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '10px 12px 0' }}>
|
||
{currentFlagIds.map(fid => {
|
||
const def = flagDefs.find(d => d.id === fid)
|
||
if (!def) return null
|
||
return (
|
||
<button
|
||
key={fid}
|
||
onClick={() => doSaveFlags(currentFlagIds.filter(id => id !== fid))}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 12,
|
||
padding: '10px 14px',
|
||
background: (def.color || '#6295F3') + '22',
|
||
border: `1px solid ${def.color || '#6295F3'}`,
|
||
borderRadius: 12, cursor: 'pointer', textAlign: 'left',
|
||
width: '100%',
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 20 }}>{def.emoji || '🏷️'}</span>
|
||
<span style={{ flex: 1, fontSize: 14, fontWeight: 600, color: def.color || '#6295F3' }}>{def.name}</span>
|
||
<span style={{ fontSize: 18, color: def.color || '#6295F3', opacity: 0.6 }}>✕</span>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{!order && (
|
||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||
<p style={{ color: '#94a3b8', marginBottom: 24 }}>Δεν υπάρχει ενεργή παραγγελία</p>
|
||
<button className="btn btn--primary btn--lg" onClick={openOrder}>
|
||
Άνοιγμα Παραγγελίας
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{order && (
|
||
<div
|
||
className="detail-body"
|
||
ref={scrollRef}
|
||
style={{ overflowY: 'auto' }}
|
||
>
|
||
{/* Unprinted items warning */}
|
||
{unprintedItems.length > 0 && (
|
||
<div style={{
|
||
margin: '10px 12px 0',
|
||
background: '#431407', border: '1px solid #c2410c',
|
||
borderRadius: 12, padding: '10px 14px',
|
||
display: 'flex', alignItems: 'center', gap: 10,
|
||
}}>
|
||
<span style={{ fontSize: 20 }}>⏳</span>
|
||
<div style={{ flex: 1 }}>
|
||
<p style={{ fontWeight: 700, fontSize: 13, color: '#fdba74', margin: 0 }}>
|
||
{unprintedItems.length} αντικείμενο{unprintedItems.length !== 1 ? 'α' : ''} δεν εκτυπώθηκε{unprintedItems.length !== 1 ? 'αν' : ''}
|
||
</p>
|
||
<p style={{ fontSize: 12, color: '#fdba74', margin: 0, opacity: 0.8 }}>
|
||
Δεν έχουν σταλεί στην κουζίνα/μπαρ
|
||
</p>
|
||
</div>
|
||
<button
|
||
className="btn btn--primary"
|
||
style={{ fontSize: 12, padding: '6px 12px', flexShrink: 0, opacity: retrying ? 0.7 : 1 }}
|
||
onClick={sendDraftToKitchen}
|
||
disabled={retrying}
|
||
>
|
||
{retrying ? '…' : 'Αποστολή'}
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Tabs ── */}
|
||
<div style={{
|
||
display: 'flex', gap: 0,
|
||
margin: '10px 12px 0',
|
||
background: 'var(--bg2)',
|
||
borderRadius: 10,
|
||
border: '1px solid var(--border)',
|
||
overflow: 'hidden',
|
||
}}>
|
||
<button
|
||
onClick={() => setActiveTab('active')}
|
||
style={{
|
||
flex: 1, padding: '9px 0', border: 'none', cursor: 'pointer',
|
||
fontWeight: 700, fontSize: 13,
|
||
background: activeTab === 'active' ? 'var(--accent)' : 'transparent',
|
||
color: activeTab === 'active' ? 'var(--accent-fg)' : 'var(--muted)',
|
||
transition: 'background 0.15s',
|
||
}}
|
||
>
|
||
Εκκρεμή {activeItems.length > 0 && (
|
||
<span style={{
|
||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||
background: activeTab === 'active' ? 'rgba(0,0,0,0.2)' : 'var(--bg3)',
|
||
borderRadius: 999, fontSize: 11, fontWeight: 800,
|
||
padding: '1px 7px', marginLeft: 4,
|
||
}}>{activeItems.length}</span>
|
||
)}
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('paid')}
|
||
style={{
|
||
flex: 1, padding: '9px 0', border: 'none', cursor: 'pointer',
|
||
fontWeight: 700, fontSize: 13,
|
||
background: activeTab === 'paid' ? '#14532d' : 'transparent',
|
||
color: activeTab === 'paid' ? '#86efac' : 'var(--muted)',
|
||
transition: 'background 0.15s',
|
||
}}
|
||
>
|
||
Πληρωμένα {paidItems.length > 0 && (
|
||
<span style={{
|
||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||
background: activeTab === 'paid' ? 'rgba(0,0,0,0.2)' : 'var(--bg3)',
|
||
borderRadius: 999, fontSize: 11, fontWeight: 800,
|
||
padding: '1px 7px', marginLeft: 4,
|
||
}}>{paidItems.length}</span>
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
{activeTab === 'active' && (
|
||
<>
|
||
<OrderSummary
|
||
order={{ ...order, items: activeItems }}
|
||
selectable={canInteract && !paying}
|
||
selectedIds={selectedIds}
|
||
onToggle={toggleItem}
|
||
onLongPressItem={(item) => {
|
||
// If multiple items are selected, order-again all selected items
|
||
if (selectedIds.length > 1) {
|
||
const items = activeItems.filter(i => selectedIds.includes(i.id))
|
||
setItemActionTarget({ items, singleStacked: false, multiSelect: true })
|
||
} else {
|
||
setItemActionTarget({ items: [item], singleStacked: item.quantity > 1, multiSelect: false })
|
||
}
|
||
}}
|
||
/>
|
||
|
||
{/* Floating controls row — only visible when items are selected */}
|
||
{canInteract && activeItems.length > 0 && selectedIds.length > 0 && (
|
||
<div style={{
|
||
display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 8,
|
||
padding: '10px 12px 24px',
|
||
}}>
|
||
{/* Clear selection */}
|
||
<button
|
||
onClick={() => setSelectedIds([])}
|
||
style={{
|
||
display: 'inline-flex', alignItems: 'center',
|
||
height: 36, padding: '0 16px',
|
||
borderRadius: 999,
|
||
background: 'rgba(239,68,68,0.18)', color: '#fca5a5',
|
||
border: 'none', cursor: 'pointer',
|
||
fontSize: 13, fontWeight: 600,
|
||
}}
|
||
>
|
||
καθ. επιλ.
|
||
</button>
|
||
|
||
{/* Select all — hidden once everything is already selected */}
|
||
{!allActiveSelected && (
|
||
<button
|
||
onClick={selectAll}
|
||
style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
height: 36, padding: '0 16px',
|
||
borderRadius: 999,
|
||
background: 'rgba(34,197,94,0.18)', color: '#86efac',
|
||
border: 'none', cursor: 'pointer',
|
||
fontSize: 13, fontWeight: 600,
|
||
}}
|
||
>
|
||
όλα
|
||
<span style={{
|
||
background: 'rgba(34,197,94,0.35)', color: '#86efac',
|
||
borderRadius: 999, fontSize: 11, fontWeight: 700,
|
||
padding: '1px 6px',
|
||
}}>{activeItems.length}</span>
|
||
</button>
|
||
)}
|
||
|
||
{/* Transfer items */}
|
||
<button
|
||
onClick={() => {
|
||
setAllTables([])
|
||
setAllOrders([])
|
||
loadActionData()
|
||
setActionsMode('move_items')
|
||
}}
|
||
style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
height: 36, padding: '0 16px',
|
||
borderRadius: 999,
|
||
background: 'rgba(96,165,250,0.18)', color: '#93c5fd',
|
||
border: 'none', cursor: 'pointer',
|
||
fontSize: 13, fontWeight: 600,
|
||
}}
|
||
>
|
||
μεταφορά
|
||
<span style={{
|
||
background: 'rgba(96,165,250,0.35)', color: '#93c5fd',
|
||
borderRadius: 999, fontSize: 11, fontWeight: 700,
|
||
padding: '1px 6px',
|
||
}}>{selectedIds.length}</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{activeTab === 'paid' && (
|
||
<div style={{ padding: '8px 0' }}>
|
||
{paidItems.length === 0 ? (
|
||
<p style={{ color: '#64748b', textAlign: 'center', padding: '24px 0', fontSize: 14 }}>
|
||
Δεν υπάρχουν πληρωμένα αντικείμενα
|
||
</p>
|
||
) : (
|
||
<OrderSummary
|
||
order={{ ...order, items: paidItems }}
|
||
selectable={false}
|
||
selectedIds={[]}
|
||
onToggle={() => {}}
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
)}
|
||
|
||
{/* Action bar — sticky at bottom, only when there's an active order on the active tab */}
|
||
{order && canInteract && activeTab === 'active' && (
|
||
<div className="action-bar">
|
||
<button
|
||
className="btn btn--accent"
|
||
onClick={() => navigate(`/tables/${tableId}/add`)}
|
||
style={{ borderRadius: 999, flex: 1 }}
|
||
>
|
||
<span style={{ marginRight: 6 }}>+</span>ADD
|
||
</button>
|
||
|
||
<button
|
||
className="btn btn--success"
|
||
onClick={() => setConfirmPay(true)}
|
||
disabled={paying}
|
||
style={{ borderRadius: 999, flex: 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
||
>
|
||
{paying ? '…' : (
|
||
<>
|
||
<span>€ PAY</span>
|
||
{selectedIds.length > 0 && (
|
||
<span style={{
|
||
background: 'rgba(0,0,0,0.25)', color: 'white',
|
||
borderRadius: 999, fontSize: 11, fontWeight: 700,
|
||
padding: '1px 7px',
|
||
}}>{selectedIds.length}</span>
|
||
)}
|
||
</>
|
||
)}
|
||
</button>
|
||
|
||
<button
|
||
className="btn btn--danger"
|
||
onClick={() => setConfirmClose(true)}
|
||
disabled={!allPaid}
|
||
style={{ borderRadius: 999, flex: 1 }}
|
||
>
|
||
<span style={{ marginRight: 6 }}>✕</span>CLOSE
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Item action modal (long-press) */}
|
||
{itemActionTarget && (
|
||
<ItemActionModal
|
||
target={itemActionTarget}
|
||
onOrderAgain={() => {
|
||
const items = itemActionTarget.items
|
||
sessionStorage.setItem('orderAgainItems', JSON.stringify(
|
||
items.map(it => ({
|
||
product_id: it.product_id,
|
||
quantity: it.quantity,
|
||
selected_options: (() => { try { return JSON.parse(it.selected_options || '[]') } catch { return [] } })(),
|
||
removed_ingredients: (() => { try { return JSON.parse(it.removed_ingredients || '[]') } catch { return [] } })(),
|
||
notes: it.notes || '',
|
||
}))
|
||
))
|
||
setItemActionTarget(null)
|
||
navigate(`/tables/${tableId}/add`)
|
||
}}
|
||
onSplit={() => {
|
||
setSplitItem(itemActionTarget.items[0])
|
||
setItemActionTarget(null)
|
||
}}
|
||
onClose={() => setItemActionTarget(null)}
|
||
/>
|
||
)}
|
||
|
||
{/* Split stepper modal */}
|
||
{splitItem && (
|
||
<SplitModal
|
||
item={splitItem}
|
||
onConfirm={doSplit}
|
||
onClose={() => setSplitItem(null)}
|
||
/>
|
||
)}
|
||
|
||
{/* Pay confirmation */}
|
||
{confirmPay && (
|
||
<PayConfirmModal
|
||
payAll={selectedIds.length === 0}
|
||
payIds={selectedIds.length === 0 ? activeItems.map(i => i.id) : selectedIds}
|
||
activeItems={activeItems}
|
||
onConfirm={paySelected}
|
||
onClose={() => setConfirmPay(false)}
|
||
/>
|
||
)}
|
||
|
||
{/* Close confirmation */}
|
||
{confirmClose && (
|
||
<div className="modal-overlay" onClick={() => setConfirmClose(false)}>
|
||
<div className="modal-sheet" onClick={e => e.stopPropagation()}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title">Κλείσιμο παραγγελίας;</h2>
|
||
<p style={{ color: '#94a3b8', textAlign: 'center', marginBottom: 24 }}>
|
||
Αυτή η ενέργεια δεν αναιρείται.
|
||
</p>
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button className="btn btn--secondary" style={{ flex: 1 }} onClick={() => setConfirmClose(false)}>
|
||
Άκυρο
|
||
</button>
|
||
<button className="btn btn--danger" style={{ flex: 1 }} onClick={closeOrder}>
|
||
CLOSE
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Print retry results */}
|
||
{printResults && (
|
||
<PrintResultsModal results={printResults} onClose={() => setPrintResults(null)} />
|
||
)}
|
||
|
||
{/* Actions top sheet */}
|
||
{showActions && actionsMode === null && (
|
||
<ActionsSheet
|
||
order={order}
|
||
tableId={tableId}
|
||
onClose={() => 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 && (
|
||
<TablePicker
|
||
title="Μεταφορά Τραπεζιού"
|
||
subtitle="Επιλέξτε κενό τραπέζι"
|
||
tables={transferTargets}
|
||
currentTableId={Number(tableId)}
|
||
onSelect={t => setPendingTable({ table: t, mode: 'transfer' })}
|
||
onClose={() => setActionsMode(null)}
|
||
loading={actionDataLoading}
|
||
/>
|
||
)}
|
||
|
||
{/* Merge picker */}
|
||
{actionsMode === 'merge' && !pendingTable && (
|
||
<TablePicker
|
||
title="Συγχώνευση Τραπεζιού"
|
||
subtitle="Τα αντικείμενα μεταφέρονται στο επιλεγμένο τραπέζι"
|
||
tables={mergeTargets}
|
||
currentTableId={Number(tableId)}
|
||
onSelect={t => setPendingTable({ table: t, mode: 'merge' })}
|
||
onClose={() => setActionsMode(null)}
|
||
loading={actionDataLoading}
|
||
/>
|
||
)}
|
||
|
||
{/* Move items picker */}
|
||
{actionsMode === 'move_items' && !pendingTable && (
|
||
<TablePicker
|
||
title="Μεταφορά Αντικειμένων"
|
||
subtitle={`Μεταφορά ${selectedIds.length} αντικειμένου${selectedIds.length !== 1 ? 'ων' : ''} σε τραπέζι`}
|
||
tables={moveItemsTargets}
|
||
currentTableId={Number(tableId)}
|
||
onSelect={t => setPendingTable({ table: t, mode: 'move_items' })}
|
||
onClose={() => setActionsMode(null)}
|
||
loading={actionDataLoading}
|
||
/>
|
||
)}
|
||
|
||
{/* Transfer / Merge / Move-items confirmation */}
|
||
{pendingTable && (
|
||
<div className="modal-overlay modal-overlay--top" onClick={() => setPendingTable(null)}>
|
||
<div className="modal-sheet modal-sheet--top" onClick={e => e.stopPropagation()}>
|
||
<div className="modal-handle" />
|
||
<h2 className="modal-title">
|
||
{pendingTable.mode === 'transfer' ? 'Επιβεβαίωση Μεταφοράς'
|
||
: pendingTable.mode === 'merge' ? 'Επιβεβαίωση Συγχώνευσης'
|
||
: 'Επιβεβαίωση Μεταφοράς Αντικειμένων'}
|
||
</h2>
|
||
<p style={{ textAlign: 'center', color: '#94a3b8', fontSize: 14, marginBottom: 4 }}>
|
||
{pendingTable.mode === 'transfer' ? 'Μεταφορά παραγγελίας στο τραπέζι'
|
||
: pendingTable.mode === 'merge' ? 'Συγχώνευση παραγγελίας με το τραπέζι'
|
||
: `Μεταφορά ${selectedIds.length} αντικειμένου${selectedIds.length !== 1 ? 'ων' : ''} στο τραπέζι`}
|
||
</p>
|
||
<p style={{ textAlign: 'center', fontSize: 22, fontWeight: 700, color: 'var(--accent)', marginBottom: 20 }}>
|
||
{pendingTable.table.label || `T${pendingTable.table.number}`}
|
||
</p>
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button className="btn btn--secondary" style={{ flex: 1 }} onClick={() => setPendingTable(null)}>
|
||
Άκυρο
|
||
</button>
|
||
<button
|
||
className="btn btn--primary"
|
||
style={{ flex: 1 }}
|
||
onClick={() => {
|
||
if (pendingTable.mode === 'transfer') doTransfer(pendingTable.table)
|
||
else if (pendingTable.mode === 'merge') doMerge(pendingTable.table)
|
||
else doMoveItems(pendingTable.table)
|
||
}}
|
||
>
|
||
{pendingTable.mode === 'transfer' ? 'Μεταφορά ✓'
|
||
: pendingTable.mode === 'merge' ? 'Συγχώνευση ✓'
|
||
: 'Μεταφορά ✓'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Flag picker */}
|
||
{actionsMode === 'flags' && (
|
||
<FlagPicker
|
||
tableId={tableId}
|
||
currentFlagIds={currentFlagIds}
|
||
flagDefs={flagDefs}
|
||
onSave={doSaveFlags}
|
||
onClose={() => setActionsMode(null)}
|
||
loading={actionDataLoading}
|
||
/>
|
||
)}
|
||
|
||
{/* Assign waiter */}
|
||
{actionsMode === 'assign_waiter' && order && (
|
||
<AssignWaiterPicker
|
||
orderId={order.id}
|
||
currentWaiterIds={order.waiters.map(w => w.waiter_id)}
|
||
waiters={allWaiters}
|
||
onAssigned={load}
|
||
onClose={() => setActionsMode(null)}
|
||
loading={actionDataLoading}
|
||
/>
|
||
)}
|
||
|
||
{/* Print synopsis */}
|
||
{actionsMode === 'print_synopsis' && order && (
|
||
<PrintSynopsisPicker
|
||
orderId={order.id}
|
||
onClose={() => setActionsMode(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|