- New useReportsPeriod hook reads/writes period via ?from=&to=&period= URL params, default civil year, pure resolver exported for tests - New per-domain hooks: useHighlights, useTrends, useCompare, useCategoryZoom (stubs wired to useReportsPeriod, to be fleshed out in #71-#74) - Rewire legacy useReports to consume useReportsPeriod; keep backward-compat state shape (period/customDateFrom/customDateTo) so /reports tabs keep working - Mark useReports @deprecated pending removal in #76 - Tests: 7 new cases covering resolveReportsPeriod defaults, bookmarks, invalid inputs, preset resolution Fixes #70 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
import { useSearchParams } from "react-router-dom";
|
|
import type { DashboardPeriod } from "../shared/types";
|
|
import { computeDateRange } from "../utils/dateRange";
|
|
|
|
const VALID_PERIODS: readonly DashboardPeriod[] = [
|
|
"month",
|
|
"3months",
|
|
"6months",
|
|
"year",
|
|
"12months",
|
|
"all",
|
|
"custom",
|
|
];
|
|
|
|
function isValidPeriod(p: string | null): p is DashboardPeriod {
|
|
return p !== null && (VALID_PERIODS as readonly string[]).includes(p);
|
|
}
|
|
|
|
function isValidIsoDate(s: string | null): s is string {
|
|
return !!s && /^\d{4}-\d{2}-\d{2}$/.test(s);
|
|
}
|
|
|
|
function currentYearRange(today: Date = new Date()): { from: string; to: string } {
|
|
const year = today.getFullYear();
|
|
return { from: `${year}-01-01`, to: `${year}-12-31` };
|
|
}
|
|
|
|
/**
|
|
* Pure resolver used by the hook and unit tests. Exposed to keep the core
|
|
* logic hookless and testable without rendering a router.
|
|
*/
|
|
export function resolveReportsPeriod(
|
|
rawFrom: string | null,
|
|
rawTo: string | null,
|
|
rawPeriod: string | null,
|
|
today: Date = new Date(),
|
|
): { from: string; to: string; period: DashboardPeriod } {
|
|
if (isValidIsoDate(rawFrom) && isValidIsoDate(rawTo)) {
|
|
const p = isValidPeriod(rawPeriod) ? rawPeriod : "custom";
|
|
return { from: rawFrom, to: rawTo, period: p };
|
|
}
|
|
if (isValidPeriod(rawPeriod) && rawPeriod !== "custom") {
|
|
const range = computeDateRange(rawPeriod);
|
|
const { from: defaultFrom, to: defaultTo } = currentYearRange(today);
|
|
return {
|
|
from: range.dateFrom ?? defaultFrom,
|
|
to: range.dateTo ?? defaultTo,
|
|
period: rawPeriod,
|
|
};
|
|
}
|
|
const { from, to } = currentYearRange(today);
|
|
return { from, to, period: "custom" };
|
|
}
|
|
|
|
export interface UseReportsPeriodResult {
|
|
from: string;
|
|
to: string;
|
|
period: DashboardPeriod;
|
|
setPeriod: (period: DashboardPeriod) => void;
|
|
setCustomDates: (from: string, to: string) => void;
|
|
}
|
|
|
|
/**
|
|
* Reads/writes the active reporting period via the URL query string so it is
|
|
* bookmarkable and shared across the four report sub-routes.
|
|
*
|
|
* Defaults to the current civil year (Jan 1 → Dec 31).
|
|
*/
|
|
export function useReportsPeriod(): UseReportsPeriodResult {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
const rawPeriod = searchParams.get("period");
|
|
const rawFrom = searchParams.get("from");
|
|
const rawTo = searchParams.get("to");
|
|
|
|
const { from, to, period } = useMemo(
|
|
() => resolveReportsPeriod(rawFrom, rawTo, rawPeriod),
|
|
[rawPeriod, rawFrom, rawTo],
|
|
);
|
|
|
|
const setPeriod = useCallback(
|
|
(next: DashboardPeriod) => {
|
|
setSearchParams(
|
|
(prev) => {
|
|
const params = new URLSearchParams(prev);
|
|
if (next === "custom") {
|
|
params.set("period", "custom");
|
|
} else {
|
|
params.set("period", next);
|
|
params.delete("from");
|
|
params.delete("to");
|
|
}
|
|
return params;
|
|
},
|
|
{ replace: true },
|
|
);
|
|
},
|
|
[setSearchParams],
|
|
);
|
|
|
|
const setCustomDates = useCallback(
|
|
(nextFrom: string, nextTo: string) => {
|
|
setSearchParams(
|
|
(prev) => {
|
|
const params = new URLSearchParams(prev);
|
|
params.set("period", "custom");
|
|
params.set("from", nextFrom);
|
|
params.set("to", nextTo);
|
|
return params;
|
|
},
|
|
{ replace: true },
|
|
);
|
|
},
|
|
[setSearchParams],
|
|
);
|
|
|
|
return { from, to, period, setPeriod, setCustomDates };
|
|
}
|