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 (
)
}
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)}
Μετά την επιβεβαίωση θα εμφανιστεί η αναλυτική σύνοψη βάρδιας.
)
}
// ─── Compose for single waiter (quick message from shift row) ─────────────────
function QuickMessageModal({ waiter, tables, templates, onClose, onSent }) {
const [body, setBody] = useState('')
const sendMut = useMutation({
mutationFn: (payload) => client.post('/api/messages/send', payload),
onSuccess: () => { toast.success('Εστάλη!'); onSent(); onClose() },
onError: () => toast.error('Σφάλμα αποστολής'),
})
function send() {
if (!body.trim()) return
sendMut.mutate({ body: body.trim(), target_waiter_ids: [waiter.id], table_ids: [] })
}
return (
{ if (e.target === e.currentTarget) onClose() }}>
💬 Μήνυμα σε {waiter.name}
{templates.length > 0 && (
{templates.map(t => (
))}
)}
)
}
function ShiftsCard({ activeShifts, waitersWithoutShift, isOpen, onStartShift, onEndShift, onMessageWaiter, privacyMode }) {
return (
Βάρδιες σε εξέλιξη
{isOpen && waitersWithoutShift.length > 0 && (
)}
{activeShifts.length === 0 ? (
Κανένας σερβιτόρος σε βάρδια
) : (
activeShifts.map(s => {
const activeBreak = Array.isArray(s.breaks) ? s.breaks.find(b => !b.ended_at) : null
return (
{s.waiter_name}
{activeBreak && (
☕ Διάλειμμα · {fmtDuration(activeBreak.started_at)}
)}
από {fmtTime(s.started_at)} · {fmtDuration(s.started_at)}
{s.total_collected > 0 && (
{fmtEuro(s.total_collected)}
)}
)
})
)}
)
}
// ─── Revenue bar chart ─────────────────────────────────────────────────────────
function RevenueChart({ orders }) {
const now = new Date()
const currentHour = now.getHours()
const buckets = {}
for (let h = 10; h <= 23; h++) buckets[h] = 0
orders.forEach(o => {
const h = new Date(o.opened_at).getHours()
if (h >= 10 && h <= 23) buckets[h] = (buckets[h] || 0) + orderTotal(o.items)
})
const hours = Object.keys(buckets).map(Number)
const max = Math.max(...Object.values(buckets), 1)
return (
Έσοδα ανά ώρα
{hours.map(h => {
const val = buckets[h]
const heightPct = (val / max) * 100
const isCurrent = h === currentHour
const isFuture = h > currentHour
return (
0 ? `${heightPct}%` : '3px',
minHeight: val > 0 ? 4 : 3, borderRadius: 4,
background: isCurrent ? '#3758c9' : isFuture ? '#edeff1' : '#c2cff0',
transition: 'height 300ms ease',
}} />
{h}
)
})}
)
}
// ─── Reservations stub ────────────────────────────────────────────────────────
function ReservationsCard() {
return (
📅
Σύντομα — σύστημα κρατήσεων υπό ανάπτυξη
)
}
// ─── Messages card ────────────────────────────────────────────────────────────
function ComposeModal({ waiters, tables, templates, onClose, onSent }) {
const [selectedWaiters, setSelectedWaiters] = useState([]) // [] = all
const [selectedTables, setSelectedTables] = useState([])
const [body, setBody] = useState('')
const sendMut = useMutation({
mutationFn: (payload) => client.post('/api/messages/send', payload),
onSuccess: () => { toast.success('Εστάλη!'); onSent(); onClose() },
onError: () => toast.error('Σφάλμα αποστολής'),
})
function toggleWaiter(id) {
setSelectedWaiters(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
}
function toggleTable(id) {
setSelectedTables(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
}
function send() {
if (!body.trim()) return
sendMut.mutate({
body: body.trim(),
target_waiter_ids: selectedWaiters, // [] = broadcast all
table_ids: selectedTables,
})
}
return (
e.stopPropagation()}>
{/* Recipients */}
Παραλήπτες {selectedWaiters.length === 0 && (Όλοι)}
{waiters.map(w => {
const name = w.nickname || w.full_name || w.username
const sel = selectedWaiters.includes(w.id)
return (
)
})}
{/* Tables (optional) */}
{tables.length > 0 && (
Σχετικά Τραπέζια (προαιρετικό)
{tables.map(t => {
const name = t.label || `T${t.number}`
const sel = selectedTables.includes(t.id)
return (
)
})}
)}
{/* Quick templates */}
{templates.length > 0 && (
Γρήγορα Μηνύματα
{templates.map(t => (
))}
)}
{/* Custom body */}
)
}
function MessagesCard({ waiters, tables }) {
const qc = useQueryClient()
const [composing, setComposing] = useState(false)
const { data: messages = [] } = useQuery({
queryKey: ['messages-all'],
queryFn: () => client.get('/api/messages/all?limit=20').then(r => r.data),
refetchInterval: 10_000,
})
const { data: templates = [] } = useQuery({
queryKey: ['quick-templates'],
queryFn: () => client.get('/api/messages/templates').then(r => r.data),
staleTime: 60_000,
})
function parseJsonField(val) {
if (Array.isArray(val)) return val
try { return JSON.parse(val || '[]') } catch { return [] }
}
function fmtTargets(msg) {
const ids = parseJsonField(msg.target_waiter_ids)
if (ids.length === 0) return 'Όλοι'
return ids.map(id => {
const w = waiters.find(w => w.id === id)
return w ? (w.nickname || w.full_name || w.username) : `#${id}`
}).join(', ')
}
return (
<>
{composing && (
setComposing(false)}
onSent={() => qc.invalidateQueries({ queryKey: ['messages-all'] })}
/>
)}
Μηνύματα
{messages.length === 0 ? (
💬
Δεν υπάρχουν μηνύματα ακόμα
) : (
{messages.map(msg => {
const ackedIds = parseJsonField(msg.acked_by).length
const totalTargets = parseJsonField(msg.target_waiter_ids).length || waiters.length
return (
{msg.body}
Προς: {fmtTargets(msg)} · {new Date(msg.created_at).toLocaleTimeString('el-GR', { hour: '2-digit', minute: '2-digit' })}
= totalTargets ? '#16a34a' : '#f59e0b',
background: ackedIds >= totalTargets ? '#f0fdf4' : '#fffbeb',
border: `1px solid ${ackedIds >= totalTargets ? '#bbf7d0' : '#fde68a'}`,
borderRadius: 999, padding: '2px 8px', whiteSpace: 'nowrap', flexShrink: 0,
}}>
{ackedIds}/{totalTargets} ✓
)
})}
)}
>
)
}
// ─── Pending prints panel ──────────────────────────────────────────────────────
function PendingPrintsPanel({ pendingPrintOrders, onRetryAll, onRetrySingle, onViewOrder, retryingId }) {
if (pendingPrintOrders.length === 0) return null
return (
⏳
Εκκρεμείς Εκτυπώσεις
{pendingPrintOrders.length} παραγγελί{pendingPrintOrders.length !== 1 ? 'ες' : 'α'} δεν έχ{pendingPrintOrders.length !== 1 ? 'ουν' : 'ει'} σταλεί
{pendingPrintOrders.map(({ table, order }) => {
const unprinted = order.items.filter(i => i.status === 'active' && !i.printed)
const tableName = table.label || `T${table.number}`
return (
{tableName}
{unprinted.length} αντικείμενο{unprinted.length !== 1 ? 'α' : ''} εκκρεμούν
{unprinted.map(i => i.product?.name || `#${i.product_id}`).join(', ')}
)
})}
)
}
// ─── Main page ─────────────────────────────────────────────────────────────────
export default function DashboardPage() {
const [showStartShift, setShowStartShift] = useState(false)
const [closeDetails, setCloseDetails] = useState(null)
const [forceClosing, setForceClosing] = useState(false)
const [retryingId, setRetryingId] = useState(null)
const [quickView, setQuickView] = useState(null)
const [licenseBlock, setLicenseBlock] = useState(null) // { code, message } when open is blocked
// End shift flow: confirm modal → summary modal
const [endShiftTarget, setEndShiftTarget] = useState(null) // shift object to confirm
const [endShiftBusy, setEndShiftBusy] = useState(false)
const [shiftSummaryId, setShiftSummaryId] = useState(null) // show summary for this shift id
// Quick message to single waiter
const [messageWaiter, setMessageWaiter] = useState(null) // { id, name }
const [privacyMode, setPrivacyMode] = useState(() => localStorage.getItem('privacyMode') === 'true')
const navigate = useNavigate()
const qc = useQueryClient()
const license = useContext(LicenseContext)
const { data: businessDay } = useQuery({
queryKey: ['business-day'],
queryFn: () => client.get('/api/business-day/current').then(r => r.data),
refetchInterval: 10_000,
})
const { data: activeShifts = [] } = useQuery({
queryKey: ['active-shifts'],
queryFn: () => client.get('/api/shifts/?active_only=true').then(r => r.data.shifts ?? []),
refetchInterval: 10_000,
})
const { data: allWaiters = [] } = useQuery({
queryKey: ['waiters'],
queryFn: () => client.get('/api/waiters/').then(r => r.data),
staleTime: 60_000,
})
const { data: quickTemplates = [] } = useQuery({
queryKey: ['quick-templates'],
queryFn: () => client.get('/api/messages/templates').then(r => r.data),
staleTime: 60_000,
})
const { data: tables = [], isLoading: tablesLoading } = useQuery({
queryKey: ['tables'],
queryFn: () => client.get('/api/tables/').then(r => r.data),
refetchInterval: 5_000,
})
const { data: orders = [], isLoading: ordersLoading } = useQuery({
queryKey: ['orders-active'],
queryFn: () => client.get('/api/orders/').then(r => r.data),
refetchInterval: 5_000,
})
const openDayMut = useMutation({
mutationFn: () => client.post('/api/business-day/open', {}),
onSuccess: () => { toast.success('Ημέρα ανοίχτηκε!'); qc.invalidateQueries({ queryKey: ['business-day'] }) },
onError: (e) => {
const detail = e.response?.data?.detail
if (detail?.code === 'SYSTEM_LOCKED' || detail?.code === 'LICENSE_EXPIRED') {
setLicenseBlock({ code: detail.code, message: detail.message })
} else {
toast.error(typeof detail === 'string' ? detail : 'Σφάλμα')
}
},
})
function handleOpenDay() {
if (license?.isBlocked) {
setLicenseBlock({
code: license.lock_reason === 'admin' ? 'SYSTEM_LOCKED' : 'LICENSE_EXPIRED',
message: license.lock_reason === 'admin'
? 'Το σύστημα έχει κλειδωθεί από διαχειριστή. Επικοινωνήστε με την υποστήριξη.'
: 'Η άδεια χρήσης έχει λήξει. Ανανεώστε την άδεια ή επικοινωνήστε με την υποστήριξη.',
})
return
}
openDayMut.mutate()
}
async function handleCloseDay(force = false) {
setForceClosing(force)
try {
await client.post('/api/business-day/close', { force })
toast.success('Ημέρα έκλεισε!')
setCloseDetails(null)
qc.invalidateQueries({ queryKey: ['business-day'] })
qc.invalidateQueries({ queryKey: ['active-shifts'] })
qc.invalidateQueries({ queryKey: ['orders-active'] })
qc.invalidateQueries({ queryKey: ['tables'] })
qc.invalidateQueries({ queryKey: ['messages-all'] })
} catch (e) {
const detail = e.response?.data?.detail
if (e.response?.status === 409 && detail?.open_orders) setCloseDetails(detail)
else toast.error(typeof detail === 'string' ? detail : 'Σφάλμα κλεισίματος')
} finally {
setForceClosing(false)
}
}
async function handleEndShiftConfirm(notes) {
if (!endShiftTarget) return
setEndShiftBusy(true)
try {
await client.post(`/api/shifts/manager/end/${endShiftTarget.id}`, { notes: notes || null })
qc.invalidateQueries({ queryKey: ['active-shifts'] })
const summaryTarget = { id: endShiftTarget.id, waiter_id: endShiftTarget.waiter_id }
setEndShiftTarget(null)
setShiftSummaryId(summaryTarget)
} catch (e) {
toast.error(e.response?.data?.detail || 'Σφάλμα')
} finally {
setEndShiftBusy(false)
}
}
async function handleStartShift(waiterId, startingCash) {
await client.post('/api/shifts/manager/start', { waiter_id: waiterId, starting_cash: startingCash })
toast.success('Βάρδια ξεκίνησε!')
qc.invalidateQueries({ queryKey: ['active-shifts'] })
}
async function retrySingleOrder(orderId) {
setRetryingId(orderId)
try {
const res = await client.post(`/api/orders/${orderId}/retry-print`)
const results = res.data.print_results ?? []
const allOk = results.length === 0 || results.every(r => r.success)
if (allOk) toast.success('Εκτυπώθηκε επιτυχώς')
else {
const failed = results.filter(r => !r.success).map(r => r.printer_name).join(', ')
toast.error(`Αποτυχία: ${failed}`)
}
qc.invalidateQueries({ queryKey: ['orders-active'] })
} catch {
toast.error('Σφάλμα επικοινωνίας')
} finally {
setRetryingId(null)
}
}
async function retryAllOrders() {
for (const { order } of pendingPrintOrders) {
if (order) await retrySingleOrder(order.id)
}
}
const isOpen = !!businessDay
const waitersWithoutShift = allWaiters.filter(
w => w.role === 'waiter' && !activeShifts.some(s => s.waiter_id === w.id)
)
// Build table cards
const tableCards = tables.map(table => {
const order = orders.find(o =>
o.table_id === table.id && ['open', 'partially_paid', 'paid'].includes(o.status)
)
const tableStatus = order ? order.status : 'free'
const hasPendingPrint = order ? order.items.some(i => i.status === 'active' && !i.printed) : false
return { table, order, tableStatus, hasPendingPrint }
})
const pendingPrintOrders = tableCards.filter(c => c.hasPendingPrint)
// KPIs — revenue scoped to orders that opened after business day start
const businessDayStart = businessDay?.opened_at ? new Date(businessDay.opened_at) : null
const dayOrders = businessDayStart
? orders.filter(o => new Date(o.opened_at) >= businessDayStart)
: orders
const totalRevenue = dayOrders.reduce((s, o) => s + orderTotal(o.items), 0)
const openCount = tableCards.filter(c => c.tableStatus === 'open').length
const partialCount = tableCards.filter(c => c.tableStatus === 'partially_paid').length
const occupiedCount = openCount + partialCount
const freeCount = tableCards.filter(c => c.tableStatus === 'free').length
// Avg ticket: revenue divided by number of orders with at least one paid item
const paidOrders = dayOrders.filter(o => o.items.some(i => i.paid_by))
const avgTicket = paidOrders.length > 0 ? totalRevenue / paidOrders.length : 0
// Total collected by all active shifts
const totalCollected = activeShifts.reduce((s, sh) => s + (sh.total_collected || 0), 0)
const todayStr = new Date().toLocaleDateString('el-GR', { weekday: 'long', day: 'numeric', month: 'long' })
if (tablesLoading || ordersLoading) {
return Φόρτωση…
}
return (
{/* ── Header bar ─────────────────────────────────────────────────────── */}
{isOpen ? 'Ημέρα Ανοιχτή' : 'Ημέρα Κλειστή'}
{todayStr}
{isOpen && businessDay?.opened_at && ` · από ${fmtTime(businessDay.opened_at)} · ${fmtDuration(businessDay.opened_at)}`}
{isOpen ? (
) : (
)}
{/* ── KPI strip — full-width flex row ────────────────────────────────── */}
0 ? (occupiedCount / tables.length) * 100 : 0}
/>
0 ? activeShifts.map(s => s.waiter_name.split(' ')[0]).join(', ') : 'Κανένας σε βάρδια'}
accent="#2f9e5e"
/>
{pendingPrintOrders.length > 0 && (
)}
{/* ── Main 2-column layout: 65 / 35 ──────────────────────────────────── */}
{/* ── Left column — Shifts + Tables + Revenue + Pending prints ───────── */}
{/* Shifts card — FIRST */}
setShowStartShift(true)}
onEndShift={(shift) => setEndShiftTarget(shift)}
onMessageWaiter={(s) => setMessageWaiter({ id: s.waiter_id, name: s.waiter_name })}
privacyMode={privacyMode}
/>
{/* Tables overview — SECOND */}
Τραπέζια τώρα
{tableCards.map(({ table, tableStatus, order }) => (
setQuickView({ orderId: order.id, tableName: table.label || `T${table.number}` }) : undefined}
/>
))}
{Object.entries(TABLE_COLORS).map(([key, c]) => (
{c.label}
{tableCards.filter(tc => tc.tableStatus === key).length}
))}
{/* Revenue chart */}
{/* Pending prints */}
setQuickView({ orderId: id, tableName: tables.find(t => tableCards.find(tc => tc.order?.id === id)?.table.id === t.id)?.label || '—' })}
retryingId={retryingId}
/>
{/* ── Right column — Reservations + Messages ───────────────────────── */}
{/* ── Modals ─────────────────────────────────────────────────────────── */}
{showStartShift && (
setShowStartShift(false)}
onStart={handleStartShift}
/>
)}
{endShiftTarget && (
setEndShiftTarget(null)}
onConfirm={handleEndShiftConfirm}
busy={endShiftBusy}
/>
)}
{shiftSummaryId && (
setShiftSummaryId(null)}
/>
)}
{messageWaiter && (
setMessageWaiter(null)}
onSent={() => qc.invalidateQueries({ queryKey: ['messages-all'] })}
/>
)}
{closeDetails && (
setCloseDetails(null)}
onConfirm={() => handleCloseDay(true)}
busy={forceClosing}
/>
)}
{quickView && (
setQuickView(null)}
onOpenFull={() => { navigate(`/orders/${quickView.orderId}`); setQuickView(null) }}
/>
)}
{licenseBlock && (
{licenseBlock.code === 'SYSTEM_LOCKED' ? '🔒' : '⚠️'}
{licenseBlock.code === 'SYSTEM_LOCKED' ? 'Σύστημα Κλειδωμένο' : 'Άδεια Χρήσης Ληγμένη'}
{licenseBlock.message}
)}
)
}