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:
parent
84816fa466
commit
36dff2ff09
6 changed files with 59 additions and 6 deletions
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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";
|
||||
}
|
||||
Loading…
Reference in a new issue