fix(frontend): untick prosthesis when a picked tooth breaks its map

initialVoiceSelection deliberately never auto-ticks an incomplete
prosthesis map, because a detail with an untyped tooth cannot ship — it
fails at dispatch instead. Picking a candidate tooth walked straight
through that rule: the tick was seeded once, so a map that was complete at
extraction stayed ticked after a tooth with no prosthesis type joined it,
and Apply attached a map assertCompleteToothProsthesisMap rejects.

Recomputed on each pick, and only ever downwards — re-ticking is the
clinician's call, not a side effect of un-picking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 05:04:15 +08:00
parent b7ee61433e
commit 62deff0523

View File

@@ -69,12 +69,23 @@ export function VoiceReviewSheet({
const selectedCount = countSelected(selection, available); const selectedCount = countSelected(selection, available);
const pickCandidate = (tooth: FdiToothId) => { const pickCandidate = (tooth: FdiToothId) => {
setChosen((prev) => const nextChosen = chosen.includes(tooth)
prev.includes(tooth) ? prev.filter((t) => t !== tooth) : [...prev, tooth], ? chosen.filter((t) => t !== tooth)
); : [...chosen, tooth];
setChosen(nextChosen);
setSelection((prev) => ({
...prev,
// The teeth row starts unticked whenever the recording produced no teeth of its own, // The teeth row starts unticked whenever the recording produced no teeth of its own,
// and a picked tooth that is not ticked applies nothing. // and a picked tooth that is not ticked applies nothing.
setSelection((prev) => (prev.teeth ? prev : { ...prev, teeth: true })); teeth: true,
// The picked tooth has no prosthesis type, which makes the map unshippable. Leaving
// the row ticked would apply a map that `assertCompleteToothProsthesisMap` rejects
// at dispatch — the exact failure the never-auto-tick-incomplete rule exists to
// prevent. Only ever unticks: re-ticking is the clinician's call.
prosthesis:
prev.prosthesis &&
withChosenTeeth(result, nextChosen).prosthesis?.complete !== false,
}));
}; };
const labelFor = (code: string | null, catalog: { code: string; label: string }[]) => const labelFor = (code: string | null, catalog: { code: string; label: string }[]) =>