Files
xenia-pos-local/manager_dashboard/src/pages/reports/restaurant/WorkDaySummary.jsx
bonamin 8ba8c95ecd feat: initial commit — local services (backend + manager dashboard + waiter PWA)
Includes all work to date:
- local_backend: FastAPI backend with products, orders, tables, shifts, cloud sync
- manager_dashboard: React manager UI with product/category management, reports, settings
- waiter_pwa: React PWA for waiter devices
- Category reparent endpoint and UI
- Waiter domain: local_ip sent on heartbeat, waiter_domain persisted from cloud response
- QR code modal in AppInfoTab for waiter domain
- Product form: number input spinners removed, category pre-selected on new product
- Category row: count badge moved to far right

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:04:38 +03:00

146 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
import client from '../../../api/client'
import { FilterBar, FilterDateInput } from '../shared/FilterBar'
import { Panel, DataTable, THead, TH, TR, TD, StatusBadge, ChartTooltip } from '../shared/TablePrimitives'
import DrillDownModal from '../shared/DrillDownModal'
import EmptyState from '../shared/EmptyState'
import SkeletonTable from '../shared/SkeletonTable'
import ExportButton from '../shared/ExportButton'
import { fmtEUR, fmtNum, fmtDate, fmtTime, fmtDuration } from '../shared/reportDesignTokens'
function today() { return new Date().toISOString().slice(0, 10) }
function monthAgo() { const d = new Date(); d.setDate(d.getDate() - 30); return d.toISOString().slice(0, 10) }
export default function WorkDaySummary() {
const [from, setFrom] = useState(monthAgo())
const [to, setTo] = useState(today())
const [drillId, setDrillId] = useState(null)
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ['business-days', from, to],
queryFn: () => client.get('/api/reports/business-days', { params: { from: from + 'T00:00:00', to: to + 'T23:59:59' } }).then(r => r.data),
staleTime: 60 * 1000,
})
const { data: drillData } = useQuery({
queryKey: ['business-day-orders', drillId],
queryFn: () => client.get('/api/reports/orders/history', { params: { business_day_id: drillId, page_size: 200 } }).then(r => r.data),
enabled: !!drillId,
staleTime: 60 * 1000,
})
const days = data?.business_days || []
const drillDay = drillId ? days.find(d => d.id === drillId) : null
const drillOrders = Array.isArray(drillData) ? drillData : []
const chartData = [...days].reverse().map(d => ({
date: fmtDate(d.opened_at),
revenue: d.revenue,
}))
if (isLoading) return <div className="flex-1 overflow-y-auto p-6"><SkeletonTable rows={8} columns={9} showChart /></div>
if (isError) return (
<div className="flex flex-col flex-1 min-h-0">
<FilterBar><span className="text-[12px] text-slate-500">Αδυναμία φόρτωσης δεδομένων</span></FilterBar>
<div className="flex flex-1 items-center justify-center"><button onClick={() => refetch()} className="rounded-md border border-slate-200 px-4 py-2 text-sm hover:bg-slate-50">Επανάληψη</button></div>
</div>
)
return (
<div className="flex flex-col flex-1 min-h-0">
<FilterBar right={
<ExportButton endpoint="/api/reports/orders/export" params={{ from: from + 'T00:00:00', to: to + 'T23:59:59' }} filename={`workdays-${from}-to-${to}.csv`} />
}>
<FilterDateInput value={from} onChange={setFrom} label="Από" />
<FilterDateInput value={to} onChange={setTo} label="Έως" />
</FilterBar>
<div className="flex-1 overflow-y-auto p-6">
{chartData.length > 0 && (
<Panel title="Έσοδα ανά Εργάσιμη Μέρα" subtitle={`${days.length} μέρες · ${from}${to}`}>
<div style={{ height: 220 }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 8, right: 16, bottom: 4, left: 4 }}>
<CartesianGrid vertical={false} stroke="#f1f5f9" />
<XAxis dataKey="date" tick={{ fontSize: 10, fill: '#94a3b8' }} stroke="#cbd5e1" axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 11, fill: '#94a3b8' }} stroke="#cbd5e1" axisLine={false} tickLine={false} tickFormatter={v => '€' + v} />
<Tooltip content={<ChartTooltip formatter={v => fmtEUR(v)} />} />
<Line type="monotone" dataKey="revenue" stroke="#60a5fa" strokeWidth={2} dot={{ r: 3, fill: '#60a5fa' }} activeDot={{ r: 5 }} />
</LineChart>
</ResponsiveContainer>
</div>
</Panel>
)}
<div className="mt-4">
<Panel title="Εργάσιμες Μέρες" padded={false}>
{days.length === 0 ? (
<EmptyState title="Δεν βρέθηκαν εργάσιμες μέρες" description="Δοκιμάστε ευρύτερο εύρος ημερομηνιών." />
) : (
<DataTable>
<THead>
<TH>Εργάσιμη Μέρα</TH>
<TH>Άνοιξε</TH>
<TH>Έκλεισε</TH>
<TH align="right">Διάρκεια</TH>
<TH align="right">Παραγγελίες</TH>
<TH align="right">Έσοδα</TH>
<TH align="right">Ακυρώσεις</TH>
<TH align="right">Σερβιτόροι</TH>
<TH>Κατάσταση</TH>
</THead>
<tbody>
{days.map(d => (
<TR key={d.id} onClick={() => setDrillId(d.id)} striped>
<TD className="font-medium text-slate-900">{fmtDate(d.opened_at)}</TD>
<TD mono>{fmtTime(d.opened_at)}</TD>
<TD mono>{d.closed_at ? fmtTime(d.closed_at) : '—'}</TD>
<TD mono align="right">{fmtDuration(d.opened_at, d.closed_at)}</TD>
<TD mono align="right">{fmtNum(d.order_count)}</TD>
<TD mono align="right" className="font-semibold text-slate-900">{fmtEUR(d.revenue)}</TD>
<TD mono align="right">{fmtNum(d.cancellation_count)}</TD>
<TD mono align="right">{fmtNum(d.waiter_count)}</TD>
<TD><StatusBadge status={d.status} pulse /></TD>
</TR>
))}
</tbody>
</DataTable>
)}
</Panel>
</div>
</div>
{drillDay && (
<DrillDownModal
title={`Εργάσιμη Μέρα · ${fmtDate(drillDay.opened_at)}`}
subtitle={`${drillOrders.length} παραγγελίες · ${fmtEUR(drillDay.revenue)} έσοδα`}
onClose={() => setDrillId(null)}
>
<DataTable>
<THead>
<TH>#</TH><TH>Τραπέζι</TH><TH>Άνοιξε</TH><TH>Έκλεισε</TH><TH align="right">Σύνολο</TH><TH>Κατάσταση</TH>
</THead>
<tbody>
{drillOrders.map(o => {
const total = (o.items || []).filter(i => ['active', 'paid'].includes(i.status)).reduce((s, i) => s + i.unit_price * i.quantity, 0)
return (
<TR key={o.id} striped>
<TD mono>#{o.id}</TD>
<TD>{o.table_id}</TD>
<TD mono>{fmtDate(o.opened_at)} {fmtTime(o.opened_at)}</TD>
<TD mono>{o.closed_at ? fmtTime(o.closed_at) : '—'}</TD>
<TD mono align="right" className="font-semibold">{fmtEUR(total)}</TD>
<TD><StatusBadge status={o.status} /></TD>
</TR>
)
})}
</tbody>
</DataTable>
</DrillDownModal>
)}
</div>
)
}