diff --git a/src/app/api/clock/cash-float/route.test.ts b/src/app/api/clock/cash-float/route.test.ts index 9c76de3..1c1fac5 100644 --- a/src/app/api/clock/cash-float/route.test.ts +++ b/src/app/api/clock/cash-float/route.test.ts @@ -24,7 +24,7 @@ const card = (over: Record = {}) => ({ limit: 100000, balance: 234000, spentThisMonth: 60000, - lastSlip: null, + spentSinceTopup: 0, low: false, hasOpening: true, ...over, diff --git a/src/app/dashboard/management/cash-floats/cash-floats-view.test.tsx b/src/app/dashboard/management/cash-floats/cash-floats-view.test.tsx index 226da48..47ea943 100644 --- a/src/app/dashboard/management/cash-floats/cash-floats-view.test.tsx +++ b/src/app/dashboard/management/cash-floats/cash-floats-view.test.tsx @@ -27,7 +27,7 @@ const card = (over: Partial = {}): CashFloatCard => ({ limit: 100000, balance: 234000, spentThisMonth: 60000, - lastSlip: { date: "2026-10-20T08:00:00Z", supplier: "Build Supply" }, + spentSinceTopup: 25000, low: false, hasOpening: true, ...over, diff --git a/src/app/dashboard/management/cash-floats/cash-floats-view.tsx b/src/app/dashboard/management/cash-floats/cash-floats-view.tsx index f41b337..8ebf87c 100644 --- a/src/app/dashboard/management/cash-floats/cash-floats-view.tsx +++ b/src/app/dashboard/management/cash-floats/cash-floats-view.tsx @@ -116,7 +116,7 @@ export function CashFloatsView({ cards, users }: { cards: CashFloatCard[]; users
- + {!cards.length && } {visible.map(card => ( @@ -131,7 +131,7 @@ export function CashFloatsView({ cards, users }: { cards: CashFloatCard[]; users - +
Cash float cards, their holders and their balances
CardHolderAssignedBalanceSpent this monthLast slipStatusActions
CardHolderAssignedBalanceSpent this monthSpent since top-upStatusActions
No cards yet. Add one to get started.
{card.assignedOn || "—"} {formatCents(card.balance)} {formatCents(card.spentThisMonth)}{card.lastSlip ? `${card.lastSlip.date.slice(0, 10)}${card.lastSlip.supplier ? ` · ${card.lastSlip.supplier}` : ""}` : "—"}{formatCents(card.spentSinceTopup)} {card.low ? Under the limit diff --git a/src/lib/cash-float-server.test.ts b/src/lib/cash-float-server.test.ts index 94ade4e..7a4f948 100644 --- a/src/lib/cash-float-server.test.ts +++ b/src/lib/cash-float-server.test.ts @@ -138,7 +138,7 @@ describe("loadCashFloatCards", () => { expect(cards[1].hasOpening).toBe(true); }); - it("counts what was spent this month and finds the last slip", async () => { + it("counts what was spent this month and since the last top-up", async () => { const { pb } = fakePb({ users: [holder()], cash_float_log: [ @@ -149,7 +149,22 @@ describe("loadCashFloatCards", () => { }); const [card] = await loadCashFloatCards(pb, new Date("2026-10-24T00:00:00Z")); expect(card.spentThisMonth).toBe(60000); - expect(card.lastSlip).toEqual({ date: "2026-10-20T08:00:00Z", supplier: "Build Supply" }); + // Never topped up: counts from the opening on the 1st. + expect(card.spentSinceTopup).toBe(60000); + }); + + it("counts only the slips after the latest top-up", async () => { + const { pb } = fakePb({ + users: [holder()], + cash_float_log: [ + log({ id: "l1", kind: "opening", amount: 1500, occurred_on: "2026-10-01T08:00:00Z" }), + log({ id: "l2", amount: -600, occurred_on: "2026-10-05T08:00:00Z" }), + log({ id: "l3", kind: "topup", amount: 1000, occurred_on: "2026-10-10T08:00:00Z" }), + log({ id: "l4", amount: -250, occurred_on: "2026-10-20T08:00:00Z" }), + ], + }); + const [card] = await loadCashFloatCards(pb, new Date("2026-10-24T00:00:00Z")); + expect(card.spentSinceTopup).toBe(25000); }); it("leaves unticked users out", async () => { diff --git a/src/lib/cash-float-server.ts b/src/lib/cash-float-server.ts index 7e0e99b..ccaac3a 100644 --- a/src/lib/cash-float-server.ts +++ b/src/lib/cash-float-server.ts @@ -39,7 +39,8 @@ export interface CashFloatCard { balance: number; /** What has been spent off the card this month, in cents, positive. */ spentThisMonth: number; - lastSlip: { date: string; supplier: string } | null; + /** What has been spent since the last top-up (or the opening, if never topped up), in cents, positive. */ + spentSinceTopup: number; low: boolean; /** A card with no opening row has not been set up yet. */ hasOpening: boolean; @@ -140,7 +141,7 @@ export async function cardHeldBy(pb: PocketBase, userId: string): Promise<{ card * Every card, lowest balance first. * * The balance of every card comes from the one log read; the per-card - * figures (spent this month, the last slip) are picked out of the same rows. + * figures (spent this month, spent since the last top-up) are picked out of the same rows. */ export async function loadCashFloatCards(pb: PocketBase, now: Date = new Date()): Promise { const [holders, logRows] = await Promise.all([loadHolders(pb), loadLogRows(pb)]); @@ -157,9 +158,15 @@ export async function loadCashFloatCards(pb: PocketBase, now: Date = new Date()) const limit = lowLimit(holder); const slips = cardRows.filter((row) => row.kind === "slip"); const thisMonth = slips.filter((row) => rowMonth(row) === month); - const lastSlip = slips - .slice() - .sort((a, b) => (b.occurred_on ?? "").localeCompare(a.occurred_on ?? ""))[0]; + // The reference point is the latest top-up; a card never topped up counts from its opening. + const latest = (kind: string) => + cardRows + .filter((row) => row.kind === kind) + .map((row) => row.occurred_on ?? "") + .sort() + .pop(); + const since = latest("topup") ?? latest("opening") ?? ""; + const sinceTopup = slips.filter((row) => (row.occurred_on ?? "") >= since); return { userId: holder.id, holder: holderName(holder), @@ -169,7 +176,7 @@ export async function loadCashFloatCards(pb: PocketBase, now: Date = new Date()) limit, balance, spentThisMonth: thisMonth.reduce((total, row) => total + Math.abs(row.amount), 0), - lastSlip: lastSlip ? { date: lastSlip.occurred_on ?? "", supplier: lastSlip.supplier_name ?? "" } : null, + spentSinceTopup: sinceTopup.reduce((total, row) => total + Math.abs(row.amount), 0), low: isLow(balance, limit), hasOpening: cardRows.some((row) => row.kind === "opening"), };