feat: Phase 2K — business day close summary (expanded)

Backend — /api/business-day/summary now returns full financial summary:
- COGS: trackable cost (sum of unit_cost snapshots on paid items), uncosted item count
  and revenue, gross_profit, cogs_has_gap flag
- Labor: total_labor_cost (sum of shift_pay for the day), labor_untracked_shifts count
- Waste: waste_item_count + waste_estimated_cost from waste_log for this business day
- Expenses: expenses_total, expenses_paid_today, expenses_due for expenses logged today
- Tabbed revenue: revenue from items with status='tabbed' (deferred, not yet collected)
- Net estimate: revenue - COGS - labor - waste - expenses_paid; net_has_unknowns flag
  when any component has gaps (uncosted items or untracked shifts)
- compute_shift_pay import hoisted out of loop (bug fix)

Frontend — WorkdaySummaryModal OverviewTab fully expanded:
- New FinancialRow helper for consistent two-column P&L rows with accent/warn/indent
- REVENUE section: total sales, tabbed deduction, net collected
- COGS section: trackable cost, uncosted items warning, gross profit with gap indicator
- LABOR section: tracked labor cost, untracked shifts warning
- WASTE section: item count + estimated cost (hidden when zero)
- EXPENSES section: total, paid today, still due (hidden when zero)
- NET ESTIMATE: prominent bottom line, amber warning when unknowns exist
- Payment breakdown bar (existing)
- Cash reconciliation summary when store cash was counted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 03:40:12 +03:00
parent 06dccb981e
commit 184bee5942
2 changed files with 217 additions and 17 deletions

View File

@@ -122,38 +122,149 @@ function PaymentBar({ w, total }) {
// ── Tabs ──────────────────────────────────────────────────────────────────────
function FinancialRow({ label, value, sub, accent, warn, indent = false }) {
return (
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
padding: indent ? '4px 0 4px 16px' : '5px 0',
borderBottom: indent ? 'none' : '1px solid #f9fafb',
}}>
<span style={{ fontSize: indent ? 12 : 13, color: warn ? '#d97706' : indent ? '#6b7280' : '#374151', flex: 1 }}>
{warn && <span style={{ marginRight: 5 }}></span>}{label}
</span>
<div style={{ textAlign: 'right', flexShrink: 0 }}>
<span style={{ fontSize: indent ? 12 : 13.5, fontWeight: indent ? 500 : 700, color: accent || '#111315', fontVariantNumeric: 'tabular-nums' }}>
{value}
</span>
{sub && <div style={{ fontSize: 10.5, color: '#9ca3af' }}>{sub}</div>}
</div>
</div>
)
}
function OverviewTab({ data }) {
const waiters = data.waiters.filter(w => w.role === 'waiter')
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 10 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* Top stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))', gap: 10 }}>
<StatPill label="Συνολικά Έσοδα" value={fmt(data.total_revenue)} accent="#111315" />
<StatPill label="Παραγγελίες" value={String(data.total_orders)} accent="#3758c9" />
<StatPill label="Αντικείμενα" value={String(data.total_items_ordered)} accent="#7a44c9" />
<StatPill label="Σερβιτόροι" value={String(waiters.length)} accent="#0d7a8a" />
</div>
<div>
<SectionTitle>Ώρα Λειτουργίας</SectionTitle>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<div style={{ background: '#f9fafb', border: '1px solid #f0f0ef', borderRadius: 10, padding: '12px 16px', flex: 1, minWidth: 150 }}>
<div style={{ fontSize: 11, color: '#9ca3af', fontWeight: 600, marginBottom: 2 }}>ΑΝΟΙΓΜΑ</div>
<div style={{ fontSize: 15, fontWeight: 700 }}>{fmtDateTime(data.opened_at)}</div>
{/* Hours */}
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<div style={{ background: '#f9fafb', border: '1px solid #f0f0ef', borderRadius: 10, padding: '10px 14px', flex: 1, minWidth: 130 }}>
<div style={{ fontSize: 10, color: '#9ca3af', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em' }}>ΑΝΟΙΓΜΑ</div>
<div style={{ fontSize: 14, fontWeight: 700, marginTop: 2 }}>{fmtDateTime(data.opened_at)}</div>
</div>
<div style={{ background: '#f9fafb', border: '1px solid #f0f0ef', borderRadius: 10, padding: '10px 14px', flex: 1, minWidth: 130 }}>
<div style={{ fontSize: 10, color: '#9ca3af', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em' }}>ΚΛΕΙΣΙΜΟ</div>
<div style={{ fontSize: 14, fontWeight: 700, marginTop: 2 }}>
{data.closed_at ? fmtDateTime(data.closed_at) : <span style={{ color: '#16a34a' }}>Ημέρα Ενεργή</span>}
</div>
<div style={{ background: '#f9fafb', border: '1px solid #f0f0ef', borderRadius: 10, padding: '12px 16px', flex: 1, minWidth: 150 }}>
<div style={{ fontSize: 11, color: '#9ca3af', fontWeight: 600, marginBottom: 2 }}>ΚΛΕΙΣΙΜΟ</div>
<div style={{ fontSize: 15, fontWeight: 700 }}>
{data.closed_at
? fmtDateTime(data.closed_at)
: <span style={{ color: '#16a34a' }}>Ημέρα Ενεργή</span>}
</div>
</div>
{/* Full P&L */}
<div style={{ border: '1px solid #e5e7eb', borderRadius: 12, overflow: 'hidden' }}>
{/* REVENUE */}
<div style={{ padding: '10px 16px', background: '#f9fafb', borderBottom: '1px solid #e5e7eb' }}>
<SectionTitle>Έσοδα</SectionTitle>
<FinancialRow label="Σύνολο πωλήσεων" value={fmt(data.total_revenue)} accent="#111315" />
{data.tabbed_revenue > 0 && (
<FinancialRow label="Καρτέλες (μη εισπραχθέντα)" value={` ${fmt(data.tabbed_revenue)}`} accent="#d97706" indent />
)}
<FinancialRow
label="Καθαρά εισπραχθέντα"
value={fmt(data.total_revenue - (data.tabbed_revenue || 0))}
accent="#16a34a"
/>
</div>
{/* COGS */}
<div style={{ padding: '10px 16px', borderBottom: '1px solid #e5e7eb' }}>
<SectionTitle>Κόστος Πωληθέντων (COGS)</SectionTitle>
<FinancialRow label="Κόστος καταγεγραμμένων" value={fmt(data.cogs_trackable)} accent="#374151" />
{data.cogs_has_gap && (
<FinancialRow
label={`${data.cogs_uncosted_count} αντ. χωρίς κόστος (${fmt(data.cogs_uncosted_revenue)} έσοδα)`}
value="?" warn indent
/>
)}
<FinancialRow
label="Μικτό κέρδος"
value={fmt(data.gross_profit)}
accent={data.gross_profit >= 0 ? '#16a34a' : '#dc2626'}
warn={data.cogs_has_gap}
sub={data.cogs_has_gap ? 'Μερικό — υπάρχουν αντ. χωρίς κόστος' : undefined}
/>
</div>
{/* LABOR */}
<div style={{ padding: '10px 16px', borderBottom: '1px solid #e5e7eb' }}>
<SectionTitle>Μισθοδοσία</SectionTitle>
<FinancialRow label="Κόστος εργασίας (καταγεγρ.)" value={fmt(data.total_labor_cost)} accent="#374151" />
{data.labor_untracked_shifts > 0 && (
<FinancialRow label={`${data.labor_untracked_shifts} βάρδιες χωρίς ωριαία αμοιβή`} value="?" warn indent />
)}
</div>
{/* WASTE */}
{(data.waste_item_count > 0) && (
<div style={{ padding: '10px 16px', borderBottom: '1px solid #e5e7eb' }}>
<SectionTitle>Αποβλήτα</SectionTitle>
<FinancialRow
label={`${data.waste_item_count} καταχωρήσεις`}
value={fmt(data.waste_estimated_cost)}
accent="#d97706"
/>
</div>
)}
{/* EXPENSES */}
{data.expenses_total > 0 && (
<div style={{ padding: '10px 16px', borderBottom: '1px solid #e5e7eb' }}>
<SectionTitle>Έξοδα (σήμερα)</SectionTitle>
<FinancialRow label="Σύνολο εξόδων" value={fmt(data.expenses_total)} accent="#374151" />
<FinancialRow label="Πλη<CEBB><CEB7>ωμένα σήμερα" value={fmt(data.expenses_paid_today)} accent="#374151" indent />
{data.expenses_due > 0 && (
<FinancialRow label="Ακόμη οφείλονται" value={fmt(data.expenses_due)} accent="#dc2626" warn indent />
)}
</div>
)}
{/* NET */}
<div style={{ padding: '12px 16px', background: '#f9fafb' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: 13, fontWeight: 700, color: '#374151' }}>
Εκτιμώμενο Καθαρό Αποτέλεσμα
</div>
{data.net_has_unknowns && (
<div style={{ fontSize: 11, color: '#d97706', marginTop: 1 }}>
Μερικό υπάρχουν άγνωστα κόστη
</div>
)}
</div>
<div style={{
fontSize: 20, fontWeight: 800,
color: data.net_estimate >= 0 ? '#16a34a' : '#dc2626',
fontVariantNumeric: 'tabular-nums',
}}>
{fmt(data.net_estimate)}
</div>
</div>
</div>
</div>
{/* Payment breakdown */}
<div>
<SectionTitle>Κατανομή Πληρωμών</SectionTitle>
<div style={{ background: '#f9fafb', border: '1px solid #f0f0ef', borderRadius: 10, padding: '16px 18px' }}>
<div style={{ background: '#f9fafb', border: '1px solid #f0f0ef', borderRadius: 10, padding: '14px 16px' }}>
{(() => {
const totals = { cash: 0, card: 0, transfer: 0, treat: 0, other: 0 }
for (const w of data.waiters) {
@@ -163,6 +274,23 @@ function OverviewTab({ data }) {
})()}
</div>
</div>
{/* Cash reconciliation summary (if counted) */}
{data.store_closing_cash != null && (
<div>
<SectionTitle>Ταμείο</SectionTitle>
<div style={{ background: '#f9fafb', border: '1px solid #f0f0ef', borderRadius: 10, padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 6 }}>
<FinancialRow label="Αναμενόμενο ταμείο" value={fmt(data.store_expected_closing)} />
<FinancialRow label="Μετρημένο ταμείο" value={fmt(data.store_closing_cash)} />
<FinancialRow
label="Διαφορά"
value={data.store_cash_discrepancy === 0 ? '✓ Εντάξει' : fmt(data.store_cash_discrepancy)}
accent={data.store_cash_discrepancy === 0 ? '#16a34a' : '#dc2626'}
warn={data.store_cash_discrepancy !== 0}
/>
</div>
</div>
)}
</div>
)
}