improvement: new prosthesis type data structure implemented and finally working!
This commit is contained in:
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useRouter } from '@/i18n/navigation';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { WizardStepper } from '@/components/ui/shared/WizardStepper';
|
||||
import { PatientSearchCombobox } from '@/components/ui/patient/PatientSearchCombobox';
|
||||
import {
|
||||
TreatmentLabCasesPanel,
|
||||
@@ -14,6 +13,7 @@ import { TreatmentRailSection } from '@/components/ui/treatment/TreatmentRailSec
|
||||
import { AppointmentsStrip } from '@/components/ui/treatment/AppointmentsStrip';
|
||||
import { NewTreatmentPatientPicker } from '@/components/ui/treatment/NewTreatmentPatientPicker';
|
||||
import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart';
|
||||
import { ProsthesisAssignChart } from '@/components/ui/treatment/ProsthesisAssignChart';
|
||||
import { LabCasesDispatchPanel } from '@/components/ui/treatment/LabCasesDispatchPanel';
|
||||
import { LabDispatchAttentionPanel } from '@/components/ui/treatment/LabDispatchAttentionPanel';
|
||||
import { PastTreatmentsPanel } from '@/components/ui/treatment/PastTreatmentsPanel';
|
||||
@@ -44,10 +44,8 @@ import { unscheduledStripColorCode, type DayStripItem } from '@/components/treat
|
||||
import {
|
||||
areDetailsPersistable,
|
||||
defaultTreatmentTypeForAppointment,
|
||||
isDetailReadyForLabDispatch,
|
||||
isDetailTypeSelected,
|
||||
isEmptyDraftDetail,
|
||||
isLabDependentDetailMissingTeeth,
|
||||
areUnscheduledDetailsStripDeletable,
|
||||
} from '@/components/treatment/treatmentDetailRules';
|
||||
import {
|
||||
@@ -62,6 +60,7 @@ import {
|
||||
toggleToothInGroups,
|
||||
unlinkAdjacentTeeth,
|
||||
} from '@/components/treatment/toothSelectionGroups';
|
||||
import { hasArchJobs } from '@/components/treatment/prosthesisTree';
|
||||
import type { LabDispatchAttentionItem } from '@/components/treatment/labDispatchAttention';
|
||||
import { collectLabDispatchAttention } from '@/components/treatment/labDispatchAttention';
|
||||
import {
|
||||
@@ -97,7 +96,6 @@ import type {
|
||||
import type { PatientLabCaseSummary } from '@/types/lab-case-activity';
|
||||
|
||||
type WorkspaceMode = 'live' | 'historical';
|
||||
type EntryStep = 'treatment' | 'lab';
|
||||
|
||||
function isTreatmentDayHistorical(treatmentAt: string, todayStart: Date): boolean {
|
||||
return compareLocalDayStart(new Date(treatmentAt), todayStart) < 0;
|
||||
@@ -107,6 +105,15 @@ function withoutEmptyLabCaseDrafts(drafts: LabCaseDraft[]): LabCaseDraft[] {
|
||||
return drafts.filter((lc) => lc.sentAt || Boolean(lc.detailClientId));
|
||||
}
|
||||
|
||||
/** Prefer the unsent draft this chart writes to; fall back to a sent case for locked details. */
|
||||
function labCaseDraftForDetail(
|
||||
drafts: LabCaseDraft[],
|
||||
detailClientId: string,
|
||||
): LabCaseDraft | undefined {
|
||||
const rows = drafts.filter((lc) => lc.detailClientId === detailClientId);
|
||||
return rows.find((lc) => !lc.sentAt) ?? rows[0];
|
||||
}
|
||||
|
||||
function labCaseDraftsToPast(
|
||||
labCaseDrafts: LabCaseDraft[],
|
||||
details: TreatmentDetailDraft[],
|
||||
@@ -132,6 +139,7 @@ function labCaseDraftsToPast(
|
||||
}
|
||||
: null,
|
||||
sends: lc.sends ?? [],
|
||||
toothProsthesis: lc.toothProsthesis,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -450,8 +458,8 @@ export function TreatmentWorkspace({
|
||||
const pendingLabCaseIdRef = useRef<string | null>(initialLabCaseId);
|
||||
const labPanelRef = useRef<HTMLDivElement>(null);
|
||||
const historyRequestRef = useRef(0);
|
||||
/** When set, activeDetailId effect opens this step instead of resetting to treatment. */
|
||||
const pendingEntryStepRef = useRef<EntryStep | null>(null);
|
||||
/** When set, switching the active detail scrolls to the lab dispatch panel. */
|
||||
const pendingScrollToLabRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
pendingAppointmentIdRef.current = initialAppointmentId;
|
||||
@@ -470,7 +478,6 @@ export function TreatmentWorkspace({
|
||||
const [organizationSearch, setOrganizationSearch] = useState('');
|
||||
const [recentOrganizationIds, setRecentOrganizationIds] = useState<string[]>([]);
|
||||
const [showWholeTreatmentPlan, setShowWholeTreatmentPlan] = useState(false);
|
||||
const [entryStep, setEntryStep] = useState<EntryStep>('treatment');
|
||||
|
||||
|
||||
const [voiceAvailability, setVoiceAvailability] = useState<VoiceAvailability | null>(null);
|
||||
@@ -629,27 +636,17 @@ export function TreatmentWorkspace({
|
||||
[details, activeDetailId],
|
||||
);
|
||||
|
||||
const showLabDispatchPanel = useMemo(
|
||||
() => details.some((d) => isDetailReadyForLabDispatch(d, labDependentCodes)),
|
||||
[details, labDependentCodes],
|
||||
);
|
||||
|
||||
/** Lab send sheet only for prosthesis (lab-dependent) types on the active detail. */
|
||||
const showLabWizardStep = useMemo(
|
||||
const showLabDispatch = useMemo(
|
||||
() => Boolean(activeDetail && labDependentCodes.has(activeDetail.treatmentType)),
|
||||
[activeDetail, labDependentCodes],
|
||||
);
|
||||
|
||||
const showLabShipmentBlocked = useMemo(
|
||||
() =>
|
||||
Boolean(
|
||||
activeDetail && isLabDependentDetailMissingTeeth(activeDetail, labDependentCodes),
|
||||
),
|
||||
[activeDetail, labDependentCodes],
|
||||
);
|
||||
|
||||
const activeTypeSelected = Boolean(activeDetail && isDetailTypeSelected(activeDetail));
|
||||
const activeLocked = Boolean(activeDetail && isDetailLocked(activeDetail));
|
||||
const activeHasArchJobs = hasArchJobs(
|
||||
labCaseDraftForDetail(labCaseDrafts, activeDetailId)?.toothProsthesis ?? [],
|
||||
activeDetailId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedStandaloneId || draftHydratingRef.current) return;
|
||||
@@ -853,28 +850,27 @@ export function TreatmentWorkspace({
|
||||
const chartSelectedTeeth = showWholeTreatmentPlan ? wholePlanTeethSet : selectedTeethSet;
|
||||
const chartToothColors = showWholeTreatmentPlan ? wholePlanToothColors : undefined;
|
||||
const chartLinkedEdges = showWholeTreatmentPlan ? wholePlanLinkedEdges : linkedToothEdges;
|
||||
const prosthesisAssignActive =
|
||||
!showWholeTreatmentPlan &&
|
||||
Boolean(activeDetail && labDependentCodes.has(activeDetail.treatmentType)) &&
|
||||
prosthesisCatalog.length > 0;
|
||||
|
||||
// Reset whole-plan overview when switching details.
|
||||
// Prefer pendingEntryStepRef (e.g. Lab shipments → Lab step) over defaulting to treatment.
|
||||
useEffect(() => {
|
||||
setShowWholeTreatmentPlan(false);
|
||||
rangeAnchorRef.current = null;
|
||||
setOrganizationSearch('');
|
||||
const pending = pendingEntryStepRef.current;
|
||||
pendingEntryStepRef.current = null;
|
||||
setEntryStep(pending ?? 'treatment');
|
||||
}, [activeDetailId]);
|
||||
|
||||
// Leave Lab step if the active detail is no longer prosthesis / lab-dependent.
|
||||
useEffect(() => {
|
||||
if (entryStep === 'lab' && !showLabWizardStep) {
|
||||
setEntryStep('treatment');
|
||||
if (pendingScrollToLabRef.current) {
|
||||
pendingScrollToLabRef.current = false;
|
||||
requestAnimationFrame(() => {
|
||||
scrollWithinMainScrollContainer(labPanelRef.current);
|
||||
});
|
||||
}
|
||||
}, [entryStep, showLabWizardStep]);
|
||||
}, [activeDetailId]);
|
||||
|
||||
// Sync active lab shipment when the selected treatment detail changes.
|
||||
useEffect(() => {
|
||||
const match = labCaseDrafts.find((lc) => lc.detailClientId === activeDetailId);
|
||||
const match = labCaseDraftForDetail(labCaseDrafts, activeDetailId);
|
||||
setActiveLabCaseId(match?.clientId ?? null);
|
||||
}, [activeDetailId, labCaseDrafts]);
|
||||
|
||||
@@ -1569,14 +1565,14 @@ export function TreatmentWorkspace({
|
||||
skipNextGetDraftRef.current = true;
|
||||
draftHydratingRef.current = true;
|
||||
if (focusDetailClientId && options?.scrollToLabPanel !== false) {
|
||||
pendingEntryStepRef.current = 'lab';
|
||||
pendingScrollToLabRef.current = true;
|
||||
}
|
||||
hydrateFromTreatment(treatmentToLoad);
|
||||
draftHydratingRef.current = false;
|
||||
|
||||
if (focusDetailClientId) {
|
||||
if (options?.scrollToLabPanel !== false) {
|
||||
pendingEntryStepRef.current = 'lab';
|
||||
pendingScrollToLabRef.current = true;
|
||||
}
|
||||
setActiveDetailId(focusDetailClientId);
|
||||
const mappedLabCases = withoutEmptyLabCaseDrafts(
|
||||
@@ -1589,7 +1585,6 @@ export function TreatmentWorkspace({
|
||||
setActiveLabCaseId(linked.clientId);
|
||||
}
|
||||
if (options?.scrollToLabPanel !== false) {
|
||||
setEntryStep('lab');
|
||||
requestAnimationFrame(() => {
|
||||
scrollWithinMainScrollContainer(labPanelRef.current);
|
||||
});
|
||||
@@ -1690,7 +1685,7 @@ export function TreatmentWorkspace({
|
||||
(item: LabDispatchAttentionItem) => {
|
||||
if (item.isCurrentDraft) {
|
||||
exitBrowse();
|
||||
pendingEntryStepRef.current = 'lab';
|
||||
pendingScrollToLabRef.current = true;
|
||||
setActiveDetailId(item.detailClientId);
|
||||
const linked = labCaseDrafts.find(
|
||||
(lc) => !lc.sentAt && lc.detailClientId === item.detailClientId,
|
||||
@@ -1698,7 +1693,6 @@ export function TreatmentWorkspace({
|
||||
if (linked) {
|
||||
setActiveLabCaseId(linked.clientId);
|
||||
}
|
||||
setEntryStep('lab');
|
||||
requestAnimationFrame(() => {
|
||||
scrollWithinMainScrollContainer(labPanelRef.current);
|
||||
});
|
||||
@@ -1759,13 +1753,12 @@ export function TreatmentWorkspace({
|
||||
workspaceMode === 'live' &&
|
||||
!isBrowsing
|
||||
) {
|
||||
pendingEntryStepRef.current = 'lab';
|
||||
pendingScrollToLabRef.current = true;
|
||||
setActiveDetailId(item.detailClientId);
|
||||
const matchingDraft = labCaseDrafts.find((lc) => lc.id === item.labCaseId);
|
||||
if (matchingDraft) {
|
||||
setActiveLabCaseId(matchingDraft.clientId);
|
||||
}
|
||||
setEntryStep('lab');
|
||||
requestAnimationFrame(() => {
|
||||
scrollWithinMainScrollContainer(labPanelRef.current);
|
||||
});
|
||||
@@ -2003,7 +1996,6 @@ export function TreatmentWorkspace({
|
||||
// persistDraft reads detailsRef, and setDetails has not rendered yet.
|
||||
detailsRef.current = nextDetails;
|
||||
setActiveDetailId(detail.clientId);
|
||||
setEntryStep('treatment');
|
||||
|
||||
// Lab-side rows ride on a lab case draft keyed by the detail's *client* id, so a
|
||||
// brand-new unsaved detail can still carry one; it is persisted after the detail is.
|
||||
@@ -2098,7 +2090,6 @@ export function TreatmentWorkspace({
|
||||
detailsRef.current = nextDetails;
|
||||
setDetails(nextDetails);
|
||||
setActiveDetailId(nextActive);
|
||||
if (nextDetails.length === 0) setEntryStep('treatment');
|
||||
|
||||
if (labCaseDrafts.some((lc) => lc.detailClientId === detailClientId)) {
|
||||
handleLabCasesChange(
|
||||
@@ -2115,7 +2106,61 @@ export function TreatmentWorkspace({
|
||||
handleLabCasesChange,
|
||||
isDetailLocked,
|
||||
labCaseDrafts,
|
||||
setEntryStep,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
const handleTreatmentTypeChange = useCallback(
|
||||
(nextType: string) => {
|
||||
if (!canEditTreatmentForDay) return;
|
||||
const detail = details.find((d) => d.clientId === activeDetailId);
|
||||
if (!detail || isDetailLocked(detail)) return;
|
||||
if (detail.treatmentType === nextType) return;
|
||||
|
||||
const leavingLab = labDependentCodes.has(detail.treatmentType);
|
||||
const jobs =
|
||||
labCaseDraftForDetail(labCaseDrafts, activeDetailId)?.toothProsthesis ?? [];
|
||||
const hasWork =
|
||||
detail.teeth.length > 0 ||
|
||||
detail.toothSelectionGroups.some((g) => g.teeth.length > 0) ||
|
||||
jobs.length > 0;
|
||||
|
||||
if (leavingLab && hasWork && !window.confirm(t('confirmLeaveProsthesis'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextDetails = details.map((d) => {
|
||||
if (d.clientId !== activeDetailId) return d;
|
||||
if (leavingLab) {
|
||||
return {
|
||||
...d,
|
||||
treatmentType: nextType,
|
||||
teeth: [],
|
||||
toothSelectionGroups: [],
|
||||
};
|
||||
}
|
||||
return { ...d, treatmentType: nextType };
|
||||
});
|
||||
detailsRef.current = nextDetails;
|
||||
setDetails(nextDetails);
|
||||
|
||||
if (leavingLab) {
|
||||
const nextDrafts = labCaseDrafts.filter(
|
||||
(lc) => lc.detailClientId !== activeDetailId || Boolean(lc.sentAt),
|
||||
);
|
||||
if (nextDrafts.length !== labCaseDrafts.length) {
|
||||
handleLabCasesChange(nextDrafts);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
activeDetailId,
|
||||
canEditTreatmentForDay,
|
||||
details,
|
||||
handleLabCasesChange,
|
||||
isDetailLocked,
|
||||
labCaseDrafts,
|
||||
labDependentCodes,
|
||||
t,
|
||||
],
|
||||
);
|
||||
@@ -2133,16 +2178,8 @@ export function TreatmentWorkspace({
|
||||
}
|
||||
|
||||
const activeDetail = details.find((d) => d.clientId === activeDetailId);
|
||||
if (
|
||||
activeDetail &&
|
||||
isLabDependentDetailMissingTeeth(activeDetail, labDependentCodes)
|
||||
) {
|
||||
showError(t('labShipmentBlockedBody'));
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldIncludeActive = Boolean(
|
||||
activeDetail && isDetailReadyForLabDispatch(activeDetail, labDependentCodes),
|
||||
activeDetail && labDependentCodes.has(activeDetail.treatmentType),
|
||||
);
|
||||
|
||||
const orphan = cleaned.find((lc) => !lc.sentAt && !lc.detailClientId);
|
||||
@@ -2191,13 +2228,12 @@ export function TreatmentWorkspace({
|
||||
tErrors,
|
||||
]);
|
||||
|
||||
// Auto-open shipment draft when entering Lab (no manual "Add lab shipment" click).
|
||||
// Auto-create an unsent draft when the active detail is prosthesis (no wizard step).
|
||||
useEffect(() => {
|
||||
if (entryStep !== 'lab') return;
|
||||
if (!showLabDispatch) return;
|
||||
if (!canEditTreatmentForDay || !hasLiveContext) return;
|
||||
const activeDetail = details.find((d) => d.clientId === activeDetailId);
|
||||
if (!activeDetail || !isDetailReadyForLabDispatch(activeDetail, labDependentCodes)) return;
|
||||
if (isLabDependentDetailMissingTeeth(activeDetail, labDependentCodes)) return;
|
||||
if (!activeDetail || !labDependentCodes.has(activeDetail.treatmentType)) return;
|
||||
// Already shipped (or locked) — do not create another draft (avoids post-send flicker).
|
||||
if (isDetailLocked(activeDetail)) return;
|
||||
if (labCaseDrafts.some((lc) => lc.detailClientId === activeDetailId && lc.sentAt)) return;
|
||||
@@ -2208,12 +2244,12 @@ export function TreatmentWorkspace({
|
||||
activeDetailId,
|
||||
canEditTreatmentForDay,
|
||||
details,
|
||||
entryStep,
|
||||
handleAddLabCase,
|
||||
hasLiveContext,
|
||||
isDetailLocked,
|
||||
labCaseDrafts,
|
||||
labDependentCodes,
|
||||
selectedAppointment,
|
||||
showLabDispatch,
|
||||
]);
|
||||
|
||||
const handleSendLabCase = useCallback(
|
||||
@@ -2572,6 +2608,7 @@ export function TreatmentWorkspace({
|
||||
activeDetailId={activeDetailId}
|
||||
onActiveDetailChange={setActiveDetailId}
|
||||
onDetailsChange={setDetails}
|
||||
onTreatmentTypeChange={handleTreatmentTypeChange}
|
||||
isDetailLocked={isDetailLocked}
|
||||
labDependentCodes={labDependentCodes}
|
||||
treatmentCatalog={treatmentDropdownCatalog}
|
||||
@@ -2590,7 +2627,6 @@ export function TreatmentWorkspace({
|
||||
const next = newDetail(seedFromAppointment);
|
||||
setDetails((prev) => [...prev, next]);
|
||||
setActiveDetailId(next.clientId);
|
||||
setEntryStep('treatment');
|
||||
}}
|
||||
onRemoveDetail={handleRemoveDetail}
|
||||
onUploadFiles={(files, onProgress) =>
|
||||
@@ -2619,14 +2655,125 @@ export function TreatmentWorkspace({
|
||||
);
|
||||
}}
|
||||
showChrome
|
||||
showFields={entryStep === 'treatment'}
|
||||
showFields
|
||||
hasArchJobs={activeHasArchJobs}
|
||||
voice={voiceForEditor}
|
||||
chartLocked={
|
||||
entryStep === 'treatment' && !activeTypeSelected && !showWholeTreatmentPlan
|
||||
}
|
||||
chartLocked={!activeTypeSelected && !showWholeTreatmentPlan}
|
||||
chartLockMessage={t('selectTypeBeforeTeeth')}
|
||||
chart={
|
||||
entryStep === 'treatment' ? (
|
||||
prosthesisAssignActive ? (
|
||||
<ProsthesisAssignChart
|
||||
className="w-full"
|
||||
groups={
|
||||
activeDetail?.toothSelectionGroups?.length
|
||||
? activeDetail.toothSelectionGroups
|
||||
: groupsFromFlatTeeth(activeDetail?.teeth ?? [])
|
||||
}
|
||||
toothProsthesis={
|
||||
labCaseDraftForDetail(labCaseDrafts, activeDetailId)?.toothProsthesis ??
|
||||
[]
|
||||
}
|
||||
detailClientId={activeDetailId}
|
||||
catalog={prosthesisCatalog}
|
||||
disabled={!canEditTreatmentForDay || activeLocked}
|
||||
headerControl={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWholeTreatmentPlan((open) => !open)}
|
||||
aria-pressed={showWholeTreatmentPlan}
|
||||
className={`
|
||||
rounded-[var(--radius-md)] border px-2.5 py-1 text-[11px] leading-none transition-colors
|
||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45
|
||||
${
|
||||
showWholeTreatmentPlan
|
||||
? 'border-primary bg-primary-soft font-medium text-text-primary'
|
||||
: 'border-border/70 text-text-secondary hover:border-border hover:bg-background-card/50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{t('toothChartWholePlan')}
|
||||
</button>
|
||||
}
|
||||
onToggleLink={(a, b) => {
|
||||
if (
|
||||
!canEditTreatmentForDay ||
|
||||
activeLocked ||
|
||||
!activeDetailId ||
|
||||
!activeTypeSelected
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const detail = details.find((d) => d.clientId === activeDetailId);
|
||||
if (!detail) return;
|
||||
const currentGroups =
|
||||
detail.toothSelectionGroups.length > 0
|
||||
? detail.toothSelectionGroups
|
||||
: groupsFromFlatTeeth(detail.teeth);
|
||||
const edgeLinked = linkedEdgesFromGroups(currentGroups).has(
|
||||
toothEdgeKey(a, b),
|
||||
);
|
||||
const nextGroups = edgeLinked
|
||||
? unlinkAdjacentTeeth(currentGroups, a, b)
|
||||
: linkAdjacentTeeth(currentGroups, a, b);
|
||||
if (!nextGroups) return;
|
||||
setDetails((prev) =>
|
||||
prev.map((d) =>
|
||||
d.clientId !== activeDetailId
|
||||
? d
|
||||
: {
|
||||
...d,
|
||||
toothSelectionGroups: nextGroups,
|
||||
teeth: deriveTeethFromGroups(nextGroups),
|
||||
},
|
||||
),
|
||||
);
|
||||
setLabCaseDrafts((prev) =>
|
||||
prev.map((lc) => {
|
||||
if (lc.detailClientId !== activeDetailId) return lc;
|
||||
return {
|
||||
...lc,
|
||||
toothProsthesis: pruneToothProsthesisForGroups(
|
||||
lc.toothProsthesis,
|
||||
activeDetailId,
|
||||
nextGroups,
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onChange={({ groups, toothProsthesis }) => {
|
||||
setDetails((prev) =>
|
||||
prev.map((d) =>
|
||||
d.clientId !== activeDetailId
|
||||
? d
|
||||
: {
|
||||
...d,
|
||||
toothSelectionGroups: groups,
|
||||
teeth: deriveTeethFromGroups(groups),
|
||||
},
|
||||
),
|
||||
);
|
||||
setLabCaseDrafts((prev) => {
|
||||
const existing = prev.find(
|
||||
(lc) => lc.detailClientId === activeDetailId && !lc.sentAt,
|
||||
);
|
||||
if (existing) {
|
||||
return prev.map((lc) =>
|
||||
lc.clientId === existing.clientId ? { ...lc, toothProsthesis } : lc,
|
||||
);
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
...newLabCaseDraft(),
|
||||
detailClientId: activeDetailId,
|
||||
toothProsthesis,
|
||||
},
|
||||
];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FdiToothChart
|
||||
className="w-full"
|
||||
selected={chartSelectedTeeth}
|
||||
@@ -2765,75 +2912,64 @@ export function TreatmentWorkspace({
|
||||
activeLocked ||
|
||||
!activeTypeSelected
|
||||
}
|
||||
onReset={() => {
|
||||
if (
|
||||
!canEditTreatmentForDay ||
|
||||
activeLocked ||
|
||||
showWholeTreatmentPlan ||
|
||||
!activeDetailId ||
|
||||
!activeTypeSelected
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const detail = details.find((d) => d.clientId === activeDetailId);
|
||||
if (!detail) return;
|
||||
const currentGroups =
|
||||
detail.toothSelectionGroups.length > 0
|
||||
? detail.toothSelectionGroups
|
||||
: groupsFromFlatTeeth(detail.teeth);
|
||||
if (deriveTeethFromGroups(currentGroups).length === 0) return;
|
||||
if (!window.confirm(t('confirmResetChart'))) return;
|
||||
const nextGroups = groupsFromFlatTeeth([]);
|
||||
setDetails((prev) =>
|
||||
prev.map((d) =>
|
||||
d.clientId !== activeDetailId
|
||||
? d
|
||||
: {
|
||||
...d,
|
||||
toothSelectionGroups: nextGroups,
|
||||
teeth: [],
|
||||
},
|
||||
),
|
||||
);
|
||||
setLabCaseDrafts((prev) =>
|
||||
prev.map((lc) => {
|
||||
if (lc.detailClientId !== activeDetailId) return lc;
|
||||
return {
|
||||
...lc,
|
||||
toothProsthesis: pruneToothProsthesisForGroups(
|
||||
lc.toothProsthesis,
|
||||
activeDetailId,
|
||||
nextGroups,
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
stepper={
|
||||
showLabWizardStep ? (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
<WizardStepper
|
||||
className="min-w-0 flex-1"
|
||||
aria-label={t('entryWizardLabel')}
|
||||
steps={[
|
||||
{ id: 'treatment', label: t('entryStepTreatment') },
|
||||
{ id: 'lab', label: t('entryStepLab') },
|
||||
]}
|
||||
currentStepId={entryStep}
|
||||
onStepChange={(stepId) => {
|
||||
const next = stepId as EntryStep;
|
||||
if (next === 'lab' && canEditTreatmentForDay) {
|
||||
void persistDraft({ force: true })
|
||||
.then(() => setEntryStep('lab'))
|
||||
.catch((error: unknown) => {
|
||||
showError(getUserFacingError(error, tErrors, t('errorSaveDraft')));
|
||||
});
|
||||
return;
|
||||
}
|
||||
setEntryStep(next);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2 sm:justify-end shrink-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
disabled={entryStep === 'treatment'}
|
||||
onClick={() => setEntryStep('treatment')}
|
||||
>
|
||||
{t('entryStepBack')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={entryStep === 'lab'}
|
||||
onClick={async () => {
|
||||
if (canEditTreatmentForDay) {
|
||||
try {
|
||||
await persistDraft({ force: true });
|
||||
} catch (error: unknown) {
|
||||
showError(getUserFacingError(error, tErrors, t('errorSaveDraft')));
|
||||
return;
|
||||
}
|
||||
}
|
||||
setEntryStep('lab');
|
||||
}}
|
||||
>
|
||||
{t('entryStepNext')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{entryStep === 'lab' ? (
|
||||
{showLabDispatch ? (
|
||||
<div ref={labPanelRef} className="space-y-3">
|
||||
{showLabDispatchPanel ? (
|
||||
<LabCasesDispatchPanel
|
||||
<LabCasesDispatchPanel
|
||||
details={details}
|
||||
activeDetailId={activeDetailId}
|
||||
labCases={labCaseDrafts}
|
||||
labDependentCodes={labDependentCodes}
|
||||
treatmentCatalog={treatmentCatalog}
|
||||
prosthesisCatalog={prosthesisCatalog}
|
||||
labCaseSummary={activeLabCaseSummary}
|
||||
locale={locale}
|
||||
onLabCaseSummaryChange={handleLabCaseSummaryChange}
|
||||
@@ -2857,11 +2993,6 @@ export function TreatmentWorkspace({
|
||||
canInviteLab={canAccessOrganizations}
|
||||
onInviteLab={() => router.push('/organizations?action=invite-lab')}
|
||||
/>
|
||||
) : showLabShipmentBlocked ? null : (
|
||||
<p className="text-sm text-text-muted surface-card p-4">
|
||||
{t('entryStepLabUnavailable')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user