src/features/reviews/use-review-edits.ts
Linked repository file from this build.
import { useCallback, useEffect, useRef } from "react";
import type { DocumentStorage } from "@/lib/documents/storage";
import type {
ReviewAnswers,
Scenario,
ScenarioId,
SnapReview,
} from "@/programs/snap-negative/types";
import { canEdit, type ReviewCommand } from "./commands";
import type { Actor, ReviewDocument, ReviewIdentityPatch, ReviewOutcome } from "./types";
/**
* Stable callbacks that turn workspace edits into review commands.
* Field edits stay local until Save; column saves, Final Results and documents save at once.
*/
export function useReviewEdits({
review,
actor,
perform,
flush,
storage,
}: {
review: SnapReview;
actor: Actor;
perform(command: ReviewCommand): boolean;
flush(): Promise<boolean>;
storage: DocumentStorage;
}) {
const reviewRef = useRef(review);
const actorRef = useRef(actor);
const answersRef = useRef(review.facts.answers);
useEffect(() => {
reviewRef.current = review;
actorRef.current = actor;
answersRef.current = review.facts.answers;
}, [review, actor]);
const updateAnswers = useCallback(
(patch: Partial<ReviewAnswers>) => {
const value = { ...answersRef.current, ...patch };
value.variances = value.variances.slice(0, 1);
if (perform({ type: "answers", value })) answersRef.current = value;
},
[perform],
);
const updateIdentity = useCallback(
(patch: ReviewIdentityPatch) => perform({ type: "identity", value: patch }),
[perform],
);
const updateNegativeAction = useCallback(
(patch: { negativeAction?: string; noticeDate?: string; actionDate?: string }) =>
perform({ type: "negative_action", value: patch }),
[perform],
);
const updateHousehold = useCallback(
(members: SnapReview["facts"]["members"]) => perform({ type: "household", value: members }),
[perform],
);
const updateOutcome = useCallback(
(outcome: ReviewOutcome) => perform({ type: "outcome", value: outcome }),
[perform],
);
const saveScenario = useCallback(
async (scenarioId: ScenarioId, value: Scenario) =>
perform({ type: "scenario", scenario: scenarioId, value }) && flush(),
[perform, flush],
);
const applyFinal = useCallback(
async () => perform({ type: "apply_final" }) && flush(),
[perform, flush],
);
async function uploadDocument(
file: File,
category: ReviewDocument["category"],
description: string,
name: string,
) {
if (!canEdit(reviewRef.current, actorRef.current))
throw new Error("Upload is not allowed for the current role or workflow state.");
const storageKey = await storage.put(file);
if (!canEdit(reviewRef.current, actorRef.current))
throw new Error(
"Upload no longer allowed. The role or workflow state changed during upload.",
);
const ok = perform({
type: "document",
document: {
id: crypto.randomUUID(),
filename: file.name,
name: name.trim(),
category,
description: description.trim(),
mimeType: "application/pdf",
size: file.size,
storageKey,
},
});
if (!ok) throw new Error("Unable to attach document to the review.");
if (!(await flush())) throw new Error("Document attached locally; saving failed. Retry Save.");
}
async function updateDocument(documentId: string, name: string, description: string) {
if (!perform({ type: "update_document", documentId, name, description }) || !(await flush()))
throw new Error("Unable to save document details.");
}
return {
updateAnswers,
updateIdentity,
updateNegativeAction,
updateHousehold,
updateOutcome,
saveScenario,
applyFinal,
uploadDocument,
updateDocument,
};
}