src/features/reviews/workspace-header.tsx
Linked repository file from this build.
"use client";
import type { SaveStatus } from "./save-status-label";
import { NavigationLink as Link } from "@/components/navigation-link";
import { AppointmentPicker } from "./appointment-picker";
import { useReview } from "./provider";
import { returnPath } from "./return-path";
import { fiscalYear } from "./review-key";
import { useSession } from "@/lib/auth/client";
import { ArrowLeft } from "lucide-react";
import { can } from "@/lib/auth/scope";
import { SaveStatusButton } from "./save-status";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { fieldClass, fieldMultilineClass, Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { SnapReview } from "@/programs/snap-negative/types";
import {
roleLabels,
dropKinds,
dropKindLabels,
type Actor,
workflowLabels,
type ReviewIdentityPatch,
type DemoRole,
type ReviewOutcome,
} from "./types";
import {
outcomeLabel,
WorkspaceFinishActions,
type PendingFinish,
} from "./workspace-finish-actions";
export function WorkspaceHeader({
review,
actor,
onApplyRules,
onSave,
onClearAll,
onResetAll,
onUndo,
editable,
resetAvailable,
saveStatus,
lastSavedAt,
onRoleChange,
pendingFinish,
onPendingFinishChange,
onFinishExecute,
}: {
review: SnapReview;
actor: Actor;
onApplyRules(): void;
onSave(): void;
onClearAll(): void;
onResetAll(): void;
onUndo(): void;
editable: boolean;
resetAvailable: boolean;
saveStatus: SaveStatus;
lastSavedAt: string | null;
onRoleChange(role: DemoRole): void;
pendingFinish: PendingFinish | null;
onPendingFinishChange(pending: PendingFinish | null): void;
onFinishExecute(
payload: Parameters<typeof WorkspaceFinishActions>[0]["onExecute"] extends (
value: infer T,
) => unknown
? T
: never,
): Promise<boolean>;
}) {
const { isSignedIn } = useSession();
const { navigate } = useReview();
return (
<header className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 px-1 py-2">
<Button asChild variant="outline" size="sm" className="h-8 px-2">
<Link
href="/dashboard"
onNavigate={(event) => {
event.preventDefault();
void navigate(returnPath());
}}
>
<ArrowLeft className="size-3.5" aria-hidden />
Return
</Link>
</Button>
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<h1 className="text-3xl font-semibold tracking-tight">{review.reviewNumber}</h1>
<span className="text-sm text-muted-foreground">FFY {fiscalYear(review.sampleMonth)}</span>
<Badge variant="secondary">{workflowLabels[review.state]}</Badge>
<Badge variant="outline">Outcome: {outcomeLabel(review.outcome)}</Badge>
</div>
<div className="ml-auto flex min-w-0 flex-wrap items-center justify-end gap-2">
{editable && (
<>
<Button size="sm" variant="ghost" onClick={onUndo}>
Undo
</Button>
<Button size="sm" variant="ghost" onClick={onClearAll}>
Clear all
</Button>
<Button size="sm" variant="ghost" disabled={!resetAvailable} onClick={onResetAll}>
Reset all
</Button>
</>
)}
<SaveStatusButton saveStatus={saveStatus} lastSavedAt={lastSavedAt} onSave={onSave} />
{(actor.role === "admin" || actor.role === "statistics") && (
<Button size="sm" variant="ghost" className="h-8" onClick={onApplyRules}>
Apply new rules
</Button>
)}
{!isSignedIn && (
<>
<Label htmlFor="demo-role" className="sr-only">
Acting role
</Label>
<Select value={actor.role} onValueChange={(value) => onRoleChange(value as DemoRole)}>
<SelectTrigger id="demo-role" className="h-8 w-[9.5rem] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{(Object.keys(roleLabels) as DemoRole[]).map((role) => (
<SelectItem key={role} value={role}>
{roleLabels[role]}
</SelectItem>
))}
</SelectContent>
</Select>
</>
)}
<WorkspaceFinishActions
review={review}
actor={actor}
pendingFinish={pendingFinish}
onPendingFinishChange={onPendingFinishChange}
onExecute={onFinishExecute}
/>
</div>
</header>
);
}
export function WorkspaceGeneral({
mobileOpen,
review,
actor,
editable,
onAppointmentChange,
onIdentityChange,
onNegativeActionChange,
onOutcomeChange,
onDropKindChange,
onClear,
onReset,
resetAvailable,
}: {
review: SnapReview;
actor: Actor;
editable: boolean;
onAppointmentChange(value: string): void;
onIdentityChange(patch: ReviewIdentityPatch): void;
onNegativeActionChange(patch: {
negativeAction?: string;
noticeDate?: string;
actionDate?: string;
}): void;
onOutcomeChange(outcome: ReviewOutcome): void;
onDropKindChange(value: NonNullable<SnapReview["dropKind"]>): void;
onClear(): void;
onReset(): void;
resetAvailable: boolean;
mobileOpen: boolean;
}) {
const pecLine = review.pecDecision
? `PEC ${review.pecDecision.decision}${review.pecDecision.policyNote ? ` — ${review.pecDecision.policyNote}` : ""}`
: review.conditionalNotes
? `Conditions: ${review.conditionalNotes}`
: null;
return (
<section
id="review-general"
aria-labelledby="review-general-heading"
className={`${mobileOpen ? "flex" : "hidden"} max-h-[35dvh] w-full shrink-0 flex-col gap-2 overflow-y-auto border-b p-3 lg:mr-3 lg:flex lg:max-h-none lg:w-80 lg:overflow-y-auto lg:border-b-0 lg:border-r`}
>
<div className="flex shrink-0 items-center justify-between gap-1">
<h2 id="review-general-heading" className="min-w-0 text-base font-semibold tracking-tight">
Identifying Information (II)
</h2>
<div className="flex shrink-0 gap-1">
<Button
size="sm"
variant="ghost"
className="h-6 px-2 text-xs"
disabled={!editable}
onClick={onClear}
>
Clear
</Button>
<Button
size="sm"
variant="ghost"
className="h-6 px-2 text-xs"
disabled={!editable || !resetAvailable}
onClick={onReset}
>
Reset
</Button>
</div>
</div>
{pecLine && <p className="text-xs text-muted-foreground">{pecLine}</p>}
<div className="grid grid-cols-2 gap-2 text-sm">
<CompactField
id="client-name"
label="A. Client name"
value={review.clientName}
disabled={!editable}
className="col-span-2"
onCommit={(value) =>
value !== review.clientName && onIdentityChange({ clientName: value })
}
/>
<CompactField
id="case-number"
label="2. Case number"
value={review.caseNumber}
disabled={!editable}
onCommit={(value) =>
value !== review.caseNumber && onIdentityChange({ caseNumber: value })
}
/>
<CompactField
type="month"
id="sample-month"
label="4. Sample month"
value={review.sampleMonth}
disabled={!editable}
onCommit={(value) =>
value !== review.sampleMonth && onIdentityChange({ sampleMonth: value })
}
/>
<div className="space-y-0.5">
<Label htmlFor="review-outcome" className="text-xs text-muted-foreground">
Outcome
</Label>
<Select
value={review.outcome || "unset"}
disabled={!editable}
onValueChange={(value) =>
onOutcomeChange(value === "unset" ? "" : (value as ReviewOutcome))
}
>
<SelectTrigger id="review-outcome" className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="unset">Unset</SelectItem>
<SelectItem value="clean">Valid</SelectItem>
<SelectItem value="error">Invalid</SelectItem>
<SelectItem value="drop">Drop</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-0.5">
<Label htmlFor="negative-action" className="text-xs text-muted-foreground">
8. Negative action
</Label>
<Select
value={review.facts.negativeAction || undefined}
disabled={!editable}
onValueChange={(value) => onNegativeActionChange({ negativeAction: value })}
>
<SelectTrigger id="negative-action" className="h-8 w-full">
<SelectValue placeholder="Select action" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Denial">Denial</SelectItem>
<SelectItem value="Termination">Termination</SelectItem>
<SelectItem value="Suspension">Suspension</SelectItem>
</SelectContent>
</Select>
</div>
<CompactField
type="date"
id="notice-date"
label="6. Notice date"
value={review.facts.noticeDate}
disabled={!editable}
onCommit={(value) =>
value !== review.facts.noticeDate && onNegativeActionChange({ noticeDate: value })
}
/>
<CompactField
type="date"
id="action-date"
label="7. Action date"
value={review.facts.actionDate}
disabled={!editable}
onCommit={(value) =>
value !== review.facts.actionDate && onNegativeActionChange({ actionDate: value })
}
/>
<CompactField
id="county"
label="County"
value={review.county}
disabled={!editable}
onCommit={(value) => value !== review.county && onIdentityChange({ county: value })}
/>
<CompactField
id="district"
label="District"
value={review.district}
disabled={!editable}
onCommit={(value) => value !== review.district && onIdentityChange({ district: value })}
/>
<CompactField
id="local-agency"
label="3. CAO"
value={review.localAgency}
disabled={!editable}
className="col-span-2"
onCommit={(value) =>
value !== review.localAgency && onIdentityChange({ localAgency: value })
}
/>
<CompactField
id="address"
label="C. Mailing address"
value={review.address}
disabled={!editable}
multiline
className="col-span-2"
onCommit={(value) => value !== review.address && onIdentityChange({ address: value })}
/>
</div>
{review.outcome === "drop" && (
<label className="space-y-1 text-sm">
Drop kind
<select
aria-label="Drop kind"
className={fieldClass}
disabled={!editable}
value={review.dropKind ?? ""}
onChange={(event) =>
onDropKindChange(event.target.value as NonNullable<SnapReview["dropKind"]>)
}
>
<option value="" disabled>
Select drop kind
</option>
{dropKinds.map((kind) => (
<option key={kind} value={kind}>
{dropKindLabels[kind]}
</option>
))}
</select>
</label>
)}
<div className="space-y-1">
<Label htmlFor="appointment" className="text-xs text-muted-foreground">
Appointment (Eastern time)
</Label>
<AppointmentPicker
value={review.appointment ?? ""}
editable={can(actor, "schedule")}
onChange={onAppointmentChange}
/>
</div>
</section>
);
}
function CompactField({
type = "text",
id,
label,
value,
disabled,
className,
multiline,
onCommit,
}: {
id: string;
label: string;
value: string;
disabled?: boolean;
className?: string;
multiline?: boolean;
onCommit(value: string): void;
type?: "text" | "date" | "month";
}) {
return (
<div
className={`space-y-0.5 ${type === "date" || type === "month" ? "col-span-2" : ""} ${className ?? ""}`}
>
<Label htmlFor={id} className="text-xs text-muted-foreground">
{label}
</Label>
{multiline ? (
<textarea
id={id}
rows={2}
value={value}
readOnly={disabled}
className={`${fieldMultilineClass} h-16 resize-none break-words read-only:border-dashed`}
onChange={(event) => onCommit(event.target.value)}
/>
) : (
<Input
id={id}
type={type}
value={value}
readOnly={disabled}
className="read-only:border-dashed"
onChange={(event) => onCommit(event.target.value)}
/>
)}
</div>
);
}