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>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user