src/features/reviews/downstream-panel.tsx
Linked repository file from this build.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { fieldClass, fieldMultilineClass, Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { SnapReview } from "@/programs/snap-negative/types";
import type { ReviewCommand } from "./commands";
import { memoEligibility, PEC_ROUTING_PENDING } from "./downstream";
import { memoTypeLabels, type Actor, type CaoResponse } from "./types";
export function DownstreamPanel({
review,
actor,
perform,
flush,
}: {
review: SnapReview;
actor: Actor;
perform(command: ReviewCommand): boolean;
flush(): Promise<boolean>;
}) {
const currentMemo = review.memos?.at(-1);
const [draft, setDraft] = useState<string | null>(null);
const [deadline, setDeadline] = useState<string | null>(null);
const [status, setStatus] = useState<CaoResponse["status"]>(
review.caoResponse?.status ?? "agree",
);
const [rootCause, setRootCause] = useState(review.caoResponse?.rootCause ?? "");
const [note, setNote] = useState(review.caoResponse?.note ?? "");
const [returnReason, setReturnReason] = useState("");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const eligibility = memoEligibility(review, actor);
const canRespond = actor.role === "program_manager" && review.state === "awaiting_cao_response";
async function save(command: ReviewCommand) {
setError("");
setBusy(true);
try {
if (!perform(command)) return;
if (!(await flush()))
setError("The change could not be saved. Retry after the review syncs.");
} catch (cause) {
setError(cause instanceof Error ? cause.message : "The action failed.");
} finally {
setBusy(false);
}
}
return (
<div className="space-y-6" aria-busy={busy}>
{error && (
<p role="alert" className="text-sm text-destructive">
{error}
</p>
)}
<section className="space-y-3 rounded-lg border p-4" aria-label="Memo">
<h2 className="font-semibold">Memo</h2>
<p className="text-sm text-muted-foreground">{eligibility.message}</p>
{currentMemo && (
<>
<p className="text-sm">
{currentMemo.type ? memoTypeLabels[currentMemo.type] : PEC_ROUTING_PENDING}
</p>
<Label htmlFor="cao-memo">Letter to the CAO</Label>
<textarea
id="cao-memo"
className={`${fieldMultilineClass} min-h-60 p-3`}
value={draft ?? currentMemo.body}
readOnly={!eligibility.canCreate || busy}
onChange={(event) => setDraft(event.target.value)}
/>
{currentMemo.outboundAt && (
<p className="text-sm">
Outbound status recorded {new Date(currentMemo.outboundAt).toLocaleString()}.
Delivery is handled outside this app.
</p>
)}
{eligibility.canCreate && (
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
disabled={busy || !(draft ?? currentMemo.body).trim()}
onClick={() => void save({ type: "memo", body: draft ?? currentMemo.body })}
>
Save memo
</Button>
<Button
size="sm"
disabled={
busy || !currentMemo.type || (draft !== null && draft !== currentMemo.body)
}
onClick={() => void save({ type: "approve_memo" })}
>
Approve memo
</Button>
</div>
)}
</>
)}
{actor.role === "program_manager" &&
review.state === "awaiting_manager" &&
review.outcome !== "drop" && (
<div className="space-y-2 border-t pt-3">
<Label htmlFor="manager-return">Reason for return to examiner</Label>
<textarea
id="manager-return"
className={fieldMultilineClass}
value={returnReason}
onChange={(event) => setReturnReason(event.target.value)}
/>
<Button
size="sm"
variant="outline"
disabled={busy || !returnReason.trim()}
onClick={() =>
void save({ type: "transition", action: "request_changes", note: returnReason })
}
>
Return to examiner
</Button>
</div>
)}
</section>
{review.outcome !== "drop" && (
<section className="space-y-3 rounded-lg border p-4" aria-label="Error summary reporting">
<h2 className="font-semibold">Error summary reporting</h2>
{review.errorSummaryReleased ? (
<p className="text-sm">Released to Program Error Summary and the transmittal file.</p>
) : (
<p className="text-sm text-muted-foreground">
Release follows CAO agreement with a root cause, or no response after the deadline.
The program manager must approve release.
</p>
)}
<div className="flex flex-wrap items-end gap-2">
<div className="space-y-1">
<Label htmlFor="cao-deadline">Response deadline</Label>
<Input
id="cao-deadline"
type="date"
value={deadline ?? review.caoResponseDeadline ?? ""}
disabled={!canRespond || busy}
onChange={(event) => setDeadline(event.target.value)}
/>
</div>
{canRespond && (
<Button
size="sm"
variant="outline"
disabled={busy || !(deadline ?? review.caoResponseDeadline)}
onClick={() =>
void save({
type: "cao_deadline",
value: deadline ?? review.caoResponseDeadline ?? "",
})
}
>
Save deadline
</Button>
)}
</div>
<Label htmlFor="cao-response">CAO response</Label>
<select
id="cao-response"
className={fieldClass}
value={status}
disabled={!canRespond || busy}
onChange={(event) => setStatus(event.target.value as CaoResponse["status"])}
>
<option value="agree">Agree</option>
<option value="dispute">Dispute</option>
<option value="no_response">No response</option>
</select>
{status === "agree" && (
<>
<Label htmlFor="cao-root-cause">CAO-reported root cause</Label>
<textarea
id="cao-root-cause"
className={fieldMultilineClass}
value={rootCause}
disabled={!canRespond || busy}
onChange={(event) => setRootCause(event.target.value)}
/>
</>
)}
<Label htmlFor="cao-response-note">Response or follow-up note</Label>
<textarea
id="cao-response-note"
className={fieldMultilineClass}
value={note}
disabled={!canRespond || busy}
onChange={(event) => setNote(event.target.value)}
/>
{status === "dispute" && (
<p className="text-sm text-muted-foreground">
Update the deadline for follow-up. Reassignment after a dispute needs management
confirmation.
</p>
)}
{canRespond && (
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
disabled={busy}
onClick={() => void save({ type: "cao_response", status, rootCause, note })}
>
Save response
</Button>
<Button
size="sm"
disabled={busy || !review.caoResponse}
onClick={() => void save({ type: "release_summary" })}
>
Release to reporting
</Button>
</div>
)}
</section>
)}
</div>
);
}