From 36dff2ff09f193dafccca7931e19100053c66f4d Mon Sep 17 00:00:00 2001 From: DEls Date: Sat, 10 Oct 2026 00:24:35 +0200 Subject: [PATCH] fix(financial): open Suppliers on the period the tile linked to The "Received this month" tile linked to the Suppliers page, which always opened on "All", so the page showed a different number than the tile. Add a validated parseInitialPeriod helper, pass the parsed period through the page as initialPeriod, and have the tile link carry ?period=thisMonth. Co-Authored-By: Claude Opus 5.5 --- src/app/dashboard/financial/page.test.tsx | 2 +- src/app/dashboard/financial/page.tsx | 2 +- .../dashboard/financial/suppliers/page.tsx | 10 +++++-- .../financial/suppliers/suppliers-view.tsx | 5 +++- src/lib/supplier-period.test.ts | 26 +++++++++++++++++++ src/lib/supplier-period.ts | 20 +++++++++++++- 6 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/app/dashboard/financial/page.test.tsx b/src/app/dashboard/financial/page.test.tsx index 091f554..17e988e 100644 --- a/src/app/dashboard/financial/page.test.tsx +++ b/src/app/dashboard/financial/page.test.tsx @@ -188,7 +188,7 @@ it("counts only deliveries received this month, and names the suppliers owed", a // R400 + R200 this month; the R100 from September is out of the window. expect(tileValue(html, "Received this month")).toBe("R 600"); expect(tileSub(html, "Received this month")).toBe("Owed to 2 suppliers for this month"); - expect(html).toContain('/dashboard/financial/suppliers?site=site1'); + expect(html).toContain('/dashboard/financial/suppliers?site=site1&period=thisMonth'); }); it("says 'supplier' when only one supplier is owed", async () => { diff --git a/src/app/dashboard/financial/page.tsx b/src/app/dashboard/financial/page.tsx index b75ef47..45abe23 100644 --- a/src/app/dashboard/financial/page.tsx +++ b/src/app/dashboard/financial/page.tsx @@ -204,7 +204,7 @@ export default async function FinancialLanding({ number -- and the same number the Suppliers page shows. */} }) { - const scope = await getSiteScope(await searchParams, "financial"); + const params = await searchParams; + const scope = await getSiteScope(params, "financial"); + // A deep link such as the Financial landing's "Received this month" tile + // must open on the period it linked to, or the number on the tile and the + // number on this page disagree. + const initialPeriod = parseInitialPeriod(params.period); const pb = await pbAdmin(); const [suppliers, pos, items, deliveryLines, candidates] = await Promise.all([ @@ -91,7 +97,7 @@ export default async function SuppliersPage({ searchParams }: { searchParams: Pr subtitle="What's owed to whom — computed live from purchase orders and deliveries." right={} /> - + ); } diff --git a/src/app/dashboard/financial/suppliers/suppliers-view.tsx b/src/app/dashboard/financial/suppliers/suppliers-view.tsx index 65c82e9..527964a 100644 --- a/src/app/dashboard/financial/suppliers/suppliers-view.tsx +++ b/src/app/dashboard/financial/suppliers/suppliers-view.tsx @@ -105,14 +105,17 @@ function periodSubLabel(period: PeriodFilter): string { export function SuppliersView({ rows, deliveryValues, + initialPeriod = "all", }: { rows: SupplierRow[]; deliveryValues: DeliveryValue[]; + /** Pre-selected filter, e.g. from `?period=thisMonth`. Unvalidated here. */ + initialPeriod?: PeriodFilter; }) { const [search, setSearch] = React.useState(""); const [sortKey, setSortKey] = React.useState("outstanding"); const [sortDir, setSortDir] = React.useState("desc"); - const [period, setPeriod] = React.useState("all"); + const [period, setPeriod] = React.useState(initialPeriod); const [fromDate, setFromDate] = React.useState(""); const [toDate, setToDate] = React.useState(""); diff --git a/src/lib/supplier-period.test.ts b/src/lib/supplier-period.test.ts index 655dc4c..9e16c07 100644 --- a/src/lib/supplier-period.test.ts +++ b/src/lib/supplier-period.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { isInPeriod, + parseInitialPeriod, resolvePeriodWindow, type PeriodWindow, } from "./supplier-period"; @@ -87,4 +88,29 @@ describe("isInPeriod", () => { expect(isInPeriod("2026-08-31T00:00:00.000Z", sept)).toBe(false); expect(isInPeriod("2026-11-01T00:00:00.000Z", sept)).toBe(false); }); +}); + +describe("parseInitialPeriod", () => { + it("accepts a known period value", () => { + expect(parseInitialPeriod("thisMonth")).toBe("thisMonth"); + expect(parseInitialPeriod("week2")).toBe("week2"); + }); + + it("takes the first value when given an array", () => { + expect(parseInitialPeriod(["thisWeek", "thisMonth"])).toBe("thisWeek"); + }); + + it("falls back to all for custom, which needs date inputs", () => { + expect(parseInitialPeriod("custom")).toBe("all"); + }); + + it("falls back to all for anything unrecognised", () => { + expect(parseInitialPeriod("nonsense")).toBe("all"); + expect(parseInitialPeriod(["nonsense"])).toBe("all"); + expect(parseInitialPeriod("__proto__")).toBe("all"); + }); + + it("falls back to all when absent", () => { + expect(parseInitialPeriod(undefined)).toBe("all"); + }); }); \ No newline at end of file diff --git a/src/lib/supplier-period.ts b/src/lib/supplier-period.ts index 45830ca..8086db9 100644 --- a/src/lib/supplier-period.ts +++ b/src/lib/supplier-period.ts @@ -168,4 +168,22 @@ export const PERIOD_OPTIONS: { value: PeriodFilter; label: string }[] = [ { value: "month2", label: "Month 2" }, { value: "month3", label: "Month 3" }, { value: "custom", label: "Custom range…" }, -]; \ No newline at end of file +]; + +/** + * Period a deep link (`?period=`) may preselect. "custom" is excluded: + * it needs a from/to date pair the URL does not carry, so it stays a + * deliberate choice in the UI. Anything not on the known list -- garbage, + * a prototype key, a missing param -- falls back to "all" instead of + * reaching the filter state unchecked. + */ +const INITIAL_PERIODS = PERIOD_OPTIONS + .map((option) => option.value) + .filter((value): value is PeriodFilter => value !== "custom"); + +export function parseInitialPeriod(raw: string | string[] | undefined): PeriodFilter { + const value = Array.isArray(raw) ? raw[0] : raw; + return value && INITIAL_PERIODS.includes(value as PeriodFilter) + ? (value as PeriodFilter) + : "all"; +} \ No newline at end of file