src/features/reports/reports-page.tsx
Linked repository file from this build.
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { ReportView, type Run } from "@/features/reports/report-view";
import { LoadingPreview } from "@/components/loading-preview";
import { AppShell } from "@/components/app-shell";
import { useReview } from "@/features/reviews/provider";
import { isCalendarMonth } from "@/programs/snap-negative/input-limits";
import {
aggregateReport,
countyLabel,
examinerLabel,
reportCsv,
type ReportFilters,
type ReportResponse,
} from "@/features/reports/report-data";
import { can } from "@/lib/auth/scope";
const FILTER_DEBOUNCE_MS = 300;
function reportQuery(filters: ReportFilters) {
const committed: ReportFilters = {};
if (filters.from && isCalendarMonth(filters.from)) committed.from = filters.from;
if (filters.through && isCalendarMonth(filters.through)) committed.through = filters.through;
if (filters.examiner) committed.examiner = filters.examiner;
if (filters.county) committed.county = filters.county;
return new URLSearchParams(committed as Record<string, string>).toString();
}
export function ReportsPage({
initial,
}: {
initial: { scope: string; data: ReportResponse; filters: ReportFilters };
}) {
const { actor } = useReview();
return (
<AppShell title="Reports">
<ReportsContent
key={JSON.stringify(actor)}
initial={initial.scope === JSON.stringify(actor) ? initial.data : undefined}
initialFilters={initial.filters}
/>
</AppShell>
);
}
function ReportsContent({
initial,
initialFilters,
}: {
initial?: ReportResponse;
initialFilters: ReportFilters;
}) {
const { actor, setRole, isSignedIn } = useReview();
const [data, setData] = useState<ReportResponse>(
initial ?? {
summary: aggregateReport([]),
examiners: [],
counties: [],
},
);
const [filters, setFilters] = useState(initialFilters);
const [error, setError] = useState("");
const [loading, setLoading] = useState(!initial);
const skipInitial = useRef(Boolean(initial));
const [loadedAt, setLoadedAt] = useState("");
const [refresh, setRefresh] = useState(0);
const [runs, setRuns] = useState<Run[]>([]);
const canView = can(actor, "report");
const from = filters.from && isCalendarMonth(filters.from) ? filters.from : "";
const through = filters.through && isCalendarMonth(filters.through) ? filters.through : "";
const invalidRange = !!from && !!through && from > through;
const filterQuery = reportQuery(filters);
const [loadedQuery, setLoadedQuery] = useState<string | null>(initial ? filterQuery : null);
const lastRefresh = useRef(refresh);
useEffect(() => {
if (skipInitial.current) {
setLoadedAt(new Date().toISOString());
skipInitial.current = false;
lastRefresh.current = refresh;
return;
}
if (!canView || invalidRange) return;
const controller = new AbortController();
const immediate = lastRefresh.current !== refresh;
lastRefresh.current = refresh;
async function load() {
setLoading(true);
setError("");
try {
const response = await fetch(`/api/reports?${filterQuery}`, {
cache: "no-store",
signal: controller.signal,
});
if (!response.ok) throw new Error("Unable to load reports. Please try Refresh.");
const result = (await response.json()) as ReportResponse;
if (!controller.signal.aborted) {
setData(result);
setLoadedQuery(filterQuery);
setLoadedAt(new Date().toISOString());
}
} catch (cause) {
if (!controller.signal.aborted)
setError(cause instanceof Error ? cause.message : "Unable to load reports.");
} finally {
if (!controller.signal.aborted) setLoading(false);
}
}
const timer = window.setTimeout(() => void load(), immediate ? 0 : FILTER_DEBOUNCE_MS);
return () => {
window.clearTimeout(timer);
controller.abort();
};
}, [refresh, filterQuery, canView, invalidRange]);
const { summary, examiners, counties } = data;
const pending = canView && !invalidRange && (loading || (!error && loadedQuery !== filterQuery));
const update = useCallback(
(patch: Partial<ReportFilters>) => setFilters((current) => ({ ...current, ...patch })),
[],
);
function runReport() {
const at = new Date().toISOString();
const scope = [
filters.from || filters.through
? `${filters.from || "First month"} through ${filters.through || "Latest month"}`
: "All sample months",
filters.examiner ? examinerLabel(filters.examiner) : "All examiners",
filters.county ? countyLabel(filters.county) : "All counties",
].join(" · ");
setRuns((previous) =>
[
{
id: crypto.randomUUID(),
at,
scope,
total: summary.total,
eligible: summary.eligible,
csv: reportCsv(summary, filters, at),
},
...previous,
].slice(0, 10),
);
}
return (
<ReportView
actor={actor}
isSignedIn={isSignedIn}
setRole={setRole}
loading={pending}
loadedAt={pending ? "" : loadedAt}
error={error}
canView={canView}
filters={filters}
update={update}
setFilters={setFilters}
examiners={examiners}
counties={counties}
invalidRange={invalidRange}
summary={summary}
runs={runs}
runReport={runReport}
onRefresh={() => setRefresh((value) => value + 1)}
/>
);
}
export function ReportsLoading() {
const { actor, isSignedIn } = useReview();
return (
<LoadingPreview label="Loading reports…">
<ReportView
actor={actor}
isSignedIn={isSignedIn}
setRole={() => {}}
loading
loadedAt=""
error=""
canView={true}
filters={{}}
update={() => {}}
setFilters={() => {}}
examiners={[]}
counties={[]}
invalidRange={false}
summary={aggregateReport([])}
runs={[]}
runReport={() => {}}
onRefresh={() => {}}
/>
</LoadingPreview>
);
}