From 184bee5942f328dc5d5f3f5585d8586bae29331b Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 8 Jun 2026 03:40:12 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=202K=20=E2=80=94=20business=20day?= =?UTF-8?q?=20close=20summary=20(expanded)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- local_backend/routers/business_day.py | 76 ++++++++- .../src/components/WorkdaySummaryModal.jsx | 158 ++++++++++++++++-- 2 files changed, 217 insertions(+), 17 deletions(-) diff --git a/local_backend/routers/business_day.py b/local_backend/routers/business_day.py index 7566267..77ed20e 100644 --- a/local_backend/routers/business_day.py +++ b/local_backend/routers/business_day.py @@ -300,8 +300,8 @@ def business_day_summary( else: e["other"] += val + from routers.shifts import compute_shift_pay for shift in shifts: - from routers.shifts import compute_shift_pay e = _waiter_entry(shift.waiter_id) if e["shift_started_at"] is None or (shift.started_at and _dt(shift.started_at) < e["shift_started_at"]): e["shift_started_at"] = _dt(shift.started_at) @@ -422,6 +422,62 @@ def business_day_summary( total_waiter_collected = sum(s.total_collected or 0.0 for s in shifts) store_expected_closing = (day.store_opening_cash or 0.0) + total_waiter_collected + # ── Phase 2K: full financial summary ──────────────��───────────────────── + + # COGS from Phase 2A cost snapshots on paid items + from models.waste import WasteLog + from models.expenses import Expense + from models.tabs import Tab, TabEntry + + cogs_trackable = 0.0 + cogs_uncosted_revenue = 0.0 + cogs_uncosted_count = 0 + for item in paid_items: + rev = item.unit_price * item.quantity + if item.unit_cost is not None: + cogs_trackable += item.unit_cost * item.quantity + else: + cogs_uncosted_revenue += rev + cogs_uncosted_count += 1 + gross_profit = round(total_revenue - cogs_trackable, 2) + cogs_has_gap = cogs_uncosted_count > 0 + + # LABOR from shifts + total_labor_cost = 0.0 + labor_untracked_shifts = 0 + for shift in shifts: + pay_data = compute_shift_pay(shift) + if pay_data["shift_pay"] is not None: + total_labor_cost += pay_data["shift_pay"] + else: + labor_untracked_shifts += 1 + + # WASTE from Phase 2H + waste_entries = db.query(WasteLog).filter(WasteLog.business_day_id == day.id).all() + waste_item_count = len(waste_entries) + waste_estimated_cost = round(sum(w.total_cost for w in waste_entries if w.total_cost is not None), 2) + + # EXPENSES logged today + expenses_today = db.query(Expense).filter(Expense.business_day_id == day.id).all() + expenses_total = round(sum(e.total_amount for e in expenses_today), 2) + expenses_paid_today = round(sum(e.paid_amount for e in expenses_today), 2) + expenses_due = round(expenses_total - expenses_paid_today, 2) + + # TABBED REVENUE (items moved to tabs — not yet collected) + tabbed_items = [i for i in all_items if i.status == "tabbed"] + tabbed_revenue = round(sum(i.unit_price * i.quantity for i in tabbed_items), 2) + + # NET estimate (revenue - COGS - labor - waste - expenses paid) + net_estimate = round( + total_revenue + - cogs_trackable + - total_labor_cost + - waste_estimated_cost + - expenses_paid_today, + 2 + ) + net_has_unknowns = cogs_has_gap or labor_untracked_shifts > 0 + return { "day_id": day.id, "status": day.status, @@ -438,12 +494,28 @@ def business_day_summary( "with_unpaid_items": with_unpaid_items, "active_shifts": active_shift_count, }, - # Phase 2E + # Phase 2E — cash reconciliation "store_opening_cash": day.store_opening_cash, "store_closing_cash": day.store_closing_cash, "store_cash_discrepancy": day.store_cash_discrepancy, "store_expected_closing": round(store_expected_closing, 2), "total_waiter_collected": round(total_waiter_collected, 2), + # Phase 2K — full financial summary + "cogs_trackable": round(cogs_trackable, 2), + "cogs_uncosted_revenue": round(cogs_uncosted_revenue, 2), + "cogs_uncosted_count": cogs_uncosted_count, + "cogs_has_gap": cogs_has_gap, + "gross_profit": gross_profit, + "total_labor_cost": round(total_labor_cost, 2), + "labor_untracked_shifts": labor_untracked_shifts, + "waste_item_count": waste_item_count, + "waste_estimated_cost": waste_estimated_cost, + "expenses_total": expenses_total, + "expenses_paid_today": expenses_paid_today, + "expenses_due": expenses_due, + "tabbed_revenue": tabbed_revenue, + "net_estimate": net_estimate, + "net_has_unknowns": net_has_unknowns, } diff --git a/manager_dashboard/src/components/WorkdaySummaryModal.jsx b/manager_dashboard/src/components/WorkdaySummaryModal.jsx index edb9948..c0f34e1 100644 --- a/manager_dashboard/src/components/WorkdaySummaryModal.jsx +++ b/manager_dashboard/src/components/WorkdaySummaryModal.jsx @@ -122,38 +122,149 @@ function PaymentBar({ w, total }) { // ── Tabs ────────────────────────────────────────────────────────────────────── +function FinancialRow({ label, value, sub, accent, warn, indent = false }) { + return ( +
+ + {warn && }{label} + +
+ + {value} + + {sub &&
{sub}
} +
+
+ ) +} + function OverviewTab({ data }) { const waiters = data.waiters.filter(w => w.role === 'waiter') + return ( -
-
+
+ {/* Top stats */} +
-
- Ώρα Λειτουργίας -
-
-
ΑΝΟΙΓΜΑ
-
{fmtDateTime(data.opened_at)}
+ {/* Hours */} +
+
+
ΑΝΟΙΓΜΑ
+
{fmtDateTime(data.opened_at)}
+
+
+
ΚΛΕΙΣΙΜΟ
+
+ {data.closed_at ? fmtDateTime(data.closed_at) : Ημέρα Ενεργή}
-
-
ΚΛΕΙΣΙΜΟ
-
- {data.closed_at - ? fmtDateTime(data.closed_at) - : Ημέρα Ενεργή} +
+
+ + {/* Full P&L */} +
+ {/* REVENUE */} +
+ Έσοδα + + {data.tabbed_revenue > 0 && ( + + )} + +
+ + {/* COGS */} +
+ Κόστος Πωληθέντων (COGS) + + {data.cogs_has_gap && ( + + )} + = 0 ? '#16a34a' : '#dc2626'} + warn={data.cogs_has_gap} + sub={data.cogs_has_gap ? 'Μερικό — υπάρχουν αντ. χωρίς κόστος' : undefined} + /> +
+ + {/* LABOR */} +
+ Μισθοδοσία + + {data.labor_untracked_shifts > 0 && ( + + )} +
+ + {/* WASTE */} + {(data.waste_item_count > 0) && ( +
+ Αποβλήτα + +
+ )} + + {/* EXPENSES */} + {data.expenses_total > 0 && ( +
+ Έξοδα (σήμερα) + + + {data.expenses_due > 0 && ( + + )} +
+ )} + + {/* NET */} +
+
+
+
+ Εκτιμώμενο Καθαρό Αποτέλεσμα +
+ {data.net_has_unknowns && ( +
+ ⚠ Μερικό — υπάρχουν άγνωστα κόστη +
+ )} +
+
= 0 ? '#16a34a' : '#dc2626', + fontVariantNumeric: 'tabular-nums', + }}> + {fmt(data.net_estimate)}
+ {/* Payment breakdown */}
Κατανομή Πληρωμών -
+
{(() => { 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 }) { })()}
+ + {/* Cash reconciliation summary (if counted) */} + {data.store_closing_cash != null && ( +
+ Ταμείο +
+ + + +
+
+ )}
) }