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 */}