import { useState, useEffect, useRef, useCallback } from 'react' import { useQuery, useMutation } from '@tanstack/react-query' import toast from 'react-hot-toast' import client from '../api/client' import StatusBadge from '../components/StatusBadge' import { DateTimeInput } from '../components/DateInput' function today() { return new Date().toISOString().slice(0, 10) } function todayStart() { return today() + 'T00:00' } function todayEnd() { return today() + 'T23:59' } function fmtDt(dt) { if (!dt) return '—' return new Date(dt).toLocaleString('el-GR', { dateStyle: 'short', timeStyle: 'short' }) } function csvDownload(rows, filename) { const header = Object.keys(rows[0]).join(',') const body = rows.map(r => Object.values(r).join(',')).join('\n') const blob = new Blob([header + '\n' + body], { type: 'text/csv' }) const a = document.createElement('a') a.href = URL.createObjectURL(blob) a.download = filename a.click() } // ── Shared label class for consistent toolbar height ───────────────────────── // All toolbar controls use h-10 so they align with date inputs const CTRL = 'h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-primary-500' const SELECT = CTRL + ' pr-8 appearance-none bg-[url(\'data:image/svg+xml;utf8,\')] bg-[length:1.25rem] bg-[right_0.5rem_center] bg-no-repeat' const BTN_SEC = 'h-10 px-4 rounded-lg border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors whitespace-nowrap' const BTN_PRI = 'h-10 px-4 rounded-lg bg-primary-600 text-white text-sm font-semibold hover:bg-primary-700 transition-colors whitespace-nowrap' // ── Shared modal close hook (ESC + click-outside) ──────────────────────────── function useModalClose(onClose) { const overlayRef = useRef(null) useEffect(() => { function onKey(e) { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) }, [onClose]) const onOverlayClick = useCallback((e) => { if (e.target === overlayRef.current) onClose() }, [onClose]) return { overlayRef, onOverlayClick } } // ── Print Modal (waiter / printer reports) ──────────────────────────────────── function PrintModal({ title, onClose, onPrint, printers, defaultFrom, defaultTo }) { const [mode, setMode] = useState('simple') const [fromDt, setFromDt] = useState(defaultFrom || todayStart()) const [toDt, setToDt] = useState(defaultTo || todayEnd()) const [printerId, setPrinterId] = useState(printers[0]?.id ?? '') const { overlayRef, onOverlayClick } = useModalClose(onClose) function submit() { if (!printerId) { toast.error('Επιλέξτε εκτυπωτή'); return } onPrint({ mode, fromDt, toDt, printerId: Number(printerId) }) onClose() } return (

{title}

Τύπος εκτύπωσης

setFromDt(e.target.value)} />
setToDt(e.target.value)} />
) } // ── Printer select modal (single order) ─────────────────────────────────────── function PrintOrderModal({ onClose, onPrint, printers }) { const [printerId, setPrinterId] = useState(printers[0]?.id ?? '') const { overlayRef, onOverlayClick } = useModalClose(onClose) function submit() { if (!printerId) { toast.error('Επιλέξτε εκτυπωτή'); return } onPrint(Number(printerId)); onClose() } return (

Εκτύπωση παραγγελίας

) } // ── No-printers fallback modal ──────────────────────────────────────────────── function NoPrintersModal({ onClose }) { const { overlayRef, onOverlayClick } = useModalClose(onClose) return (

Δεν βρέθηκαν ενεργοί εκτυπωτές.

) } // ── Printer Details Modal ───────────────────────────────────────────────────── function PrinterDetailsModal({ printerRow, tableMap, onClose }) { const orderData = printerRow?.order_data ?? [] const { overlayRef, onOverlayClick } = useModalClose(onClose) return (

Εκτυπωτής: {printerRow?.printer_name}

Εργασίες: {printerRow?.print_jobs} Παραγγελίες: {printerRow?.orders} Αντικείμενα: {printerRow?.items} Σύνολο: €{printerRow?.total?.toFixed(2)}
{orderData.length === 0 && (

Δεν υπάρχουν αναλυτικά δεδομένα.
Χρησιμοποιήστε εκτύπωση Αναλυτική για πλήρη λεπτομέρεια.

)} {orderData.length > 0 && (
{orderData.map((od, i) => (
{od.time} — {tableMap[od.table_id] ?? od.table} €{od.total.toFixed(2)}
{od.items?.length > 0 && (
    {od.items.map((item, j) => (
  • {item.quantity} × {item.name}
  • ))}
)}
))}
)}
) } // ── Payers Modal (multi-waiter payment breakdown) ──────────────────────────── function PayersModal({ order, waiterMap, onClose }) { const { overlayRef, onOverlayClick } = useModalClose(onClose) const paidItems = order.items.filter(i => i.status === 'paid' && i.paid_by) // Group items by (waiter_id, paid_at rounded to minute) to create payment groups const groups = {} for (const item of paidItems) { const key = `${item.paid_by}__${item.paid_at}` if (!groups[key]) { groups[key] = { waiter_id: item.paid_by, paid_at: item.paid_at, payment_method: item.payment_method, items: [] } } groups[key].items.push(item) } const paymentGroups = Object.values(groups).sort((a, b) => (a.paid_at || '') < (b.paid_at || '') ? -1 : 1) const pmLabel = (m) => m === 'card' ? 'Κάρτα' : m === 'cash' ? 'Μετρητά' : m === 'other' ? 'Άλλο' : '—' return (

Πληρωμές — Παραγγελία #{order.id}

{paymentGroups.map((g, i) => { const groupTotal = g.items.reduce((s, it) => s + it.unit_price * it.quantity, 0) const waiterName = waiterMap[g.waiter_id] || `#${g.waiter_id}` return (
{fmtDt(g.paid_at)} · {waiterName} {g.payment_method && ( {pmLabel(g.payment_method)} )}
€{groupTotal.toFixed(2)}
    {g.items.map(it => (
  • {it.product?.name ?? `#${it.product_id}`}{it.quantity > 1 ? ` ×${it.quantity}` : ''} €{(it.unit_price * it.quantity).toFixed(2)}
  • ))}
) })}
) } // ── Order Details Modal ─────────────────────────────────────────────────────── function OrderDetailsModal({ order, tableMap, waiterMap, onClose, printers, onPrint }) { const [showPrint, setShowPrint] = useState(false) const { overlayRef, onOverlayClick } = useModalClose(onClose) if (!order) return null const activeItems = order.items.filter(i => i.status !== 'cancelled') const total = activeItems.reduce((s, i) => s + i.unit_price * i.quantity, 0) // Per-waiter subtotals for paid items const waiterTotals = {} for (const item of activeItems.filter(i => i.status === 'paid' && i.paid_by)) { const wid = item.paid_by if (!waiterTotals[wid]) waiterTotals[wid] = 0 waiterTotals[wid] += item.unit_price * item.quantity } const waiterTotalEntries = Object.entries(waiterTotals) const pmLabel = (m) => m === 'card' ? 'Κάρτα' : m === 'cash' ? 'Μετρητά' : m === 'other' ? 'Άλλο' : '—' return (

Παραγγελία #{order.id}

Τραπέζι: {tableMap[order.table_id] || `#${order.table_id}`} Ανοίχτηκε: {fmtDt(order.opened_at)} Έκλεισε: {fmtDt(order.closed_at)} {order.notes && "{order.notes}"}
{order.items.map(item => ( ))} {waiterTotalEntries.length > 1 && waiterTotalEntries.map(([wid, wTotal]) => ( ))}
Προϊόν Ποσ. Τιμή/τμχ Σύνολο Κατ. Πληρώθηκε Τύπος Σερβιτόρος
{item.product?.name ?? `#${item.product_id}`} {item.quantity} €{item.unit_price.toFixed(2)} €{(item.unit_price * item.quantity).toFixed(2)} {item.paid_at ? fmtDt(item.paid_at) : '—'} {item.payment_method ? pmLabel(item.payment_method) : '—'} {item.paid_by ? (waiterMap[item.paid_by] || `#${item.paid_by}`) : '—'}
Σύνολο — {waiterMap[wid] || `#${wid}`} €{wTotal.toFixed(2)}
Σύνολο €{total.toFixed(2)}
{printers.length > 0 && ( )}
{showPrint && ( setShowPrint(false)} onPrint={(printerId) => { onPrint(order.id, printerId); setShowPrint(false) }} /> )}
) } // ── Main Page ───────────────────────────────────────────────────────────────── export default function ReportsPage() { const [tab, setTab] = useState('shift') const [historyFilters, setHistoryFilters] = useState({ from: todayStart(), to: todayEnd(), status: '', table_id: '', hideEmpty: true }) const TABS = [ ['shift', 'Σύνοψη Πληρωμών Βάρδιας'], ['shift-orders', 'Σύνοψη Παραγγελιών Βάρδιας'], ['printers', 'Σύνοψη εκτυπωτών'], ['history', 'Ιστορικό παραγγελιών'], ] return (

Αναφορές

{TABS.map(([key, label]) => ( ))}
{tab === 'shift' && } {tab === 'shift-orders' && } {tab === 'printers' && } {tab === 'history' && }
) } // ── Shift / Waiter Totals Tab ───────────────────────────────────────────────── function WaiterShiftDetailsModal({ row, tableMap, onClose }) { const { overlayRef, onOverlayClick } = useModalClose(onClose) return (

Σερβιτόρος: {row.waiter_name}

Παραγγελίες: {row.orders} Αντικείμενα: {row.items} Σύνολο: €{row.total.toFixed(2)}
{(row.order_data ?? []).length === 0 && (

Δεν βρέθηκαν παραγγελίες.

)} {(row.order_data ?? []).length > 0 && ( {row.order_data.map(od => ( ))}
# Τραπέζι Άνοιγμα Κλείσιμο Σύνολο
{od.id} {od.table} {od.time_open} {od.time_close || '—'} €{od.total.toFixed(2)}
)}
) } function ShiftTab({ endpoint, title }) { const [fromDt, setFromDt] = useState(todayStart()) const [toDt, setToDt] = useState(todayEnd()) const [printTarget, setPrintTarget] = useState(null) // waiter row const [detailTarget, setDetailTarget] = useState(null) // waiter row const { data, isLoading, refetch } = useQuery({ queryKey: ['report-shift', endpoint, fromDt, toDt], queryFn: () => client.get(`${endpoint}?from=${encodeURIComponent(fromDt)}&to=${encodeURIComponent(toDt)}`).then(r => r.data), }) const { data: printers = [] } = useQuery({ queryKey: ['printers'], queryFn: () => client.get('/api/system/printers').then(r => r.data), staleTime: 60_000, }) const { data: tables = [] } = useQuery({ queryKey: ['tables'], queryFn: () => client.get('/api/tables/').then(r => r.data), staleTime: 60_000, }) const tableMap = Object.fromEntries(tables.map(t => [t.id, t.label || `T${t.number}`])) const printMutation = useMutation({ mutationFn: (body) => client.post('/api/reports/print/waiter', body), onSuccess: () => toast.success('Αποστολή στον εκτυπωτή…'), onError: () => toast.error('Σφάλμα εκτύπωσης'), }) // New API returns { waiters: [{waiter_id, waiter_name, orders, items, total, order_data}] } const rows = data?.waiters ?? [] const grandTotal = rows.reduce((s, r) => s + r.total, 0) const grandOrders = rows.reduce((s, r) => s + r.orders, 0) const grandItems = rows.reduce((s, r) => s + r.items, 0) function handlePrint({ mode, fromDt: fd, toDt: td, printerId }) { printMutation.mutate({ waiter_name: printTarget.waiter_name, printer_id: printerId, mode, from_dt: fd, to_dt: td }) } const csvRows = rows.map(r => ({ Σερβιτόρος: r.waiter_name, Παραγγελίες: r.orders, Αντικείμενα: r.items, 'Σύνολο (€)': r.total.toFixed(2), })) return (
{/* Toolbar */}
setFromDt(e.target.value)} />
setToDt(e.target.value)} />
{rows.length > 0 && ( )}
{isLoading &&

Φόρτωση…

} {!isLoading && rows.length === 0 && (

Δεν υπάρχουν δεδομένα για αυτή την ημερομηνία.

)} {rows.length > 0 && (
{rows.map((r, i) => ( ))}
Σερβιτόρος Παραγγελίες Αντικείμενα Σύνολο (€)
{r.waiter_name} {r.orders} {r.items} €{r.total.toFixed(2)}
Σύνολο {grandOrders} {grandItems} €{grandTotal.toFixed(2)}
)} {printTarget && printers.length > 0 && ( setPrintTarget(null)} onPrint={handlePrint} /> )} {printTarget && printers.length === 0 && setPrintTarget(null)} />} {detailTarget && ( setDetailTarget(null)} /> )}
) } // ── Printer Totals Tab ──────────────────────────────────────────────────────── function PrintersTab() { const [fromDt, setFromDt] = useState(todayStart()) const [toDt, setToDt] = useState(todayEnd()) const [printTarget, setPrintTarget] = useState(null) // printer_id const [detailTarget, setDetailTarget] = useState(null) // full printer row object const params = new URLSearchParams({ from: fromDt, to: toDt }) const { data, isLoading, refetch } = useQuery({ queryKey: ['report-printers', fromDt, toDt], queryFn: () => client.get(`/api/reports/printers?${params}`).then(r => r.data), }) const { data: printers = [] } = useQuery({ queryKey: ['printers'], queryFn: () => client.get('/api/system/printers').then(r => r.data), staleTime: 60_000, }) const { data: tables = [] } = useQuery({ queryKey: ['tables'], queryFn: () => client.get('/api/tables/').then(r => r.data), staleTime: 60_000, }) const tableMap = Object.fromEntries(tables.map(t => [t.id, t.label || `T${t.number}`])) const printMutation = useMutation({ mutationFn: (body) => client.post('/api/reports/print/printer', body), onSuccess: () => toast.success('Αποστολή στον εκτυπωτή…'), onError: () => toast.error('Σφάλμα εκτύπωσης'), }) const rows = data?.printers ?? [] function handlePrint({ mode, fromDt: fd, toDt: td, printerId }) { printMutation.mutate({ printer_target_id: printTarget, printer_id: printerId, mode, from_dt: fd, to_dt: td }) } return (
{/* Toolbar */}
setFromDt(e.target.value)} />
setToDt(e.target.value)} />
{isLoading &&

Φόρτωση…

} {!isLoading && rows.length === 0 && (

Δεν βρέθηκαν δεδομένα για αυτό το διάστημα.

)} {rows.length > 0 && (
{rows.map((r, i) => ( ))}
Εκτυπωτής Εργασίες Παραγγελίες Αντικείμενα Σύνολο (€)
{r.printer_name} {r.print_jobs} {r.orders} {r.items} €{r.total.toFixed(2)}
Σύνολο {rows.reduce((s, r) => s + r.print_jobs, 0)} {rows.reduce((s, r) => s + r.orders, 0)} {rows.reduce((s, r) => s + r.items, 0)} €{rows.reduce((s, r) => s + r.total, 0).toFixed(2)}
)} {printTarget !== null && printers.length > 0 && ( r.printer_id === printTarget)?.printer_name ?? 'Εκτυπωτής'}`} printers={printers} defaultFrom={fromDt} defaultTo={toDt} onClose={() => setPrintTarget(null)} onPrint={handlePrint} /> )} {detailTarget && ( setDetailTarget(null)} /> )}
) } // ── Order History Tab ───────────────────────────────────────────────────────── function HistoryTab({ filters, setFilters }) { const [page, setPage] = useState(1) const [detailOrder, setDetailOrder] = useState(null) // full order object for modal const [printOrderId, setPrintOrderId] = useState(null) const [payersModal, setPayersModal] = useState(null) // order object for multi-payer modal const { data: tables = [] } = useQuery({ queryKey: ['tables'], queryFn: () => client.get('/api/tables/').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 { data: waiters = [] } = useQuery({ queryKey: ['waiters'], queryFn: () => client.get('/api/waiters/').then(r => r.data), staleTime: 60_000, }) const tableMap = Object.fromEntries(tables.map(t => [t.id, t.label || `T${t.number}`])) const waiterMap = Object.fromEntries(waiters.map(w => [w.id, w.full_name || w.username])) const params = new URLSearchParams({ from: filters.from, to: filters.to, page }) if (filters.status) params.set('status', filters.status) if (filters.table_id) params.set('table_id', filters.table_id) const { data: orders = [], isLoading } = useQuery({ queryKey: ['order-history', filters, page], queryFn: () => client.get(`/api/reports/orders/history?${params}`).then(r => r.data), }) const printMutation = useMutation({ mutationFn: ({ orderId, printerId }) => client.post(`/api/orders/${orderId}/print`, { printer_id: printerId }), onSuccess: () => toast.success('Αποστολή στον εκτυπωτή…'), onError: () => toast.error('Σφάλμα εκτύπωσης'), }) function setF(k, v) { setFilters(f => ({ ...f, [k]: v })); setPage(1) } const visibleOrders = filters.hideEmpty ? orders.filter(o => o.items.some(i => i.status !== 'cancelled')) : orders return (
{/* Toolbar */}
setF('from', e.target.value)} />
setF('to', e.target.value)} />
{isLoading &&

Φόρτωση…

} {!isLoading && visibleOrders.length === 0 && (

Δεν βρέθηκαν παραγγελίες.

)} {visibleOrders.length > 0 && (
{visibleOrders.map(o => { const total = o.items .filter(i => i.status !== 'cancelled') .reduce((s, i) => s + i.unit_price * i.quantity, 0) // Opener / closer const openerName = waiterMap[o.opened_by] || `#${o.opened_by}` const closerName = o.closed_by ? (waiterMap[o.closed_by] || `#${o.closed_by}`) : null // Latest payment info from items const paidItems = o.items.filter(i => i.status === 'paid' && i.paid_by) const payerIds = [...new Set(paidItems.map(i => i.paid_by))] const latestPaidAt = paidItems.reduce((max, i) => (!max || i.paid_at > max) ? i.paid_at : max, null) return ( ) })}
# Τραπέζι Ανοίχτηκε Έκλεισε Πληρώθηκε Κατάσταση Σημείωση Σύνολο
{o.id} {tableMap[o.table_id] || `#${o.table_id}`}
{fmtDt(o.opened_at)}
{openerName}
{o.closed_at ? <>
{fmtDt(o.closed_at)}
{closerName &&
{closerName}
} : '—'}
{payerIds.length === 0 ? '—' : payerIds.length === 1 ? ( <>
{waiterMap[payerIds[0]] || `#${payerIds[0]}`}
{latestPaidAt &&
{fmtDt(latestPaidAt)}
} ) : ( )}
{o.notes || '—'} €{total.toFixed(2)}
)}
Σελίδα {page}
{detailOrder && ( setDetailOrder(null)} onPrint={(orderId, printerId) => printMutation.mutate({ orderId, printerId })} /> )} {payersModal && ( setPayersModal(null)} /> )} {printOrderId !== null && printers.length > 0 && ( setPrintOrderId(null)} onPrint={(printerId) => { printMutation.mutate({ orderId: printOrderId, printerId }); setPrintOrderId(null) }} /> )}
) }