feat: named registers, X/Z reports, daily rollup, fix drawerSessionId

Registers:
- New register table with location association
- CRUD service + API routes (POST/GET/PATCH/DELETE /registers)
- Drawer sessions now link to a register via registerId
- Register ID persisted in localStorage per device

X/Z Reports:
- ReportService with getDrawerReport() (X or Z depending on session state)
- Z report auto-displayed on drawer close in the drawer dialog
- X report (Current Shift Report) button on open drawer view
- Report shows: sales summary, payment breakdown, discounts, cash accountability, adjustments

Daily Rollup:
- ReportService.getDailyReport() aggregates all sessions at a location for a date
- New /reports/daily endpoint with locationId + date params
- Frontend daily report page with date picker, location selector, session breakdown

Critical Fix:
- drawerSessionId is now populated on transactions when completing (was never set before)
- This enables accurate per-drawer reporting and cash accountability

Migration 0044: register table, drawer_session.register_id column

Tests: 14 new (register CRUD, drawer report X/Z, drawerSessionId population, daily rollup, register-drawer link)
Full suite: 367 passed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-05 02:21:55 +00:00
parent be8cc0ad8b
commit 7d9aeaf188
17 changed files with 1062 additions and 6 deletions

View File

@@ -181,6 +181,8 @@ export {
DrawerOpenSchema,
DrawerCloseSchema,
DrawerAdjustmentSchema,
RegisterCreateSchema,
RegisterUpdateSchema,
} from './pos.schema.js'
export type {
TransactionCreateInput,
@@ -192,6 +194,8 @@ export type {
DrawerOpenInput,
DrawerCloseInput,
DrawerAdjustmentInput,
RegisterCreateInput,
RegisterUpdateInput,
} from './pos.schema.js'
export { LogLevel, AppConfigUpdateSchema } from './config.schema.js'

View File

@@ -100,6 +100,7 @@ export type DiscountUpdateInput = z.infer<typeof DiscountUpdateSchema>
export const DrawerOpenSchema = z.object({
locationId: opt(z.string().uuid()),
registerId: opt(z.string().uuid()),
openingBalance: z.coerce.number().min(0),
})
export type DrawerOpenInput = z.infer<typeof DrawerOpenSchema>
@@ -119,3 +120,17 @@ export const DrawerCloseSchema = z.object({
notes: opt(z.string()),
})
export type DrawerCloseInput = z.infer<typeof DrawerCloseSchema>
// --- Register schemas ---
export const RegisterCreateSchema = z.object({
locationId: z.string().uuid(),
name: z.string().min(1).max(100),
})
export type RegisterCreateInput = z.infer<typeof RegisterCreateSchema>
export const RegisterUpdateSchema = z.object({
name: z.string().min(1).max(100).optional(),
isActive: z.boolean().optional(),
})
export type RegisterUpdateInput = z.infer<typeof RegisterUpdateSchema>