Live on prod 2026-05-20. Replaces the R5.3 monthly /cafe/reports screen with the
Loyverse-modeled At-a-glance Sales Summary specced in the v0.2 Notion doc on 2026-05-18.
Filter bar (date prev/next + popover w/ 8 presets + custom Start/End range, multi-store,
employee) + 5 KPI cards w/ prior-period delta (Gross sales / Refunds / Discounts / Net sales /
Cups sold — replaces Gross Profit per spec) +
Sales by Hours area chart. Existing sections (Ranking of POS, Payment Methods, Top Products,
Tax Summary) preserved and respect the new filter. Tier-agnostic — both Starter and Pro
render the same shell.
nix-cafe: fadb913 (initial ship) + 61d9970
(router.push fix — used absolute pathname so SPA nav updates the URL on prod). No backend
migration. New dep: recharts. Validated on get-coffee (Pro) + lumiere-coffee
(Starter).
/cafe/reports renders (no 5xx)?preset=yesterday, label updates)?preset=last_7_days)?preset=custom&from=…&to=…)aggregateReportSummary against prod DB/cafe/reports renders (5 KPIs render same shell)| test-phase1-prod.mjs | 11/11 |
| test-phase2-sso-outdoor-prod.mjs | 6/6 |
| test-phase2-cafe-multishop-prod.mjs | 6/6 |
| test-m1-prod.mjs | 10/10 |
| test-r7-prod.mjs | 14/14 |
| test-r8-prod.mjs | 4/4 |




?preset=last_7_days; KPIs + chart reflow


61d9970)First post-deploy run had 3 failures on client-side nav (date prev / preset / custom): the
filter bar used router.push(`?${searchParams}`) — a relative URL. On prod
Cloudflare Workers the SPA hop didn't actually update the URL on the first attempt.
Switched to usePathname() + absolute router.push(`${pathname}?${qs}`)
(the Next.js App Router documented pattern for filter bars). Re-ran prod test: 11/11.
Same code passed locally because Next dev resolves relative URLs differently than the
Workers runtime.
Took two rounds of test iteration to clear the remaining cold-worker hydration
flakes — added a hydration probe (open + close the popover before the prev-arrow click) and
used page.evaluate w/ a native value setter for type=date inputs.
All three nav assertions now use waitForURL for reliability.
Confirm with Narong:
• Cups Sold — implemented as SUM(qty − refunded_qty) across
ALL categories on paid + partial_refund order lines. Loyverse's metric is drink-only;
if Narong wants the same, we need a product category model (separate slice).
• Employee filter — single-select (cashier from cafe.orders.cashier_pin_id).
Loyverse's "All employees" UI is ambiguous about multi-select; staying single for v1.
• Time-of-day filter ("All day") — not implemented. Spec is ambiguous whether it's a slicer or just a default label.
Future polish (not requested):
• Multi-shop Ranking of POS — extends aggregateRevenueByPosConfig to accept
array. Today: ranking only filters when 0/1 shop selected.
• PDF + Excel exports — buttons still cosmetic stubs.
• Next slice — R2 Order Analysis: the second piece of the H5 spec (Odoo-pivot-style Total→Month→Store→Session tree). Separate slice.