fix(voice): stop the review sheet from contradicting itself about a target
A tooth named twice — once bare, once with a job — was counted as jobless. joblessProsthesisTargets adds an uncovered target inside the loop, but a LATER assignment can still cover it, and nothing re-checked at the end. The sheet then showed 13 with its crown AND struck through as "no prosthesis heard". prosthesisTargetLines also returns a line per target including one with nothing applied and nothing refused, so the sheet printed a dangling "13: " beside the jobless entry for the same tooth. The sheet now renders only lines carrying a code; the unfiltered list still feeds the chart, so a target pending a material pick stays selected. A line whose codes were all refused no longer opens with a leading " + ". applyVoiceResult now reads labCaseDraftsRef.current rather than the state. Every other handler writes that ref beside setLabCaseDrafts precisely because a save in the same tick reads it; this path was the one left reading state, so a draft added earlier in the same tick could be dropped. Found by /code-review. One new test, verified to fail without the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -232,6 +232,16 @@ describe('joblessProsthesisTargets', () => {
|
||||
expect(joblessProsthesisTargets(result)).toEqual(['13']);
|
||||
});
|
||||
|
||||
it('does not call a target jobless when another assignment already gave it a job', () => {
|
||||
const result = baseResult({
|
||||
prosthesisAssignments: [
|
||||
{ targets: ['13'], types: [], spoken: '' },
|
||||
{ targets: ['13'], types: ['pfm_crown'], spoken: '' },
|
||||
],
|
||||
});
|
||||
expect(joblessProsthesisTargets(result)).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not call a target jobless while it is pending a material pick', () => {
|
||||
const result = baseResult({
|
||||
prosthesisAssignments: [{ targets: ['13'], types: [], spoken: 'روکش' }],
|
||||
|
||||
@@ -239,7 +239,11 @@ export function joblessProsthesisTargets(result: VoiceExtractionResult): string[
|
||||
if (!covered.has(tooth) && !pending.has(tooth)) jobless.add(tooth);
|
||||
}
|
||||
|
||||
return [...jobless];
|
||||
// A target named twice — once with a job, once bare — is not jobless. Without this the
|
||||
// sheet shows the same tooth with its crown AND struck through as "no prosthesis heard".
|
||||
return [...jobless].filter(
|
||||
(target) => !covered.has(target) && !pending.has(target),
|
||||
);
|
||||
}
|
||||
|
||||
export type VoiceProsthesisChartData = {
|
||||
|
||||
@@ -2191,7 +2191,9 @@ export function TreatmentWorkspace({
|
||||
setActiveDetailId(detail.clientId);
|
||||
|
||||
if (labCaseDraft) {
|
||||
const updatedLabCases = [...labCaseDrafts, labCaseDraft];
|
||||
// The ref, not the state: every other handler now writes the ref beside
|
||||
// setLabCaseDrafts, so a draft added earlier in the same tick is only there.
|
||||
const updatedLabCases = [...labCaseDraftsRef.current, labCaseDraft];
|
||||
labCaseDraftsRef.current = updatedLabCases;
|
||||
setLabCaseDrafts(updatedLabCases);
|
||||
|
||||
@@ -2215,7 +2217,6 @@ export function TreatmentWorkspace({
|
||||
setVoiceResult(null);
|
||||
},
|
||||
[
|
||||
labCaseDrafts,
|
||||
labDependentCodes,
|
||||
persistDraft,
|
||||
persistLabCases,
|
||||
|
||||
@@ -106,6 +106,13 @@ export function VoiceReviewSheet({
|
||||
[effective, prosthesisCatalog],
|
||||
);
|
||||
const joblessTargets = useMemo(() => joblessProsthesisTargets(effective), [effective]);
|
||||
// A target whose assignment named no usable job still produces a line, and the jobless list
|
||||
// already names it. Rendering both printed a dangling "13: " beside "13: no prosthesis
|
||||
// heard". The unfiltered list still feeds the chart, so a pending target stays selected.
|
||||
const shownLines = useMemo(
|
||||
() => prosthesisLines.filter((line) => line.applied.length > 0 || line.refused.length > 0),
|
||||
[prosthesisLines],
|
||||
);
|
||||
const chartData = useMemo(
|
||||
() => prosthesisChartData(prosthesisLines, joblessTargets, prosthesisCatalog),
|
||||
[prosthesisLines, joblessTargets, prosthesisCatalog],
|
||||
@@ -269,7 +276,7 @@ export function VoiceReviewSheet({
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-text-primary">
|
||||
{prosthesisLines.map((line, i) => (
|
||||
{shownLines.map((line, i) => (
|
||||
<span key={`line-${line.target}`}>
|
||||
{i > 0 ? ' · ' : ''}
|
||||
{targetLabel(line.target)}:{' '}
|
||||
@@ -279,13 +286,13 @@ export function VoiceReviewSheet({
|
||||
{labelFor(code, prosthesisCatalog)}
|
||||
</span>
|
||||
))}
|
||||
{line.refused.map((code) => (
|
||||
{line.refused.map((code, j) => (
|
||||
<span
|
||||
key={code}
|
||||
title={t('voiceStackRefused')}
|
||||
className="text-text-muted line-through"
|
||||
>
|
||||
{' + '}
|
||||
{line.applied.length > 0 || j > 0 ? ' + ' : ''}
|
||||
{labelFor(code, prosthesisCatalog)}
|
||||
</span>
|
||||
))}
|
||||
@@ -296,7 +303,7 @@ export function VoiceReviewSheet({
|
||||
key={`jobless-${target}`}
|
||||
className="text-text-muted line-through"
|
||||
>
|
||||
{prosthesisLines.length > 0 || i > 0 ? ' · ' : ''}
|
||||
{shownLines.length > 0 || i > 0 ? ' · ' : ''}
|
||||
{targetLabel(target)}: {t('voiceNoProsthesisHeard')}
|
||||
</span>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user