fix(voice): prime the lab-drafts ref before the save that reads it
"برای دندون ۱۲ و ۱۴ یه فول متال کران" previewed correctly, then Apply failed
with TREATMENT_TOOTH_NOT_ON_DETAIL. The form still showed both teeth and both
crowns — that is React state. The database got a detail with no teeth.
applyVoiceResult called setLabCaseDrafts but never wrote
labCaseDraftsRef.current, which is only refreshed in the render body.
persistDraft runs in the same tick and starts by pruning each lab-dependent
detail down to the teeth its jobs cover, reading that stale ref. It found no
draft for the brand-new detail, so jobs were empty, so the detail was saved
with teeth: []. persistLabCases then posted rows for 12 and 14 against it,
and the server — which re-reads the detail's teeth from the database, because
the lab-case endpoint carries no teeth field — correctly refused.
detailsRef was already written by hand two lines above for exactly this
reason (3911477). This is the other half of the same mistake.
Two sibling handlers had the same gap and are fixed with it: adopting an
orphan lab case, and creating a new lab draft. Both call setLabCaseDrafts and
then persistDraft in the same tick. handleLabCasesChange was the only site
that already primed the ref, and it is the pattern the others now follow.
Adds a comment at the ref declaration, because the coupling between this ref
and persistDraft's prune is invisible at the call sites and has now cost two
bugs.
No automated test: this is state-and-ref ordering inside a React component,
and the frontend's Vitest scope is pure helpers only — no React, no DOM.
Mirroring the server's tooth rule on the client to make it testable would
duplicate a rule across layers, which decision 38 rules out.
Gates: tsc --noEmit clean, 37 Vitest tests, next build clean, ESLint warnings
unchanged at 10 (all pre-existing, count verified against HEAD).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -551,6 +551,13 @@ export function TreatmentWorkspace({
|
|||||||
const draftHydratingRef = useRef(false);
|
const draftHydratingRef = useRef(false);
|
||||||
const workspaceModeRef = useRef(workspaceMode);
|
const workspaceModeRef = useRef(workspaceMode);
|
||||||
workspaceModeRef.current = workspaceMode;
|
workspaceModeRef.current = workspaceMode;
|
||||||
|
/**
|
||||||
|
* `persistDraft` prunes a lab-dependent detail down to the teeth its jobs cover, and it reads
|
||||||
|
* this ref — not React state. So any handler that changes the drafts and then saves in the
|
||||||
|
* same tick must write the ref beside `setLabCaseDrafts`; the render-time assignment below has
|
||||||
|
* not run yet. Skipping it saves the detail with no teeth, and the lab-case request that
|
||||||
|
* follows is then rejected with TREATMENT_TOOTH_NOT_ON_DETAIL.
|
||||||
|
*/
|
||||||
const labCaseDraftsRef = useRef(labCaseDrafts);
|
const labCaseDraftsRef = useRef(labCaseDrafts);
|
||||||
labCaseDraftsRef.current = labCaseDrafts;
|
labCaseDraftsRef.current = labCaseDrafts;
|
||||||
const skipNextGetDraftRef = useRef(false);
|
const skipNextGetDraftRef = useRef(false);
|
||||||
@@ -2278,6 +2285,7 @@ export function TreatmentWorkspace({
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
const updatedLabCases = [...labCaseDrafts, draft];
|
const updatedLabCases = [...labCaseDrafts, draft];
|
||||||
|
labCaseDraftsRef.current = updatedLabCases;
|
||||||
setLabCaseDrafts(updatedLabCases);
|
setLabCaseDrafts(updatedLabCases);
|
||||||
|
|
||||||
// Persist the new detail first so lab-case rows can use real treatmentDetailIds.
|
// Persist the new detail first so lab-case rows can use real treatmentDetailIds.
|
||||||
@@ -2432,6 +2440,7 @@ export function TreatmentWorkspace({
|
|||||||
const updatedLabCases = cleaned.map((lc) =>
|
const updatedLabCases = cleaned.map((lc) =>
|
||||||
lc.clientId === orphan.clientId ? { ...lc, detailClientId: activeDetailId } : lc,
|
lc.clientId === orphan.clientId ? { ...lc, detailClientId: activeDetailId } : lc,
|
||||||
);
|
);
|
||||||
|
labCaseDraftsRef.current = updatedLabCases;
|
||||||
setLabCaseDrafts(updatedLabCases);
|
setLabCaseDrafts(updatedLabCases);
|
||||||
setActiveLabCaseId(orphan.clientId);
|
setActiveLabCaseId(orphan.clientId);
|
||||||
|
|
||||||
@@ -2450,6 +2459,7 @@ export function TreatmentWorkspace({
|
|||||||
attachmentIds: activeDetail?.attachmentMetas.map((a) => a.id) ?? [],
|
attachmentIds: activeDetail?.attachmentMetas.map((a) => a.id) ?? [],
|
||||||
};
|
};
|
||||||
const updatedLabCases = [...cleaned, next];
|
const updatedLabCases = [...cleaned, next];
|
||||||
|
labCaseDraftsRef.current = updatedLabCases;
|
||||||
setLabCaseDrafts(updatedLabCases);
|
setLabCaseDrafts(updatedLabCases);
|
||||||
setActiveLabCaseId(next.clientId);
|
setActiveLabCaseId(next.clientId);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user