src/features/reports/report-data.ts
Linked repository file from this build.
import { escapeCsvCell } from "@/lib/csv";
import { workflowLabels } from "@/features/reviews/types";
import { demoRoster } from "@/lib/auth/roster";
import { PA_LOCATIONS } from "@/programs/snap-negative/seed-generator-locations";
import {
elementCodeChoices,
natureChoicesForElement,
} from "@/programs/snap-negative/field-catalog";
import type { SnapReview, ReviewVariance } from "@/programs/snap-negative/types";
/** Compact report inputs contain no identity, notes, documents, or computation snapshots. */
export type ReportRow = Pick<SnapReview, "sampleMonth" | "examiner" | "county" | "state"> & {
eligible: "clean" | "error" | null;
variances: ReviewVariance[];
};
export interface ReportResponse {
summary: ReportSummary;
examiners: string[];
counties: string[];
}
export interface ReportFilters {
from?: string;
through?: string;
examiner?: string;
county?: string;
}
/** Set the initial report range from Pennsylvania's current fiscal year. */
export function currentFfyFilters(asOf = new Date()) {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
year: "numeric",
month: "numeric",
}).formatToParts(asOf);
const year = Number(parts.find((part) => part.type === "year")!.value);
const month = Number(parts.find((part) => part.type === "month")!.value);
const ffy = year + (month >= 10 ? 1 : 0);
return { from: `${ffy - 1}-10`, through: `${ffy}-09` };
}
export interface ReportCounts {
total: number;
approved: number;
completed: number;
clean: number;
error: number;
eligible: number;
ineligibleApproved: number;
}
export interface ReportGroup extends ReportCounts {
key: string;
label: string;
}
export interface ErrorCodeGroup {
element: string;
nature: string;
elementLabel: string;
natureLabel: string;
reviews: number;
}
export interface ReportSummary extends ReportCounts {
byErrorCode: ErrorCodeGroup[];
byState: ReportGroup[];
byMonth: ReportGroup[];
byExaminer: ReportGroup[];
byCounty: ReportGroup[];
}
const countyNames = new Map(PA_LOCATIONS.map(({ county, countyName }) => [county, countyName]));
export function countyLabel(county: string) {
return countyNames.get(county) ?? (county || "Unspecified county");
}
export function examinerLabel(examiner: string) {
return demoRoster.find((actor) => actor.staffId === examiner)?.name ?? (examiner || "Unassigned");
}
function monthLabel(month: string) {
if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(month)) return month || "Unspecified month";
return new Date(`${month}-15T12:00:00Z`).toLocaleDateString("en-US", {
month: "long",
year: "numeric",
timeZone: "UTC",
});
}
function emptyCounts(): ReportCounts {
return {
total: 0,
approved: 0,
completed: 0,
clean: 0,
error: 0,
eligible: 0,
ineligibleApproved: 0,
};
}
function addCounts(target: ReportCounts, source: ReportCounts) {
for (const key of Object.keys(source) as (keyof ReportCounts)[]) target[key] += source[key];
}
/** Input records must already be scoped to the signed-in reader by the server. */
export function aggregateReport(reviews: ReportRow[], filters: ReportFilters = {}): ReportSummary {
const summary: ReportSummary = {
...emptyCounts(),
byErrorCode: [],
byState: [],
byMonth: [],
byExaminer: [],
byCounty: [],
};
const errorCodes = new Map<string, ErrorCodeGroup>();
const states = new Map<string, ReportGroup>(
Object.entries(workflowLabels).map(([key, label]) => [key, { key, label, ...emptyCounts() }]),
);
const months = new Map<string, ReportGroup>();
const examiners = new Map<string, ReportGroup>();
const counties = new Map<string, ReportGroup>();
function addGroup(
groups: Map<string, ReportGroup>,
key: string,
label: () => string,
counts: ReportCounts,
) {
const group = groups.get(key) ?? { key, label: label(), ...emptyCounts() };
addCounts(group, counts);
groups.set(key, group);
}
for (const review of reviews) {
const month = review.sampleMonth.slice(0, 7);
if (
(filters.from && month < filters.from) ||
(filters.through && month > filters.through) ||
(filters.examiner && review.examiner !== filters.examiner) ||
(filters.county && review.county !== filters.county)
)
continue;
const counts: ReportCounts = {
total: 1,
approved: Number(review.state === "released_to_reporting"),
completed: Number(review.state === "released_to_reporting" || review.state === "dropped"),
clean: Number(review.eligible === "clean"),
error: Number(review.eligible === "error"),
eligible: Number(review.eligible !== null),
ineligibleApproved: Number(
review.state === "awaiting_cao_response" && review.eligible === null,
),
};
if (counts.error) {
const pairs = (review.variances ?? [])
.slice(0, 1)
.map(({ element, nature }) => ({ element: element.trim(), nature: nature.trim() }))
.filter(({ element, nature }) => element || nature);
if (!pairs.length) pairs.push({ element: "", nature: "" });
for (const key of new Set(
pairs.map(({ element, nature }) => JSON.stringify([element, nature])),
)) {
const [element, nature] = JSON.parse(key) as [string, string];
const group = errorCodes.get(key) ?? {
element,
nature,
elementLabel:
elementCodeChoices.find((choice) => choice.value === element)?.label ??
(element ? "Unknown element" : "Not coded"),
natureLabel:
natureChoicesForElement(element)
.find((choice) => choice.value === nature)
?.label.replace(/^\d+\s*[-–]\s*/, "") ?? (nature ? "Unknown nature" : "Not coded"),
reviews: 0,
};
group.reviews++;
errorCodes.set(key, group);
}
}
addCounts(summary, counts);
addGroup(states, review.state, () => workflowLabels[review.state], counts);
addGroup(months, month, () => monthLabel(month), counts);
addGroup(examiners, review.examiner, () => examinerLabel(review.examiner), counts);
addGroup(counties, review.county, () => countyLabel(review.county), counts);
}
summary.byErrorCode = [...errorCodes.values()].sort(
(a, b) =>
b.reviews - a.reviews ||
a.element.localeCompare(b.element) ||
a.nature.localeCompare(b.nature),
);
summary.byState = [...states.values()];
summary.byMonth = [...months.values()].sort((a, b) => a.key.localeCompare(b.key));
summary.byExaminer = [...examiners.values()].sort((a, b) => a.label.localeCompare(b.label));
summary.byCounty = [...counties.values()].sort((a, b) => a.label.localeCompare(b.label));
return summary;
}
/** Current-template internal report run; this is not the FNS-245 record export. */
export function reportCsv(
summary: ReportSummary,
filters: ReportFilters,
generatedAt: string,
): string {
const headings = [
"Section",
"Key",
"Label",
"Reviews",
"Released",
"Completed",
"Valid",
"Invalid",
"Error summary released",
"Awaiting release",
"Element code",
"Nature code",
];
const rows: string[][] = [headings];
function metadata(key: string, value: string) {
rows.push(["Metadata", key, value, ...Array<string>(7).fill("")]);
}
metadata("Report", "Program Error Summary");
metadata("Generated at", generatedAt);
metadata("Sample month from", filters.from || "All months");
metadata("Sample month through", filters.through || "All months");
metadata(
"Examiner",
filters.examiner ? `${filters.examiner} · ${examinerLabel(filters.examiner)}` : "All examiners",
);
metadata(
"County",
filters.county ? `${filters.county} · ${countyLabel(filters.county)}` : "All counties",
);
function row(section: string, key: string, label: string, counts: ReportCounts) {
rows.push([
section,
key,
label,
...[
counts.total,
counts.approved,
counts.completed,
counts.clean,
counts.error,
counts.eligible,
counts.ineligibleApproved,
].map(String),
]);
}
row("Summary", "all", "All matching reviews", summary);
for (const [section, groups] of [
["Workflow state", summary.byState],
["Sample month", summary.byMonth],
["Examiner", summary.byExaminer],
["County", summary.byCounty],
] as const) {
for (const group of groups) row(section, group.key, group.label, group);
}
for (const group of summary.byErrorCode) {
row(
"Error summary codes",
`${group.element || "uncoded"}/${group.nature || "uncoded"}`,
`${group.elementLabel} · ${group.natureLabel}`,
{
total: group.reviews,
approved: group.reviews,
completed: group.reviews,
clean: 0,
error: group.reviews,
eligible: group.reviews,
ineligibleApproved: 0,
},
);
rows[rows.length - 1].push(group.element, group.nature);
}
for (const values of rows) while (values.length < headings.length) values.push("");
// Quoting handles commas/newlines; an apostrophe also keeps staff-supplied text inert in spreadsheets.
const cell = (value: string) => escapeCsvCell(/^[\s]*[=+@-]/.test(value) ? `'${value}` : value);
return rows.map((values) => values.map(cell).join(",")).join("\r\n") + "\r\n";
}