src/features/exports/exports-page.tsx
Linked repository file from this build.
"use client";
import { Switch } from "@/components/ui/switch";
import { LoadingPreview } from "@/components/loading-preview";
import { useEffect, useRef, useState } from "react";
import { Download } from "lucide-react";
import { AppShell } from "@/components/app-shell";
import { Button } from "@/components/ui/button";
import { useReview } from "@/features/reviews/provider";
import {
CURRENT_TEMPLATE_FNS_DISCLAIMER,
EPP_ADAPTER_DISCLAIMER,
} from "@/features/reviews/downstream";
import type { exportMonthSummary } from "@/features/exports/month-export";
export function ExportsPage({
initial,
}: {
initial: { scope: string; data: ReturnType<typeof exportMonthSummary> };
}) {
const { actor } = useReview();
return (
<AppShell title="Exports">
<ExportsContent
key={JSON.stringify(actor)}
initial={initial.scope === JSON.stringify(actor) ? initial.data : undefined}
/>
</AppShell>
);
}
export function ExportsLoading() {
return (
<LoadingPreview label="Loading exports…">
<ExportsContent preview />
</LoadingPreview>
);
}
function ExportsContent({
initial,
preview = false,
}: {
initial?: ReturnType<typeof exportMonthSummary>;
preview?: boolean;
}) {
const { actor } = useReview();
const [months, setMonths] = useState<ReturnType<typeof exportMonthSummary>>(initial ?? []);
const [selected, setSelected] = useState<string[]>([]);
const [loading, setLoading] = useState(!initial);
const skipInitial = useRef(Boolean(initial));
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [downloaded, setDownloaded] = useState("");
const kind = actor.role === "statistics" ? "fns" : "epp";
useEffect(() => {
if (preview) return;
if (skipInitial.current) {
skipInitial.current = false;
return;
}
const controller = new AbortController();
void fetch("/api/exports", { cache: "no-store", signal: controller.signal })
.then(async (response) => {
const data = await response.json();
if (!response.ok) throw new Error(data.error);
setMonths(data.months);
})
.catch((cause) => {
if (!controller.signal.aborted)
setError(cause instanceof Error ? cause.message : "Unable to load export months.");
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [preview]);
const eligible = months
.filter((row) => selected.includes(row.month))
.reduce((sum, row) => sum + row.eligible, 0);
async function download() {
setBusy(true);
setError("");
setDownloaded("");
try {
const query = new URLSearchParams({ kind });
[...selected].sort().forEach((month) => query.append("month", month));
const response = await fetch(`/api/exports?${query}`, { cache: "no-store" });
const data = await response.json();
if (!response.ok) throw new Error(data.error);
const url = URL.createObjectURL(new Blob([data.content], { type: data.mimeType }));
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = data.filename;
anchor.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
setDownloaded(`Downloaded ${data.count} completed reviews for ${data.months.join(", ")}.`);
} catch (cause) {
setError(cause instanceof Error ? cause.message : "Export failed.");
} finally {
setBusy(false);
}
}
return (
<div className="mx-auto w-full max-w-6xl space-y-6">
<header className="space-y-2">
<h1 className="text-2xl font-semibold tracking-tight">Exports</h1>
<p className="max-w-3xl text-sm text-muted-foreground">
Choose the sample months to include in your{" "}
{kind === "fns" ? "transmittal file" : "EPP adapter TSV"}. Released reviews and
manager-approved drops can enter the transmittal file. Staff upload the file through the
QCS web portal.
</p>
</header>
<section
className="rounded-xl border bg-card p-5 shadow-sm"
aria-label="Sample month selection"
>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="font-semibold">Sample months</h2>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={loading || !months.length}
onClick={() => setSelected(months.map((row) => row.month))}
>
Select all months
</Button>
<Button
variant="ghost"
size="sm"
disabled={!selected.length}
onClick={() => setSelected([])}
>
Clear selection
</Button>
</div>
</div>
<div>
{loading || months.length ? (
<LoadingPreview loading={loading} label="Loading sample months…">
<div className="overflow-x-auto">
<table className="w-full text-sm" aria-label="Export eligibility by sample month">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="py-3 pr-4 font-medium">Include month</th>
<th className="px-3 text-right font-medium">Eligible</th>
<th className="px-3 text-right font-medium">Incomplete</th>
<th className="px-3 text-right font-medium">Dropped</th>
<th className="px-3 text-right font-medium">Awaiting release</th>
</tr>
</thead>
<tbody>
{(loading
? Array.from({ length: 6 }, (_, i) => ({
month: `2000-0${i + 1}`,
eligible: 0,
incomplete: 0,
dropped: 0,
awaitingRelease: 0,
}))
: months
).map((row) => (
<tr key={row.month} className="border-b last:border-0 hover:bg-muted/40">
<td className="py-3 pr-4">
<label className="flex cursor-pointer items-center gap-3">
<Switch
aria-label={row.month}
checked={selected.includes(row.month)}
onCheckedChange={(checked) =>
setSelected((previous) =>
checked
? [...previous, row.month]
: previous.filter((month) => month !== row.month),
)
}
/>
{row.month}
</label>
</td>
<td className="px-3 text-right font-semibold tabular-nums">
{row.eligible}
</td>
<td className="px-3 text-right tabular-nums text-muted-foreground">
{row.incomplete}
</td>
<td className="px-3 text-right tabular-nums text-muted-foreground">
{row.dropped}
</td>
<td className="px-3 text-right tabular-nums text-muted-foreground">
{row.awaitingRelease}
</td>
</tr>
))}
</tbody>
</table>
</div>
</LoadingPreview>
) : (
<p className="py-6 text-muted-foreground">No sample months in your review scope.</p>
)}
</div>
<p className="mt-4 text-xs leading-relaxed text-muted-foreground">
The transmittal file includes manager-approved drops. Program Error Summary excludes
drops. Eligibility is checked again when you download.
</p>
</section>
<section className="space-y-4 rounded-xl border bg-muted/20 p-5" aria-label="Export download">
<div>
<h2 className="font-semibold">
{kind === "fns" ? "Transmittal file" : "EPP adapter TSV"}
</h2>
<p className="mt-1 text-sm text-muted-foreground">
{kind === "fns" ? CURRENT_TEMPLATE_FNS_DISCLAIMER : EPP_ADAPTER_DISCLAIMER}
</p>
</div>
<p className="text-sm">
<span className="font-medium">Included months:</span>{" "}
{selected.length ? [...selected].sort().join(", ") : "Select sample months above."}
</p>
<div className="flex flex-wrap items-center gap-4">
<Button onClick={() => void download()} disabled={loading || busy || !eligible}>
<Download className="size-4" />
{busy ? "Preparing TSV…" : `Download ${kind.toUpperCase()} TSV`}
</Button>
<span className="text-sm text-muted-foreground">
{eligible} eligible {eligible === 1 ? "review" : "reviews"}
</span>
</div>
{downloaded ? (
<p role="status" className="text-sm">
{downloaded}
</p>
) : null}
</section>
{error ? (
<p
role="alert"
className="rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive"
>
{error}
</p>
) : null}
</div>
);
}