src/features/reviews/workspace.tsx
Linked repository file from this build.
"use client";
import dynamic from "next/dynamic";
import { useCallback, useState } from "react";
import { LockKeyhole } from "lucide-react";
import { ReviewErrorToast } from "./review-error-toast";
import { LoadingPreview } from "@/components/loading-preview";
import { createImportTemplate } from "@/programs/snap-negative/review-template";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { canEdit } from "./commands";
import { fieldTab, opensGeneralDetails, scrollToField } from "./field-focus";
import { useFinishFlow } from "./use-finish-flow";
import { useReviewEdits } from "./use-review-edits";
import { receivedSnapshot, sectionCommand } from "./section-state";
import { useSession } from "@/lib/auth/client";
import { useReview } from "./provider";
import { WorkspaceActivity } from "./workspace-activity";
import { WorkspaceGeneral, WorkspaceHeader } from "./workspace-header";
import { WorkspaceHousehold } from "./workspace-household";
import { can } from "@/lib/auth/scope";
function TabLoading() {
return (
<p data-content-loading role="status" className="p-3 text-sm text-muted-foreground">
Loading…
</p>
);
}
const DownstreamPanel = dynamic(
() => import("./downstream-panel").then((module) => module.DownstreamPanel),
{ loading: TabLoading },
);
const ComputationGrid = dynamic(
() => import("./computation-grid").then((module) => module.ComputationGrid),
{ loading: TabLoading },
);
const WorkspaceDocumentsTab = dynamic(
() => import("./workspace-documents-tab").then((module) => module.WorkspaceDocumentsTab),
{ loading: TabLoading },
);
const WorkspaceReviewFields = dynamic(
() => import("./workspace-review-fields").then((module) => module.WorkspaceReviewFields),
{ loading: TabLoading },
);
const loadingReview = createImportTemplate();
function WorkspaceSession({ preview = false }: { preview?: boolean }) {
const {
review: savedReview,
actor,
setRole,
perform,
error,
dismissError,
storage,
flush,
saveStatus,
lastSavedAt,
undo,
} = useReview();
const review = preview ? loadingReview : savedReview;
const editable = preview || canEdit(review, actor);
const resetAvailable = Boolean(receivedSnapshot(review, actor));
function changeSection(
section: "all" | "household" | "computation" | "identity",
reset: boolean,
) {
if (
!window.confirm(
`${reset ? "Reset" : "Clear"} ${section === "all" ? "all review sections" : section}? You can undo this change.`,
)
)
return;
const command = sectionCommand(review, actor, section, reset);
if (command) perform(command, section !== "identity");
}
const [activeTab, setTab] = useState("household");
const [visitedTabs, setVisitedTabs] = useState<string[]>(["household"]);
const setActiveTab = useCallback((tab: string) => {
setVisitedTabs((tabs) => (tabs.includes(tab) ? tabs : [...tabs, tab]));
setTab(tab);
}, []);
const [generalOpen, setGeneralOpen] = useState(false);
const finish = useFinishFlow({ review, actor, perform, flush, setRole });
const edits = useReviewEdits({ review, actor, perform, flush, storage });
const focusField = useCallback(
(field: string) => {
setActiveTab(fieldTab(field));
if (opensGeneralDetails(field)) setGeneralOpen(true);
scrollToField(field);
},
[setActiveTab],
);
return (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden [&_input:read-only]:bg-muted/40 [&_input:read-only]:border-dashed [&_textarea:read-only]:bg-muted/40 [&_textarea:read-only]:border-dashed [&_input:read-only]:opacity-100 [&_input:read-only]:text-foreground [&_textarea:read-only]:opacity-100 [&_textarea:read-only]:text-foreground [&_button:disabled[role=combobox]]:opacity-100 [&_select:disabled]:opacity-100">
<div className="relative flex min-h-0 min-w-0 w-full flex-1 overflow-hidden">
<div
className="review-workspace mx-auto flex min-h-0 min-w-0 w-full flex-1 flex-col overflow-hidden"
data-tab={activeTab}
>
<WorkspaceHeader
review={review}
actor={actor}
onSave={() => void flush()}
editable={editable}
resetAvailable={resetAvailable}
onClearAll={() => changeSection("all", false)}
onResetAll={() => changeSection("all", true)}
onUndo={() => void undo()}
saveStatus={saveStatus}
lastSavedAt={lastSavedAt}
onApplyRules={() => {
perform({ type: "apply_rules" });
}}
onRoleChange={finish.changeRole}
pendingFinish={finish.pendingFinish}
onPendingFinishChange={finish.changePendingFinish}
onFinishExecute={finish.executeFinish}
/>
{!editable && (
<p className="my-2 flex shrink-0 items-center gap-2 text-sm">
<LockKeyhole className="size-4" aria-hidden />
Review fields are locked for your role or this workflow state. Values can be selected
and copied.
</p>
)}
<ReviewErrorToast error={preview ? "" : error} onDismiss={dismissError} />
<div className="workspace-session flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border p-3 lg:flex-row">
<button
type="button"
className="shrink-0 rounded border px-3 py-1 text-left text-sm lg:hidden"
aria-expanded={generalOpen}
aria-controls="review-general"
onClick={() => setGeneralOpen((open) => !open)}
>
General details
</button>
<WorkspaceGeneral
mobileOpen={generalOpen}
review={review}
actor={actor}
editable={editable}
onAppointmentChange={(value) => perform({ type: "appointment", value })}
onIdentityChange={edits.updateIdentity}
onNegativeActionChange={edits.updateNegativeAction}
onOutcomeChange={edits.updateOutcome}
onDropKindChange={(value) => perform({ type: "drop_kind", value })}
onClear={() => changeSection("identity", false)}
onReset={() => changeSection("identity", true)}
resetAvailable={resetAvailable}
/>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex min-h-0 min-w-0 flex-1 flex-col gap-2 overflow-hidden"
>
<TabsList className="segmented flex h-auto min-h-9 w-full shrink-0 flex-wrap justify-start [&>button]:h-8 [&>button]:flex-none">
<TabsTrigger value="household">Household</TabsTrigger>
<TabsTrigger value="computation">Computation</TabsTrigger>
<TabsTrigger value="review">Review</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
{can(actor, "seniorReview") && <TabsTrigger value="downstream">Memo</TabsTrigger>}
</TabsList>
<div className="workspace-tab-body relative min-h-0 flex-1 p-3">
<TabsContent
value="household"
className="absolute inset-3 mt-0 flex flex-col overflow-hidden data-[state=inactive]:hidden"
>
<WorkspaceHousehold
review={review}
editable={editable}
onMembersChange={edits.updateHousehold}
onClear={() => changeSection("household", false)}
onReset={resetAvailable ? () => changeSection("household", true) : undefined}
/>
</TabsContent>
<TabsContent
value="computation"
forceMount
className="absolute inset-3 mt-0 flex flex-col overflow-hidden data-[state=inactive]:hidden"
>
{visitedTabs.includes("computation") && (
<ComputationGrid
review={review}
editable={editable}
onSave={edits.saveScenario}
onApplyFinal={edits.applyFinal}
onClear={() => changeSection("computation", false)}
onReset={
resetAvailable ? () => changeSection("computation", true) : undefined
}
/>
)}
</TabsContent>
<TabsContent
value="review"
forceMount
className="absolute inset-3 mt-0 flex flex-col overflow-hidden data-[state=inactive]:hidden"
>
{visitedTabs.includes("review") && (
<WorkspaceReviewFields
review={review}
actor={actor}
editable={editable}
submitAttempted={finish.submitAttempted}
onAnswersChange={edits.updateAnswers}
onRefuseFinding={finish.refuseFinding}
onFocusField={focusField}
/>
)}
</TabsContent>
<TabsContent
value="downstream"
className="absolute inset-3 mt-0 flex flex-col overflow-hidden data-[state=inactive]:hidden"
>
<div className="min-h-0 flex-1 overflow-auto">
<DownstreamPanel
review={review}
actor={actor}
perform={perform}
flush={flush}
/>
</div>
</TabsContent>
<TabsContent
value="documents"
className="absolute inset-3 mt-0 overflow-auto data-[state=inactive]:hidden"
>
<WorkspaceDocumentsTab
review={review}
actor={actor}
storage={storage}
onUpload={edits.uploadDocument}
onUpdateDocument={edits.updateDocument}
/>
</TabsContent>
<TabsContent
value="activity"
className="absolute inset-3 mt-0 flex flex-col overflow-hidden data-[state=inactive]:hidden"
>
<WorkspaceActivity review={review} />
</TabsContent>
</div>
</Tabs>
</div>
</div>
</div>
</div>
);
}
function ReviewWorkspaceLoading() {
const { error, retrySave } = useReview();
const session = useSession();
if (!error && !session.error) return <WorkspaceLoading />;
return (
<div role="alert" className="space-y-3">
<p>{error || session.error}</p>
<button type="button" className="underline" onClick={() => retrySave()}>
Retry loading review
</button>
</div>
);
}
export function ReviewWorkspace() {
const { sessionVersion, reviewReady } = useReview();
if (!reviewReady) return <ReviewWorkspaceLoading />;
return <WorkspaceSession key={sessionVersion} />;
}
/** The editor is mounted with local placeholder data; no provider is created or loaded. */
export function WorkspaceLoading() {
return (
<LoadingPreview label="Loading saved review…" className="workspace-loading">
<WorkspaceSession preview />
</LoadingPreview>
);
}