← Back to gallery

U8 — Thermal Shift Report LOCAL

2026-05-27 Gate 1. Item #13 on Narong's test-run doc: "Print Shift Report from POS interface → Receipt Printer". A narrow 80mm thermal receipt summarizing the cashier's shift — distinct from U7's full A4 PDF. Prints via HTML iframe + window.print() (same path as order receipts) so the existing thermal-printer driver setup transfers and any future native-shell wrapper can silently intercept.

Summary

Status
16/16 local · typecheck clean · awaiting Gate 1 approval
Repos
nix-cafe + nix-outdoor-sales-backend (migration only)
Files
1 new migration · 8 nix-cafe files (3 new + 5 modified) · ~600 LOC net
Migration
Add cafe.sessions.shift_number INTEGER (nullable) + backfill via ROW_NUMBER() OVER (PARTITION BY pos_config_id ORDER BY opened_at, id) + supporting index. Local backfill: 364 rows updated, max shift_number = 91 on lumiere's busiest register.
Source
Notion: #13 on the 5-22-2026 Test-Run doc. Single spec image attached showing the receipt layout.

What ships

Data layer

Builder + component + route

UI surfaces — 3 mounts

Sample rendered shift report (local data shape)

Generated via renderToString(<ShiftReport data={data} />) with Narong's spec-image numbers (Get Coffee TK / Toul Kork / POS-TK-1, shift #44, 8:18 PM open, 5:55 AM close, $9.75 gross, $2.75 cash + $7.00 ABA/KHQR). Open inline below or in a new tab.

16/16 local checks

Migration .ts adds shift_number INT + ROW_NUMBER backfill + supporting index
migrate.js registry has U8 entry after U6
schema.ts: cafeSessions.shiftNumber declared (nullable integer)
createOpenNixSession sets shift_number = MAX(+1) inside a transaction
Postgres: shift_number backfilled (364 rows, max 91) on local lumiere
shift_report.ts: cashRefundsForSession + salesAggregatesForSession + posConfigNameForSession exported; EXISTS-join for best-effort cash attribution
native-shift-report.ts: buildShiftReport computes expectedCash + netSales + storeLabel composition
shift-report.tsx: 3 section headings + all 13 spec rows + 5 testids + 80mm width
GET route fans out 7 DAOs in parallel + renderToString(<ShiftReport />) + @page 80mm + text/html response
ShiftReportPrintButton: iframe injection + srcdoc + window.print() + cleanup
Mount: Starter close-shift dialog (testid close-shift-print-starter)
Mount: Pro lockable shell (testid close-shift-print-pro)
Mount: Session History list — desktop table + mobile cards
Mount: pos-more-menu inline item with same state-machine as Print PDF + Daily Sale
DAO probe: cashRefunds + salesAggregates + posConfigName round-trip on real lumiere session (shift #91, $517.05 gross, $42 refunds, "Register 1" POS)
Component render probe: renderToString HTML contains all expected strings (title, shift number, store, POS, cash drawer + sales summary headings, $2.75 / $9.75 / $7.00, "ABA / KHQR")

Typecheck: npx tsc --noEmit on nix-cafe → 0 errors. Route file is .tsx since it uses JSX (renderToString).

Schema after migration (local Postgres)

Loading…

Decisions locked (from scope)

Gate 2 plan

  1. Commit nix-outdoor-sales-backend (migration), then nix-cafe.
  2. node migrate.js against prod Supabase from local. Verify cafe.sessions.shift_number backfilled across get-coffee + demo + lumiere (sums per pos_config).
  3. Wait for CF auto-deploy on nix-cafe.
  4. test-u8-prod.mjs on lumiere: fetch the shift-report HTML for a real closed session, assert text contains all expected strings (title, section headings, store label, POS name, money values matching SQL aggregates), verify Content-Type is text/html, verify button testids render in all 4 surfaces.
  5. Regression sweep 51/51 (phase2-cafe-multishop solo).
  6. Publish prod gallery.

Out of scope (followups)