diff --git a/local_backend/routers/business_day.py b/local_backend/routers/business_day.py index 1335a50..56e8ece 100644 --- a/local_backend/routers/business_day.py +++ b/local_backend/routers/business_day.py @@ -246,14 +246,25 @@ def business_day_summary( "shift_started_at": None, "shift_ended_at": None, "starting_cash": None, + # Phase 2B payroll + "hourly_rate_snapshot": None, + "duration_hours": None, + "shift_pay": None, + # distinct tables served (not order count) + "tables_served": 0, } return waiter_stats[uid] + # track unique tables per waiter (opened_by) + waiter_tables: dict = {} # uid → set of table_ids + for o in orders: if o.status == "cancelled": continue e = _waiter_entry(o.opened_by) e["orders_opened"] += 1 + if o.table_id: + waiter_tables.setdefault(o.opened_by, set()).add(o.table_id) for item in all_items: if item.status == "cancelled": @@ -281,6 +292,7 @@ def business_day_summary( e["other"] += val 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) @@ -290,6 +302,18 @@ def business_day_summary( ended = _dt(shift.ended_at) if e["shift_ended_at"] is None or ended > e["shift_ended_at"]: e["shift_ended_at"] = ended + # payroll — take the most recent non-null snapshot + if shift.hourly_rate_snapshot is not None: + e["hourly_rate_snapshot"] = shift.hourly_rate_snapshot + pay_data = compute_shift_pay(shift) + e["duration_hours"] = round((e["duration_hours"] or 0) + pay_data["duration_hours"], 2) + if pay_data["shift_pay"] is not None: + e["shift_pay"] = round((e["shift_pay"] or 0) + pay_data["shift_pay"], 2) + + # write tables_served counts + for uid, table_set in waiter_tables.items(): + if uid in waiter_stats: + waiter_stats[uid]["tables_served"] = len(table_set) # manager gets shift = day open/close manager_opener = users_map.get(day.opened_by_id) diff --git a/manager_dashboard/src/components/WorkdaySummaryModal.jsx b/manager_dashboard/src/components/WorkdaySummaryModal.jsx index 88882e5..1701e03 100644 --- a/manager_dashboard/src/components/WorkdaySummaryModal.jsx +++ b/manager_dashboard/src/components/WorkdaySummaryModal.jsx @@ -170,25 +170,8 @@ function OverviewTab({ data }) { function WaitersTab({ data }) { const [expanded, setExpanded] = useState(null) - // Compute the widest rendered string for each of the 4 metric columns so we - // can give every row the same fixed width — no DOM measurement needed, just - // pick the longest formatted value across all waiters and use ch units. - const colWidths = data.waiters.reduce( - (acc, w) => ({ - orders: Math.max(acc.orders, String(w.orders_opened).length), - items: Math.max(acc.items, String(w.items_ordered).length), - value: Math.max(acc.value, fmt(w.total_items_value).length), - collected: Math.max(acc.collected, fmt(w.total_collected).length), - }), - { orders: 4, items: 4, value: 8, collected: 8 } - ) - // Convert char-count to px using ~8.5px per char at font-size 16, plus label + padding - const colPx = { - orders: Math.max(colWidths.orders * 9 + 16, 72), - items: Math.max(colWidths.items * 9 + 16, 72), - value: Math.max(colWidths.value * 8 + 16, 96), - collected: Math.max(colWidths.collected * 8 + 16, 96), - } + // Fixed column widths for the collapsed summary row + const colPx = { orders: 88, tables: 88, items: 80, collected: 104 } return (
@@ -198,6 +181,10 @@ function WaitersTab({ data }) { {data.waiters.map(w => { const badge = roleBadge(w.role) const isOpen = expanded === w.waiter_id + + // Compute current cash in drawer: starting_cash + collected so far + const drawerNow = (w.starting_cash ?? 0) + (w.total_collected ?? 0) + return (
{badge.label}
- {/* 4 fixed-width metric columns — same width for every row */} + {/* 4 fixed-width metric columns */}
{[ - { label: 'ΠΑΡΑΓΓΕΛΙΕΣ', value: w.orders_opened, width: colPx.orders, color: '#111315' }, - { label: 'ΑΝΤΙΚΕΙΜ.', value: w.items_ordered, width: colPx.items, color: '#111315' }, - { label: 'ΑΞΙΑ ΠΑΡ.', value: fmt(w.total_items_value),width: colPx.value, color: '#374151' }, - { label: 'ΕΙΣΠΡΑΞΗ', value: fmt(w.total_collected), width: colPx.collected, color: '#16a34a' }, + { label: 'ΠΑΡΑΓΓΕΛΙΕΣ', value: w.orders_opened, width: colPx.orders, color: '#111315' }, + { label: 'ΕΞ. ΤΡΑΠΕΖΙΩΝ', value: w.tables_served ?? 0, width: colPx.tables, color: '#111315' }, + { label: 'ΑΝΤΙΚ. ΕΙΣΠΡ.', value: w.items_paid, width: colPx.items, color: '#111315' }, + { label: 'ΕΙΣΠΡΑΞΗ', value: fmt(w.total_collected), width: colPx.collected, color: '#16a34a' }, ].map(col => (
{col.label}
{col.value}
@@ -255,22 +241,56 @@ function WaitersTab({ data }) { {isOpen && ( -
- {/* 4 cells, full-width grid, equal columns */} +
+ {/* Row 1: shift timing + payroll */}
{[ - { label: 'ΑΝΤΙΚ. ΕΙΣΠΡ.', value: w.items_paid, color: '#111315' }, { label: 'ΕΝΑΡΞΗ ΒΑΡΔΙΑΣ', value: fmtTime(w.shift_started_at), color: '#111315' }, { label: 'ΛΗΞΗ ΒΑΡΔΙΑΣ', value: w.shift_ended_at ? fmtTime(w.shift_ended_at) : 'Βάρδια Ενεργή', color: w.shift_ended_at ? '#111315' : '#16a34a', }, + { + label: 'ΜΙΣΘΟΣ / ΩΡΑ', + value: w.hourly_rate_snapshot != null ? fmt(w.hourly_rate_snapshot) : '—', + color: '#111315', + }, + { + label: 'ΜΙΣΘΟΣ (εως τώρα)', + value: w.shift_pay != null ? fmt(w.shift_pay) : 'Δεν παρακολ.', + color: w.shift_pay != null ? '#059669' : '#9ca3af', + }, + ].map(cell => ( +
+
{cell.label}
+
{cell.value}
+
+ ))} +
+ {/* Row 2: cash + collections */} +
+ {[ { label: 'ΑΡΧΙΚΑ ΜΕΤΡΗΤΑ', value: w.starting_cash != null ? fmt(w.starting_cash) : '—', color: '#111315', }, + { + label: 'ΤΑΜΕΙΟ ΤΩΡΑ', + value: fmt(drawerNow), + color: '#3758c9', + }, + { + label: 'ΑΝΤΙΚ. ΕΙΣΠΡ.', + value: w.items_paid, + color: '#111315', + }, + { + label: 'ΕΞ. ΤΡΑΠΕΖΙΩΝ', + value: w.tables_served ?? 0, + color: '#111315', + }, ].map(cell => (
{cell.label}
diff --git a/manager_dashboard/src/pages/reports/shared/ShiftDetailModal.jsx b/manager_dashboard/src/pages/reports/shared/ShiftDetailModal.jsx index 07e7d8b..513bfc2 100644 --- a/manager_dashboard/src/pages/reports/shared/ShiftDetailModal.jsx +++ b/manager_dashboard/src/pages/reports/shared/ShiftDetailModal.jsx @@ -177,20 +177,42 @@ export default function ShiftDetailModal({ shiftId, shiftWaiterId, onClose }) { {summary && ( <> - {/* KPI row */} -
- {[ - { label: 'Διάρκεια', value: fmtMins(summary.duration_minutes) }, - { label: 'Αρχικά μετρητά', value: summary.starting_cash != null ? fmtEUR(summary.starting_cash) : '—' }, - { label: 'Είσπραξη', value: fmtEUR(summary.total_collected), accent: '#2f9e5e' }, - { label: 'Προς παράδοση', value: fmtEUR(summary.net_to_deliver), accent: '#3758c9' }, - ].map(k => ( -
-
{k.label}
-
{k.value}
+ {/* KPI grid — 2 rows × 4 cols */} + {(() => { + const totalOrders = summary.orders?.length ?? 0 + const tableSet = new Set(summary.orders?.map(o => o.table_name).filter(Boolean)) + const tablesServed = tableSet.size + const durationHours = summary.duration_hours ?? (summary.duration_minutes != null ? summary.duration_minutes / 60 : null) + const durationLabel = durationHours != null + ? (() => { const h = Math.floor(durationHours); const m = Math.round((durationHours - h) * 60); return h > 0 ? `${h}ω ${m}λ` : `${m}λ` })() + : fmtMins(summary.duration_minutes) + const row1 = [ + { label: 'Διάρκεια Βάρδιας', value: durationLabel }, + { label: 'Μισθός / Ώρα', value: summary.hourly_rate_snapshot != null ? fmtEUR(summary.hourly_rate_snapshot) : '—' }, + { label: 'Πληρωμή Βάρδιας', value: summary.shift_pay != null ? fmtEUR(summary.shift_pay) : 'Δεν παρακολουθείται', accent: summary.shift_pay != null ? '#059669' : undefined }, + { label: 'Αρχικά Μετρητά', value: summary.starting_cash != null ? fmtEUR(summary.starting_cash) : '—' }, + ] + const row2 = [ + { label: 'Σύνολο Παραγγελιών', value: String(totalOrders) }, + { label: 'Εισπράξεις', value: fmtEUR(summary.total_collected), accent: '#2f9e5e' }, + { label: 'Προς Παράδοση', value: fmtEUR(summary.net_to_deliver), accent: '#3758c9' }, + { label: 'Εξ. Τραπεζιών', value: String(tablesServed) }, + ] + return ( +
+ {[row1, row2].map((row, ri) => ( +
+ {row.map(k => ( +
+
{k.label}
+
{k.value}
+
+ ))} +
+ ))}
- ))} -
+ ) + })()} {/* Colour legend */}