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 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-10-10 00:24:35 +02:00
parent 84816fa466
commit 36dff2ff09
6 changed files with 59 additions and 6 deletions

View file

@ -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&amp;period=thisMonth');
});
it("says 'supplier' when only one supplier is owed", async () => {

View file

@ -204,7 +204,7 @@ export default async function FinancialLanding({
number -- and the same number the Suppliers page shows. */}
<Link
className="contents"
href={`/dashboard/financial/suppliers?site=${encodeURIComponent(scopeFallbackValue(scope))}`}
href={`/dashboard/financial/suppliers?site=${encodeURIComponent(scopeFallbackValue(scope))}&period=thisMonth`}
>
<KpiTile
label="Received this month"

View file

@ -11,11 +11,17 @@ import type { DocumentKind } from "@/lib/document-kind";
import { SuppliersView, type SupplierRow } from "./suppliers-view";
import { filterOpt, siteFilter, type SearchParams } from "@/lib/site-scope";
import { getSiteScope } from "@/lib/site-scope-server";
import { parseInitialPeriod } from "@/lib/supplier-period";
export const dynamic = "force-dynamic";
export default async function SuppliersPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
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={<AddSupplierButton />}
/>
<SuppliersView rows={rows} deliveryValues={deliveryValues} />
<SuppliersView rows={rows} deliveryValues={deliveryValues} initialPeriod={initialPeriod} />
</>
);
}

View file

@ -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<SortKey>("outstanding");
const [sortDir, setSortDir] = React.useState<SortDir>("desc");
const [period, setPeriod] = React.useState<PeriodFilter>("all");
const [period, setPeriod] = React.useState<PeriodFilter>(initialPeriod);
const [fromDate, setFromDate] = React.useState("");
const [toDate, setToDate] = React.useState("");

View file

@ -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");
});
});

View file

@ -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…" },
];
];
/**
* 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";
}