From dc23c3dd1c779738b1e4b2fbb5d88540aace04e2 Mon Sep 17 00:00:00 2001 From: Amin Mousavi Date: Thu, 10 Sep 2026 17:18:02 +0800 Subject: [PATCH] fix(voice): prime the lab-drafts ref before the save that reads it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "برای دندون ۱۲ و ۱۴ یه فول متال کران" 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) --- .../src/components/ui/treatment/TreatmentWorkspace.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx index dd659c9..ce4ac0d 100644 --- a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx +++ b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx @@ -551,6 +551,13 @@ export function TreatmentWorkspace({ const draftHydratingRef = useRef(false); const workspaceModeRef = useRef(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); labCaseDraftsRef.current = labCaseDrafts; const skipNextGetDraftRef = useRef(false); @@ -2278,6 +2285,7 @@ export function TreatmentWorkspace({ })); }); const updatedLabCases = [...labCaseDrafts, draft]; + labCaseDraftsRef.current = updatedLabCases; setLabCaseDrafts(updatedLabCases); // 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) => lc.clientId === orphan.clientId ? { ...lc, detailClientId: activeDetailId } : lc, ); + labCaseDraftsRef.current = updatedLabCases; setLabCaseDrafts(updatedLabCases); setActiveLabCaseId(orphan.clientId); @@ -2450,6 +2459,7 @@ export function TreatmentWorkspace({ attachmentIds: activeDetail?.attachmentMetas.map((a) => a.id) ?? [], }; const updatedLabCases = [...cleaned, next]; + labCaseDraftsRef.current = updatedLabCases; setLabCaseDrafts(updatedLabCases); setActiveLabCaseId(next.clientId);