Per-staff dashboards with orders served, average ticket, on-time rate, voided items, and customer feedback — rolled up nightly from your live order data, not from a separate POS export.
Staff performance in Ordering.Tools is a daily roll-up of every metric that ties a staff member to a customer or a shift: orders they served, items they fired, revenue they were responsible for, average ticket size, on-time vs late vs no-show shifts, voided items, and average customer feedback rating. Every metric comes from data the platform already captures (Order.assignedWaiterId, OrderFeedback, ClockEvent, StaffShiftAssignment). The roll-up runs nightly via the cron:nightly runner; managers can replay any window from the admin UI.
The point isn't surveillance — it's coaching. Per-staff dossiers surface the patterns that matter: who's consistently early; who has the highest avg ticket; who voids more than peers; who shows up on time even on a Saturday after a wedding. Pair this with the rota grid and managers know who to schedule for the lunch rush vs the late-night patio shift. The data is read-only for managers (no editing of past metrics); raw events live in the audit log if anyone needs to verify.
Every metric is computed from rows you already have — Order, OrderFeedback, ClockEvent, StaffShiftAssignment. No extra sensors, no separate POS export, no third-party time-tracking integration. Order.assignedWaiterId is the join key.
The roll-up runs at 03:00 UTC (cron:nightly) and writes one StaffMetricsDaily row per (venue, staff, date). If you need to recompute a window — say after a backfill — hit /api/admin/staff/metrics/replay with a from/to range; the upsert is idempotent.
We compare the staff's CLOCK_IN against their scheduled StaffShiftAssignment.startTime. Within 5 minutes = on-time; over 5 minutes = late; no clock-in by end of shift = no-show. Per-staff rolling averages surface the pattern, not just the bad night.
The metrics aren't editable — managers can't 'fix' a row to be more flattering. The raw events live in the audit log and are reproducible. If you spot a row that looks wrong, the replay endpoint recomputes it; if the recompute matches, the data is real.
Toggle enableStaffMetrics in Staff → Settings. The nightly cron starts emitting rolled-up metrics within 24 hours; the per-staff dossier tab in /admin/profile becomes visible to the staff member.
At 03:00 UTC the cron:nightly runner executes the staff-metrics step. It groups Order rows by assignedWaiterId for each enabled venue, joins ClockEvents for hours, aggregates OrderFeedback for ratings, and upserts one row per (venue, user, date).
Open Staff → Members → click a staff member. The dossier shows daily metrics over the last 30/90 days with sparklines for orders, revenue, and feedback average. Click any day to see the underlying StaffShiftAssignment + ClockEvents + Orders.
Each staff member sees their own metrics from the My Stats screen on the mobile waiter shell — read-only. Coaching conversations are framed by data both sides can see, not by anecdote.
Idempotent upsert — re-running the same window produces identical rows. The roll-up is part of the cron-system-migration grouped runner architecture; new metrics are added by extending one step, not creating a new cron line.
We compare the staff member's first CLOCK_IN of the day to the StaffShiftAssignment.startTime. Late threshold is configurable (default 5 minutes); no-show is no clock-in by end of shift. The classification writes onTimeShifts / lateShifts / noShowShifts counters.
OrderFeedback rows joined to Order.assignedWaiterId give per-staff customer ratings. The dossier shows feedbackAvg + feedbackCount; clicking through reveals individual comments (when present) so coaching has concrete examples.
If a backfill or a bug means past metrics are wrong, hit /api/admin/staff/metrics/replay with from/to. The endpoint re-runs the same logic the nightly cron uses; the upsert overwrites stale rows with fresh ones.
A waiter hired three months ago has feedbackAvg = 3.8 vs the team average of 4.4. Open their dossier — the bad ratings cluster on Saturday late-night shifts. The manager pairs them with a senior on those shifts; the average climbs to 4.3 over the next month.
Petar's noShowShifts has crept up over the last 30 days. Open the dossier — every no-show is a Sunday morning. Conversation: 'Sunday mornings are tough — let's move you to evenings'. The pattern resolves without firing anyone.
Two candidates for the head-server role. Their dossiers show identical avg tickets, but one has 92% on-time vs the other's 78%. The first gets the promotion; the second gets coaching on punctuality before being reconsidered.
A new kitchen hire has voidedItemCount 3× the team average. Most voids are wrong-cooked steaks. Conversation: 'Let's run through the doneness chart together' — voids drop to team average over two weeks.
Every venue has the server who upsells naturally and the one who doesn't. The avgTicketCents column ranks them. The top server's habits (recommending the chef's special, suggesting a side) get coached into the rest of the team.
Staff see their own stats and self-correct without manager intervention. A waiter notices their Tuesday avg ticket dropped — they start mentioning the new pasta dish; the average recovers. Improvement happens without a coaching meeting.
Most restaurant analytics platforms aggregate at the venue level — total covers, total revenue, total tickets — and stop there. The hard step is going from venue averages to per-staff insight, because that requires a staff-attribution column on every order. Ordering.Tools has had Order.assignedWaiterId since the waiter module shipped, so the per-staff roll-up is a SQL group-by, not a new instrumentation project. Add ClockEvent for hours, OrderFeedback for ratings, and StaffShiftAssignment for on-time classification, and you have a complete dossier with no new sensors.
Computing per-staff metrics live on every dashboard load means a 50-staff venue's home page does a 50-row aggregation across 30 days of orders — every page load. That's slow even with indexes, and it scales badly. Nightly roll-up writes one StaffMetricsDaily row per (venue, staff, date) and dashboards read those rows directly. P95 dossier load times drop from seconds to milliseconds, the database load drops by an order of magnitude, and the data is exactly as fresh as it needs to be (yesterday's numbers, computed at 03:00 UTC).
There's a tension between being lenient (30 minutes late = on-time) and being strict (1 minute late = late). Different cultures, different roles. Ordering.Tools defaults to 5 minutes — generous enough that traffic and bus-stop arithmetic don't tag good workers as late, strict enough that consistent 20-minute late starts get flagged. The threshold is venue-configurable. The classification is rolling — one bad day doesn't ruin a 30-day average — and time-off-approved no-shows don't count, by design.
Editable performance metrics breed distrust. If a manager can 'fix' a low-rated day to look better, staff stop trusting the dashboard and start asking 'what was the real number?'. Ordering.Tools exposes the metrics as read-only — the only way to change them is the replay endpoint that re-computes from raw events. Raw events themselves are append-only (ClockEvent has the MANUAL_ADJUST pattern; orders aren't editable post-completion). The dashboard is what the data says happened, not a manager's interpretation.
Per-staff dossiers powered by your existing order data. Premium feature, included in Staff Management.