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.
lib/db/shift_report.ts — 3 DAOs:
cashRefundsForSession — best-effort attribution: sums refunds where the parent order had any cash payment (per scope decision; exact split-tender attribution would need the R9.2 sync refund-line breakdown that doesn't exist yet).salesAggregatesForSession — gross sales + refund totals + taxes in one transaction (paid + partial_refund orders for gross/tax; all states for refund totals).posConfigNameForSession — pulls the register's display name (e.g. "POS-TK-1") for the header.lib/db/cafe_sessions.ts — createOpenNixSession now wraps the INSERT in a transaction that first reads MAX(shift_number) for the register, then inserts with shiftNumber: nextShift. The MAX-then-INSERT pair sees a consistent snapshot so two cashiers can't collide on the same shift number.lib/db/schema.ts — cafeSessions.shiftNumber added (nullable integer).lib/native-shift-report.ts — pure-fn buildShiftReport assembles ShiftReportData from raw numbers. Owns the math: expectedCash = starting + cashPayments − cashRefunds + paidIn − paidOut, netSales = gross − refunds − discounts, and the storeLabel composition (tenant + shop + parens-code, deduped for single-shop tenants where shop = tenant).components/receipt/shift-report.tsx — pure React component, inline-styled, 80mm width. Three sections: header (shift # / store / POS / opened / closed) · Cash drawer (8 rows + Difference) · Sales summary (Gross/Refunds/Discounts/Net + per-method rows + Taxes) · footer timestamp.app/api/cafe/sessions/[sessionId]/shift-report/route.tsx — GET handler. Fans out 7 DAOs in parallel, resolves user labels (tenant_users + commerce.pin_identities fallback), formats timestamps in shopTimezone || tenant.timezone (U7.1 pattern), renders the component server-side via renderToString, wraps in an HTML envelope with @page size: 80mm auto, returns text/html.app/(authed)/pos/_components/shift-report-print-button.tsx — shared client component. Fetch HTML → inject hidden iframe → iframe.contentWindow.print() → cleanup. Same flow as openPrintWindow in lib/native-receipt.ts (so the NIX Cash extension's hostname-guard, which already exempts *.nixtech.app, covers this too).starter-close-shift-dialog.tsx — button mounts next to existing PDF + CSV buttons (Starter close-shift dialog).(pos-fullscreen)/pos/register/[configId]/lockable-shell.tsx — button mounts in Pro close-shift inline JSX.app/(authed)/pos/sessions/sessions-client.tsx — button mounts per row (desktop table + mobile card) in Session History.app/(authed)/pos/_components/pos-more-menu.tsx — inline "Print Shift Report" item with the same state-machine pattern as the existing PDF + Daily Sale items.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.
| ✓ | 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).
Loading…
window.print() — matches order-receipt pattern, cross-platform / cross-driver, future-proof for native-shell intercept (Electron / Tauri / Capacitor) without refactor.node migrate.js against prod Supabase from local. Verify cafe.sessions.shift_number backfilled across get-coffee + demo + lumiere (sums per pos_config).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.window.print(). Out of scope for v1; current path matches existing order-receipt UX.