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']);
|
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', () => {
|
it('does not call a target jobless while it is pending a material pick', () => {
|
||||||
const result = baseResult({
|
const result = baseResult({
|
||||||
prosthesisAssignments: [{ targets: ['13'], types: [], spoken: 'روکش' }],
|
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);
|
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 = {
|
export type VoiceProsthesisChartData = {
|
||||||
|
|||||||
@@ -2191,7 +2191,9 @@ export function TreatmentWorkspace({
|
|||||||
setActiveDetailId(detail.clientId);
|
setActiveDetailId(detail.clientId);
|
||||||
|
|
||||||
if (labCaseDraft) {
|
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;
|
labCaseDraftsRef.current = updatedLabCases;
|
||||||
setLabCaseDrafts(updatedLabCases);
|
setLabCaseDrafts(updatedLabCases);
|
||||||
|
|
||||||
@@ -2215,7 +2217,6 @@ export function TreatmentWorkspace({
|
|||||||
setVoiceResult(null);
|
setVoiceResult(null);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
labCaseDrafts,
|
|
||||||
labDependentCodes,
|
labDependentCodes,
|
||||||
persistDraft,
|
persistDraft,
|
||||||
persistLabCases,
|
persistLabCases,
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ export function VoiceReviewSheet({
|
|||||||
[effective, prosthesisCatalog],
|
[effective, prosthesisCatalog],
|
||||||
);
|
);
|
||||||
const joblessTargets = useMemo(() => joblessProsthesisTargets(effective), [effective]);
|
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(
|
const chartData = useMemo(
|
||||||
() => prosthesisChartData(prosthesisLines, joblessTargets, prosthesisCatalog),
|
() => prosthesisChartData(prosthesisLines, joblessTargets, prosthesisCatalog),
|
||||||
[prosthesisLines, joblessTargets, prosthesisCatalog],
|
[prosthesisLines, joblessTargets, prosthesisCatalog],
|
||||||
@@ -269,7 +276,7 @@ export function VoiceReviewSheet({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-text-primary">
|
<p className="mt-1 text-sm text-text-primary">
|
||||||
{prosthesisLines.map((line, i) => (
|
{shownLines.map((line, i) => (
|
||||||
<span key={`line-${line.target}`}>
|
<span key={`line-${line.target}`}>
|
||||||
{i > 0 ? ' · ' : ''}
|
{i > 0 ? ' · ' : ''}
|
||||||
{targetLabel(line.target)}:{' '}
|
{targetLabel(line.target)}:{' '}
|
||||||
@@ -279,13 +286,13 @@ export function VoiceReviewSheet({
|
|||||||
{labelFor(code, prosthesisCatalog)}
|
{labelFor(code, prosthesisCatalog)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{line.refused.map((code) => (
|
{line.refused.map((code, j) => (
|
||||||
<span
|
<span
|
||||||
key={code}
|
key={code}
|
||||||
title={t('voiceStackRefused')}
|
title={t('voiceStackRefused')}
|
||||||
className="text-text-muted line-through"
|
className="text-text-muted line-through"
|
||||||
>
|
>
|
||||||
{' + '}
|
{line.applied.length > 0 || j > 0 ? ' + ' : ''}
|
||||||
{labelFor(code, prosthesisCatalog)}
|
{labelFor(code, prosthesisCatalog)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -296,7 +303,7 @@ export function VoiceReviewSheet({
|
|||||||
key={`jobless-${target}`}
|
key={`jobless-${target}`}
|
||||||
className="text-text-muted line-through"
|
className="text-text-muted line-through"
|
||||||
>
|
>
|
||||||
{prosthesisLines.length > 0 || i > 0 ? ' · ' : ''}
|
{shownLines.length > 0 || i > 0 ? ' · ' : ''}
|
||||||
{targetLabel(target)}: {t('voiceNoProsthesisHeard')}
|
{targetLabel(target)}: {t('voiceNoProsthesisHeard')}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user