feat(shift): confirm-before-close with X-report + split tickets vs subscriptions; fix dark <select>
CI / check (push) Failing after 31s
CI / check (push) Failing after 31s
Three changes: 1. Confirm-before-close. The header shift button closed the shift directly — a stray click would sign the irreversible Z-report. It now opens a confirm modal showing the live X-report (takings split by source + expected drawer) with Cancel / End-shift. Opening a shift stays immediate (no such risk). 2. Split takings by SOURCE. The report separates Tickets (transient) from Subscriptions (monthly sales + a subscriber's out-of-window charge), so the operator sees subscriber money apart from ticket money. Buckets are derived from the signed payment payload flags (subscriptionSale / subscriptionWindowCharge) and always reconcile to cash + card (a payment with neither flag is a ticket). Computed in #summariseWindow, carried on the signed shift_z_report payload, and shown in the X-report, the close modal, the shift history detail, and the printed Z-report. Reports predating the fields default subscription to 0 (ticket absorbs the whole take), so old shifts still reconcile. 3. Fix dark-theme native <select> popups rendering WHITE on WebKitGTK (the Tauri Linux WebView): set color-scheme dark/light on <html> per theme + explicit <option> colours, so the OS-drawn dropdown list follows the theme. Verified the split on a read-only DB copy: tickets 0, subscriptions 10,200 (10,000 sale + 200 out-of-window), reconciles to cash+card. build+lint 14/14, i18n parity (sq+en). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -51,6 +51,10 @@ export interface ShiftSummary {
|
||||
readonly cardTotalMinor: number;
|
||||
readonly currency: string | null;
|
||||
readonly paymentCount: number;
|
||||
readonly ticketTotalMinor: number;
|
||||
readonly subscriptionTotalMinor: number;
|
||||
readonly subscriptionSalesMinor: number;
|
||||
readonly subscriptionWindowMinor: number;
|
||||
readonly openingFloatMinor: number;
|
||||
readonly cashAddedMinor: number;
|
||||
readonly cashRemovedMinor: number;
|
||||
@@ -65,6 +69,15 @@ export interface ShiftReport {
|
||||
readonly cardTotalMinor: number;
|
||||
readonly currency: string | null;
|
||||
readonly paymentCount: number;
|
||||
// --- Takings split by SOURCE (cash+card combined; the drawer cash/card stay above) ---
|
||||
/** Transient TICKET money (the default — any payment not flagged subscription). */
|
||||
readonly ticketTotalMinor: number;
|
||||
/** All SUBSCRIBER money = monthly sales + out-of-window charges. */
|
||||
readonly subscriptionTotalMinor: number;
|
||||
/** Subscription SALES only (the prepaid monthly/period fee). */
|
||||
readonly subscriptionSalesMinor: number;
|
||||
/** Subscriber OUT-OF-WINDOW transient-tariff charges only. */
|
||||
readonly subscriptionWindowMinor: number;
|
||||
// --- Drawer (physical cash till; carries across shifts) ---
|
||||
/** Cash in the drawer at shift start = prior shift's expected closing drawer. */
|
||||
readonly openingFloatMinor: number;
|
||||
@@ -160,6 +173,10 @@ export class ShiftService {
|
||||
cashTotalMinor?: number;
|
||||
cardTotalMinor?: number;
|
||||
paymentCount?: number;
|
||||
ticketTotalMinor?: number;
|
||||
subscriptionTotalMinor?: number;
|
||||
subscriptionSalesMinor?: number;
|
||||
subscriptionWindowMinor?: number;
|
||||
openingFloatMinor?: number;
|
||||
cashAddedMinor?: number;
|
||||
cashRemovedMinor?: number;
|
||||
@@ -180,6 +197,16 @@ export class ShiftService {
|
||||
cardTotalMinor: pl.cardTotalMinor ?? 0,
|
||||
currency: pl.currency ?? null,
|
||||
paymentCount: pl.paymentCount ?? 0,
|
||||
// Split-by-source fields (added 2026-06-21). Old reports lack them → default the
|
||||
// subscription buckets to 0 and let ticket absorb the whole take, so the buckets
|
||||
// still reconcile to cash+card for a pre-split shift.
|
||||
subscriptionSalesMinor: pl.subscriptionSalesMinor ?? 0,
|
||||
subscriptionWindowMinor: pl.subscriptionWindowMinor ?? 0,
|
||||
subscriptionTotalMinor:
|
||||
pl.subscriptionTotalMinor ?? (pl.subscriptionSalesMinor ?? 0) + (pl.subscriptionWindowMinor ?? 0),
|
||||
ticketTotalMinor:
|
||||
pl.ticketTotalMinor ??
|
||||
(pl.cashTotalMinor ?? 0) + (pl.cardTotalMinor ?? 0) - (pl.subscriptionTotalMinor ?? 0),
|
||||
openingFloatMinor: pl.openingFloatMinor ?? 0,
|
||||
cashAddedMinor: pl.cashAddedMinor ?? 0,
|
||||
cashRemovedMinor: pl.cashRemovedMinor ?? 0,
|
||||
@@ -348,14 +375,29 @@ export class ShiftService {
|
||||
|
||||
let cashTotalMinor = 0;
|
||||
let cardTotalMinor = 0;
|
||||
// Split by SOURCE: subscription SALES (the prepaid fee), subscriber OUT-OF-WINDOW
|
||||
// charges, and everything else = transient TICKET money. Both subscriber kinds roll
|
||||
// up into subscriptionTotal; the rest is ticketTotal. The flags ride the signed
|
||||
// payment payload (subscriptionSale / subscriptionWindowCharge — see pay-station +
|
||||
// the subscription sale path).
|
||||
let subscriptionSalesMinor = 0;
|
||||
let subscriptionWindowMinor = 0;
|
||||
let currency: string | null = null;
|
||||
for (const p of payments) {
|
||||
const pl = (p.payload ?? {}) as LedgerPayload;
|
||||
const pl = (p.payload ?? {}) as LedgerPayload & {
|
||||
subscriptionSale?: boolean;
|
||||
subscriptionWindowCharge?: boolean;
|
||||
};
|
||||
const amt = typeof pl.amountMinor === "number" ? pl.amountMinor : 0;
|
||||
if (pl.tender === "card") cardTotalMinor += amt;
|
||||
else cashTotalMinor += amt;
|
||||
if (pl.subscriptionSale === true) subscriptionSalesMinor += amt;
|
||||
else if (pl.subscriptionWindowCharge === true) subscriptionWindowMinor += amt;
|
||||
// (else → transient ticket; derived below as total − subscription)
|
||||
if (pl.currency) currency = pl.currency;
|
||||
}
|
||||
const subscriptionTotalMinor = subscriptionSalesMinor + subscriptionWindowMinor;
|
||||
const ticketTotalMinor = cashTotalMinor + cardTotalMinor - subscriptionTotalMinor;
|
||||
|
||||
// --- Drawer figures ---
|
||||
// Opening float was fixed on shift_open (inherited from the chain at start);
|
||||
@@ -403,6 +445,10 @@ export class ShiftService {
|
||||
cardTotalMinor,
|
||||
currency,
|
||||
paymentCount: payments.length,
|
||||
ticketTotalMinor,
|
||||
subscriptionTotalMinor,
|
||||
subscriptionSalesMinor,
|
||||
subscriptionWindowMinor,
|
||||
openingFloatMinor,
|
||||
cashAddedMinor,
|
||||
cashRemovedMinor,
|
||||
@@ -437,6 +483,10 @@ export class ShiftService {
|
||||
cardTotalMinor,
|
||||
currency,
|
||||
paymentCount,
|
||||
ticketTotalMinor,
|
||||
subscriptionTotalMinor,
|
||||
subscriptionSalesMinor,
|
||||
subscriptionWindowMinor,
|
||||
openingFloatMinor,
|
||||
cashAddedMinor,
|
||||
cashRemovedMinor,
|
||||
@@ -455,6 +505,10 @@ export class ShiftService {
|
||||
cardTotalMinor,
|
||||
currency: currency ?? undefined,
|
||||
paymentCount,
|
||||
ticketTotalMinor,
|
||||
subscriptionTotalMinor,
|
||||
subscriptionSalesMinor,
|
||||
subscriptionWindowMinor,
|
||||
openingFloatMinor,
|
||||
cashAddedMinor,
|
||||
cashRemovedMinor,
|
||||
@@ -492,6 +546,12 @@ export class ShiftService {
|
||||
`Para në dorë: ${money(r.cashTotalMinor)} ${cur}`,
|
||||
`Kartë: ${money(r.cardTotalMinor)} ${cur}`,
|
||||
"",
|
||||
"-- Arkëtime sipas burimit --",
|
||||
`Bileta: ${money(r.ticketTotalMinor)} ${cur}`,
|
||||
`Abonime: ${money(r.subscriptionTotalMinor)} ${cur}`,
|
||||
` shitje: ${money(r.subscriptionSalesMinor)} ${cur}`,
|
||||
` jashtë orarit: ${money(r.subscriptionWindowMinor)} ${cur}`,
|
||||
"",
|
||||
"-- Arka --",
|
||||
`Fillimi (kusur): ${money(r.openingFloatMinor)} ${cur}`,
|
||||
`Para të marra: ${money(r.cashTotalMinor)} ${cur}`,
|
||||
|
||||
Reference in New Issue
Block a user