import { useState, useEffect, useContext } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import toast from 'react-hot-toast' import client from '../api/client' import Badge from '../ui/Badge' import { ConfirmModal } from '../ui/Modal' import { LicenseContext } from '../layouts/AppLayout' import ShiftDetailModal from './reports/shared/ShiftDetailModal' // ─── Helpers ────────────────────────────────────────────────────────────────── function fmtTime(iso) { if (!iso) return '—' return new Date(iso).toLocaleTimeString('el-GR', { hour: '2-digit', minute: '2-digit' }) } function fmtDuration(isoStart) { if (!isoStart) return '—' const mins = Math.floor((Date.now() - new Date(isoStart).getTime()) / 60000) if (mins < 60) return `${mins}λ` const h = Math.floor(mins / 60) const m = mins % 60 return m === 0 ? `${h}ω` : `${h}ω ${m}λ` } function fmtEuro(n) { return '€' + parseFloat(n || 0).toFixed(2) } function orderTotal(items = []) { return items .filter(i => i.status !== 'cancelled') .reduce((s, i) => s + i.unit_price * i.quantity, 0) } function avatarColor(name) { const palette = ['#3758c9', '#7a44c9', '#2f9e5e', '#d94b26', '#8a6d2b', '#0d7a8a', '#c93775'] let h = 0 for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0 return palette[h % palette.length] } function Avatar({ name, avatarUrl, size = 36 }) { if (avatarUrl) { return ( {name} ) } const parts = (name || '?').trim().split(' ') const initials = (parts[0][0] + (parts[1]?.[0] || '')).toUpperCase() return (
{initials}
) } // ─── Table color tokens ──────────────────────────────────────────────────────── const TABLE_COLORS = { open: { bg: '#eef7f0', border: '#d7ecdc', dot: '#2f9e5e', text: '#1f7042', label: 'Ανοιχτό' }, partially_paid: { bg: '#f4eefb', border: '#e3d4f3', dot: '#7a44c9', text: '#57309a', label: 'Μερική πληρ.' }, free: { bg: '#f4f4f2', border: '#dfe2e6', dot: '#8a9099', text: '#5a6169', label: 'Ελεύθερο' }, } // ─── Business Day / Shift modals ─────────────────────────────────────────────── function StartShiftModal({ waiters, onClose, onStart }) { const [waiterId, setWaiterId] = useState('') const [cash, setCash] = useState('') const [busy, setBusy] = useState(false) async function submit() { if (!waiterId) { toast.error('Επιλέξτε σερβιτόρο'); return } setBusy(true) try { await onStart(Number(waiterId), cash ? parseFloat(cash) : null) onClose() } catch (e) { toast.error(e.response?.data?.detail || 'Σφάλμα εκκίνησης βάρδιας') } finally { setBusy(false) } } return (
{ if (e.target === e.currentTarget) onClose() }}>

Έναρξη Βάρδιας

setCash(e.target.value)} className="h-10 w-full rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-800 focus:outline-none" />
) } function CloseConfirmModal({ details, onClose, onConfirm, busy }) { if (!details.partially_paid) { return (
{ if (e.target === e.currentTarget) onClose() }}>

Κλείσιμο Ημέρας

{details.open_orders} {details.open_orders === 1 ? 'τραπέζι είναι ακόμα ανοιχτό' : 'τραπέζια είναι ακόμα ανοιχτά'}

Κανένα δεν έχει εκκρεμείς χρεώσεις. Θέλετε να κλείσουν όλα και να κλείσει η ημέρα;

) } return (
{ if (e.target === e.currentTarget) onClose() }}>
!

Εκκρεμείς Πληρωμές

{details.open_orders} {details.open_orders === 1 ? 'ανοιχτό τραπέζι' : 'ανοιχτά τραπέζια'}, από τα οποία {details.partially_paid} έχ{details.partially_paid === 1 ? 'ει' : 'ουν'} εκκρεμείς πληρωμές.

Αν κλείσετε αναγκαστικά, τα απλήρωτα ποσά θα χαθούν και δεν θα καταγραφούν στις αναφορές.

Επιλέξτε Ακύρωση για να χειριστείτε χειροκίνητα τα εκκρεμή τραπέζια πριν κλείσετε την ημέρα.
) } // ─── Order quick-view modal ──────────────────────────────────────────────────── function OrderQuickModal({ orderId, tableName, onClose, onOpenFull }) { const qc = useQueryClient() const { data: order, isLoading } = useQuery({ queryKey: ['order', orderId], queryFn: () => client.get(`/api/orders/${orderId}`).then(r => r.data), enabled: !!orderId, }) const { data: waiters = [] } = useQuery({ queryKey: ['waiters'], queryFn: () => client.get('/api/waiters/').then(r => r.data), staleTime: 60_000, }) const { data: printers = [] } = useQuery({ queryKey: ['printers'], queryFn: () => client.get('/api/system/printers').then(r => r.data), staleTime: 60_000, }) const [confirmAction, setConfirmAction] = useState(null) const [printerId, setPrinterId] = useState('') const waiterMap = Object.fromEntries(waiters.map(w => [w.id, w.nickname || w.full_name || w.username])) const invalidate = () => { qc.invalidateQueries({ queryKey: ['order', orderId] }) qc.invalidateQueries({ queryKey: ['orders-active'] }) } const payItems = useMutation({ mutationFn: (item_ids) => client.post(`/api/orders/${orderId}/pay`, { item_ids }), onSuccess: () => { toast.success('Πληρώθηκε'); invalidate() }, onError: () => toast.error('Σφάλμα πληρωμής'), }) const cancelItem = useMutation({ mutationFn: (itemId) => client.delete(`/api/orders/${orderId}/items/${itemId}`), onSuccess: () => { toast.success('Αντικείμενο ακυρώθηκε'); invalidate() }, onError: () => toast.error('Σφάλμα ακύρωσης'), }) const cancelOrder = useMutation({ mutationFn: () => client.delete(`/api/orders/${orderId}`), onSuccess: () => { toast.success('Παραγγελία ακυρώθηκε'); invalidate(); onClose() }, onError: () => toast.error('Σφάλμα ακύρωσης παραγγελίας'), }) const closeOrder = useMutation({ mutationFn: () => client.post(`/api/orders/${orderId}/close`), onSuccess: () => { toast.success('Παραγγελία έκλεισε'); invalidate(); onClose() }, onError: () => toast.error('Σφάλμα κλεισίματος'), }) const printOrder = useMutation({ mutationFn: (pid) => client.post(`/api/orders/${orderId}/print`, { printer_id: pid }), onSuccess: () => toast.success('Αποστολή στον εκτυπωτή…'), onError: () => toast.error('Σφάλμα εκτύπωσης'), }) function handleConfirm() { if (!confirmAction) return if (confirmAction.type === 'cancelItem') cancelItem.mutate(confirmAction.payload) if (confirmAction.type === 'cancelOrder') cancelOrder.mutate() if (confirmAction.type === 'closeOrder') closeOrder.mutate() setConfirmAction(null) } const total = order ? orderTotal(order.items) : 0 const activeItems = order ? order.items.filter(i => i.status === 'active') : [] const isOpen = order ? ['open', 'partially_paid'].includes(order.status) : false return (
{ if (e.target === e.currentTarget) onClose() }} >
{/* Modal header */}
Τραπέζι {tableName}
{order && (
Παραγγελία #{order.id} · από {fmtTime(order.opened_at)} · {fmtDuration(order.opened_at)}
)}
{order && ( {fmtEuro(total)} )} {order && ( )}
{/* Scrollable body */}
{isLoading && (
Φόρτωση…
)} {order && ( <> {/* Waiters row */} {order.waiters.length > 0 && (
Προσωπικό
{order.waiters.map(w => ( {waiterMap[w.waiter_id] || `#${w.waiter_id}`} ))}
)} {/* Items list */}
{order.items.length === 0 && (
Κανένα αντικείμενο.
)} {order.items.map(item => (
{item.product?.name ?? `#${item.product_id}`} ×{item.quantity}
{item.notes &&
{item.notes}
} {item.paid_by && (
Πληρώθηκε
)}
{fmtEuro(item.unit_price * item.quantity)} {isOpen && item.status === 'active' && ( <> )}
))}
)}
{/* Footer actions */} {order && (
{isOpen && activeItems.length > 0 && ( )} {isOpen && ( <> )} {printers.length > 0 && (
)}
)}
{confirmAction && ( setConfirmAction(null)} /> )}
) } // ─── KPI Card ───────────────────────────────────────────────────────────────── function KpiCard({ label, value, sub, accent = '#3758c9', pct, private: isPrivate }) { return (
{label}
{value}
{sub &&
{sub}
} {pct != null && (
)}
) } // ─── Mini table chip ────────────────────────────────────────────────────────── function TableChip({ name, status, amount, onClick }) { const c = TABLE_COLORS[status] || TABLE_COLORS.free return ( ) } // ─── Shifts card ────────────────────────────────────────────────────────────── // ─── End shift confirmation modal ───────────────────────────────────────────── function EndShiftConfirmModal({ shift, onClose, onConfirm, busy }) { const [notes, setNotes] = useState('') return (
{ if (e.target === e.currentTarget) onClose() }}>
Τέλος βάρδιας;
{shift.waiter_name}
Έναρξη{fmtTime(shift.started_at)}
Διάρκεια{fmtDuration(shift.started_at)}
Είσπραξη{fmtEuro(shift.total_collected)}