'use client'; 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, type TreatmentLabCasesScope, } from '@/components/ui/treatment/TreatmentLabCasesPanel'; import { TreatmentRailSection } from '@/components/ui/treatment/TreatmentRailSection'; import { AppointmentsStrip } from '@/components/ui/treatment/AppointmentsStrip'; import { NewTreatmentPatientPicker } from '@/components/ui/treatment/NewTreatmentPatientPicker'; import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart'; import { LabCasesDispatchPanel } from '@/components/ui/treatment/LabCasesDispatchPanel'; import { LabShipmentBlockedNotice } from '@/components/ui/treatment/LabShipmentBlockedNotice'; import { LabDispatchAttentionPanel } from '@/components/ui/treatment/LabDispatchAttentionPanel'; import { PastTreatmentsPanel } from '@/components/ui/treatment/PastTreatmentsPanel'; import { TreatmentDetailsEditor } from '@/components/ui/treatment/TreatmentDetailsEditor'; import { TreatmentPreviewCard } from '@/components/ui/treatment/TreatmentPreviewCard'; import { treatmentTypeLabelFromCatalog, treatmentTypeColor } from '@/components/shared/treatmentTypeDisplay'; import { addCalendarDays, compareLocalDayStart, isSameLocalCalendarDay, startOfLocalDay, } from '@/components/appointments/appointmentTime'; import { appointmentsApi } from '@/lib/api/appointments'; import { treatmentCatalogApi } from '@/lib/api/treatment-catalog'; import { prosthesisCatalogApi } from '@/lib/api/prosthesis-catalog'; import { voiceApi } from '@/lib/api/voice'; import { useVoiceCapture } from '@/lib/voice/useVoiceCapture'; import { VoiceReviewSheet } from '@/components/ui/treatment/VoiceReviewSheet'; import type { VoiceApplySelection, VoiceAvailability, VoiceExtractionResult, } from '@/types/voice'; import { treatmentsApi } from '@/lib/api/treatments'; import { notificationsApi } from '@/lib/api/notifications'; import { pickAutoAppointment } from '@/components/shared/treatmentSelection'; import type { DayStripItem } from '@/components/treatment/dayStrip'; import { areDetailsPersistable, defaultTreatmentTypeForAppointment, isDetailReadyForLabDispatch, isDetailTypeSelected, isEmptyDraftDetail, isLabDependentDetailMissingTeeth, areUnscheduledDetailsStripDeletable, } from '@/components/treatment/treatmentDetailRules'; import { applyShiftRange, connectedTeethSet, deriveTeethFromGroups, groupsFromFlatTeeth, linkAdjacentTeeth, linkedEdgesFromGroups, pruneToothProsthesisForGroups, toothEdgeKey, toggleToothInGroups, unlinkAdjacentTeeth, } from '@/components/treatment/toothSelectionGroups'; import type { LabDispatchAttentionItem } from '@/components/treatment/labDispatchAttention'; import { collectLabDispatchAttention } from '@/components/treatment/labDispatchAttention'; import { loadLabDispatchDefaults, rememberLastLab } from '@/components/treatment/labDispatchDefaults'; import { canEditTreatment, canViewTreatment, canAccessDashboardRoute } from '@/components/shared/permissions'; import { scrollWithinMainScrollContainer } from '@/components/shared/scrollWithinMain'; import { useMarkTabReadOnVisit, useTabBadgeCounts } from '@/lib/hooks/useTabBadgeCounts'; import { tabBadgesChangedEventName } from '@/lib/tabBadgeUtils'; import { notifyTabBadgesChanged } from '@/lib/tabBadgeUtils'; import { APP_DATE, formatAppDate, formatAppTimeRange } from '@/lib/i18n/format'; import { useAuth } from '@/lib/hooks/useAuth'; import { getUserFacingError } from '@/components/shared/formatApiError'; import { useToast } from '@/lib/hooks/useToast'; import { usePatientSearchQuery } from '@/lib/hooks/usePatientSearchQuery'; import type { Organization } from '@/types/organization'; import type { Patient } from '@/types/patient'; import type { AppointmentRecord } from '@/types/appointment'; import type { ProsthesisCatalogEntry, TreatmentCatalogEntry } from '@/types/treatment-catalog'; import type { FdiToothId, LabCaseDraft, LinkedOrganizationOption, PastLabCase, PastTreatment, PastTreatmentCase, PastTreatmentDetail, TreatmentAppointment, TreatmentDetailDraft, } from '@/types/treatment'; 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; } function withoutEmptyLabCaseDrafts(drafts: LabCaseDraft[]): LabCaseDraft[] { return drafts.filter((lc) => lc.sentAt || Boolean(lc.detailClientId)); } function labCaseDraftsToPast( labCaseDrafts: LabCaseDraft[], details: TreatmentDetailDraft[], ): PastLabCase[] { return labCaseDrafts.map((lc) => { const linkedDetail = lc.detailClientId ? details.find((d) => d.clientId === lc.detailClientId) : undefined; return { id: lc.id ?? lc.clientId, clientId: lc.clientId, destinationOrganizationId: lc.destinationOrganizationId, sentAt: lc.sentAt ?? null, treatmentDetailId: linkedDetail?.id ?? null, detail: linkedDetail ? { id: linkedDetail.id ?? linkedDetail.clientId, clientId: linkedDetail.clientId, treatmentType: linkedDetail.treatmentType, teeth: linkedDetail.teeth, toothSelectionGroups: linkedDetail.toothSelectionGroups, } : null, sends: lc.sends ?? [], }; }); } function enrichDetailsWithLabSendState( details: TreatmentDetailDraft[], labCaseDrafts: LabCaseDraft[], ): TreatmentDetailDraft[] { return details.map((detail) => { const sentLabCase = labCaseDrafts.find( (lc) => lc.sentAt && lc.detailClientId === detail.clientId, ); if (!sentLabCase) return detail; return { ...detail, labCaseId: sentLabCase.id ?? detail.labCaseId, sentAt: sentLabCase.sentAt ?? detail.sentAt, sends: sentLabCase.sends ?? detail.sends, sendToOrganizationIds: sentLabCase.destinationOrganizationId ? [sentLabCase.destinationOrganizationId] : detail.sendToOrganizationIds, }; }); } function buildWorkspaceSnapshot( ctx: { id: string; patientId: string; treatmentAt: string; appointmentId: string | null }, details: TreatmentDetailDraft[], labCaseDrafts: LabCaseDraft[], title: string, id?: string, ): PastTreatment { const detailsForPreview = enrichDetailsWithLabSendState(details, labCaseDrafts); return { ...detailsToPreviewTreatment(detailsForPreview, { id: id ?? `preview-${ctx.id}`, title, patientId: ctx.patientId, treatmentAt: ctx.treatmentAt, }), appointmentId: ctx.appointmentId, labCases: labCaseDraftsToPast(labCaseDrafts, details), }; } function newDetail(defaultTreatmentType?: string): TreatmentDetailDraft { return { clientId: typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `detail-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, treatmentType: defaultTreatmentType ?? '', teeth: [], toothSelectionGroups: [], comment: '', attachmentMetas: [], sendToOrganizationIds: [], sentAt: null, }; } function newLabCaseDraft(): LabCaseDraft { return { clientId: typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `lab-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, destinationOrganizationId: null, detailClientId: null, toothProsthesis: [], attachmentIds: [], sentAt: null, sends: [], }; } function mapAppointment(record: AppointmentRecord): TreatmentAppointment { return { id: record.id, patientId: record.patientId, patientFirstName: record.patient.firstName, patientLastName: record.patient.lastName, providerUserId: record.providerUserId, startAt: record.startAt, endAt: record.endAt, purpose: record.purpose, }; } function mapDetailFromApi(d: PastTreatmentCase): TreatmentDetailDraft { const teeth = d.teeth; const toothSelectionGroups = groupsFromFlatTeeth(teeth, d.toothSelectionGroups ?? null); return { clientId: d.clientId, id: d.id, treatmentType: d.treatmentType, teeth, toothSelectionGroups, comment: d.notes ?? '', attachmentMetas: d.attachmentMetas ?? [], labCaseId: d.labCaseId ?? null, taskProgress: d.taskProgress ?? null, sendToOrganizationIds: d.destinationOrganizationId ? [d.destinationOrganizationId] : [], sends: d.sends ?? [], sentAt: d.sentAt ?? null, }; } function draftToPastDetail(d: TreatmentDetailDraft): PastTreatmentDetail { return { id: d.id ?? d.clientId, clientId: d.clientId, treatmentType: d.treatmentType, teeth: d.teeth, toothSelectionGroups: d.toothSelectionGroups, notes: d.comment, attachmentMetas: d.attachmentMetas, labCaseId: d.labCaseId ?? null, taskProgress: d.taskProgress ?? null, sends: d.sends ?? [], sentAt: d.sentAt ?? null, }; } function mapLabCaseDraftFromApi(lc: PastLabCase): LabCaseDraft { return { clientId: lc.clientId, id: lc.id, destinationOrganizationId: lc.destinationOrganizationId, detailClientId: lc.detail?.clientId ?? null, toothProsthesis: (lc.toothProsthesis ?? []).map((tp) => ({ detailClientId: lc.detail?.clientId ?? tp.treatmentDetailId ?? '', tooth: tp.tooth, prosthesisTypeCode: tp.prosthesisTypeCode, selectionGroupId: tp.selectionGroupId ?? '', })), attachmentIds: (lc.attachments ?? []).map((a) => a.id), sentAt: lc.sentAt ?? null, sends: lc.sends ?? [], dueDate: lc.dueDate ?? null, taskProgress: lc.taskProgress ?? null, }; } function serializeDetails(details: TreatmentDetailDraft[]) { return JSON.stringify( details.map((d) => ({ clientId: d.clientId, id: d.id, treatmentType: d.treatmentType, teeth: d.teeth, toothSelectionGroups: d.toothSelectionGroups, comment: d.comment, attachmentMetas: d.attachmentMetas, })), ); } function isDetailsDirty( details: TreatmentDetailDraft[], savedSnapshot: string | null, ): boolean { if (savedSnapshot === null) { return details.some((d) => !isEmptyDraftDetail(d)) || details.length > 1; } return serializeDetails(details) !== savedSnapshot; } /** Apply server ids/attachments onto local rows without replacing newer local edits. */ function mergeServerIdsIntoDetails( local: TreatmentDetailDraft[], fromServer: TreatmentDetailDraft[], ): TreatmentDetailDraft[] { const byClientId = new Map(fromServer.map((d) => [d.clientId, d])); return local.map((d) => { const s = byClientId.get(d.clientId); if (!s) return d; return { ...d, id: s.id ?? d.id, labCaseId: s.labCaseId ?? d.labCaseId, taskProgress: s.taskProgress ?? d.taskProgress, attachmentMetas: d.attachmentMetas.length > 0 ? d.attachmentMetas : s.attachmentMetas, }; }); } function detailsToPreviewTreatment( details: TreatmentDetailDraft[], meta: { title: string; patientId: string; treatmentAt: string; id?: string }, ): PastTreatment { return { id: meta.id ?? 'current-draft', patientId: meta.patientId, title: meta.title, treatmentAt: meta.treatmentAt, details: details.map((d, idx) => ({ id: d.id ?? d.clientId ?? `draft-${idx + 1}`, clientId: d.clientId, treatmentType: d.treatmentType, teeth: d.teeth, toothSelectionGroups: d.toothSelectionGroups, notes: d.comment || null, attachmentMetas: d.attachmentMetas, labCaseId: d.labCaseId ?? null, taskProgress: d.taskProgress ?? null, destinationOrganizationId: d.sendToOrganizationIds[0] ?? null, sends: d.sends ?? [], sentAt: d.sentAt ?? null, })), labCases: [], documents: [], }; } interface TreatmentWorkspaceProps { userId: string; currentOrganization: Organization | null; initialAppointmentId?: string | null; initialLabCaseId?: string | null; } export function TreatmentWorkspace({ userId, currentOrganization, initialAppointmentId = null, initialLabCaseId = null, }: TreatmentWorkspaceProps) { const locale = useLocale(); const t = useTranslations('treatment'); const tErrors = useTranslations('errors'); const tPatients = useTranslations('patients'); const router = useRouter(); const { user } = useAuth(); const { showError, showSuccess, messages: toastMessages } = useToast(); const canView = canViewTreatment(currentOrganization); const canEdit = canEditTreatment(currentOrganization); useMarkTabReadOnVisit(); const tabBadgeCounts = useTabBadgeCounts(); const initialLabCasesScopeSetRef = useRef(false); const [stripHidden, setStripHidden] = useState(false); const todayStart = useMemo(() => startOfLocalDay(new Date()), []); const [selectedDay, setSelectedDay] = useState(() => startOfLocalDay(new Date())); const [appointments, setAppointments] = useState([]); const [standaloneTreatments, setStandaloneTreatments] = useState([]); const [apptsLoading, setApptsLoading] = useState(false); const [selectionLocked, setSelectionLocked] = useState(false); const [selectedAppointmentId, setSelectedAppointmentId] = useState(null); const [selectedStandaloneId, setSelectedStandaloneId] = useState(null); const [history, setHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [historyPatientId, setHistoryPatientId] = useState(null); const [patientLabCases, setPatientLabCases] = useState([]); const [patientLabCasesLoading, setPatientLabCasesLoading] = useState(false); const [unreadLabCases, setUnreadLabCases] = useState([]); const [unreadLabCasesLoading, setUnreadLabCasesLoading] = useState(false); const [labCasesScope, setLabCasesScope] = useState('patient'); const [selectedRailLabCaseId, setSelectedRailLabCaseId] = useState(null); const [searchedPatient, setSearchedPatient] = useState | null>(null); const [patientSearchBusy, setPatientSearchBusy] = useState(false); const [newTreatmentPickerOpen, setNewTreatmentPickerOpen] = useState(false); const [creatingStandalone, setCreatingStandalone] = useState(false); const { search: patientSearch, setSearch: setPatientSearch, patients: patientSearchResults, loading: patientSearchLoading, } = usePatientSearchQuery(canView); const [orgs, setOrgs] = useState([]); const [labDependentCodes, setLabDependentCodes] = useState>(new Set()); const [treatmentCatalog, setTreatmentCatalog] = useState([]); const [prosthesisCatalog, setProsthesisCatalog] = useState([]); const treatmentDropdownCatalog = useMemo( () => treatmentCatalog.filter((entry) => entry.availableInTreatment), [treatmentCatalog], ); const [details, setDetails] = useState(() => [newDetail()]); const [labCaseDrafts, setLabCaseDrafts] = useState([]); /** * Lab cases created by confirming a voice result. * * Kept here rather than as a field on LabCaseDraft on purpose: a draft field is dropped * by mapLabCaseDraftFromApi on the first server round-trip, which is exactly the window * where the dispatch panel's remembered-prosthesis default would fire. */ const [voiceConfirmedLabCaseIds, setVoiceConfirmedLabCaseIds] = useState>( () => new Set(), ); const [activeDetailId, setActiveDetailId] = useState(() => details[0].clientId); const [activeLabCaseId, setActiveLabCaseId] = useState(null); const [savedSnapshot, setSavedSnapshot] = useState(null); const [saveStatus, setSaveStatus] = useState<'idle' | 'dirty' | 'saving' | 'saved' | 'error'>('idle'); const [selectedPreviewId, setSelectedPreviewId] = useState(null); const [workspaceMode, setWorkspaceMode] = useState('live'); const selectionLockedRef = useRef(selectionLocked); selectionLockedRef.current = selectionLocked; const detailsRef = useRef(details); detailsRef.current = details; const savedSnapshotRef = useRef(savedSnapshot); savedSnapshotRef.current = savedSnapshot; const autosaveTimerRef = useRef | null>(null); const saveInFlightRef = useRef(false); const saveQueuedRef = useRef(false); const draftHydratingRef = useRef(false); const workspaceModeRef = useRef(workspaceMode); workspaceModeRef.current = workspaceMode; const labCaseDraftsRef = useRef(labCaseDrafts); labCaseDraftsRef.current = labCaseDrafts; const skipNextGetDraftRef = useRef(false); const pendingAppointmentIdRef = useRef(initialAppointmentId); const pendingLabCaseIdRef = useRef(initialLabCaseId); const labPanelRef = useRef(null); const historyRequestRef = useRef(0); /** When set, activeDetailId effect opens this step instead of resetting to treatment. */ const pendingEntryStepRef = useRef(null); useEffect(() => { pendingAppointmentIdRef.current = initialAppointmentId; if (initialAppointmentId) { setSelectedDay(startOfLocalDay(new Date())); setSelectionLocked(false); } }, [initialAppointmentId]); useEffect(() => { pendingLabCaseIdRef.current = initialLabCaseId; }, [initialLabCaseId]); const [sendBusyId, setSendBusyId] = useState(null); const [uploadBusyDetailId, setUploadBusyDetailId] = useState(null); const [organizationSearch, setOrganizationSearch] = useState(''); const [recentOrganizationIds, setRecentOrganizationIds] = useState([]); const [showWholeTreatmentPlan, setShowWholeTreatmentPlan] = useState(false); const [entryStep, setEntryStep] = useState('treatment'); const [voiceAvailability, setVoiceAvailability] = useState(null); const [voiceResult, setVoiceResult] = useState(null); const isDetailLocked = useCallback( (detail: TreatmentDetailDraft) => labCaseDrafts.some((lc) => lc.sentAt && lc.detailClientId === detail.clientId), [labCaseDrafts], ); const activeSentLabCaseId = useMemo(() => { const match = labCaseDrafts.find( (lc) => lc.detailClientId === activeDetailId && lc.sentAt && lc.id, ); return match?.id ?? null; }, [labCaseDrafts, activeDetailId]); const isDirty = useMemo( () => isDetailsDirty(details, savedSnapshot), [details, savedSnapshot], ); const AUTOSAVE_DEBOUNCE_MS = 600; const selectedAppointment = useMemo( () => appointments.find((a) => a.id === selectedAppointmentId) ?? null, [appointments, selectedAppointmentId], ); const voice = useVoiceCapture({ // The locale the clinician is actually reading and speaking in. Sent explicitly so // the server's ASR hint, catalog labels and week start match what the microphone was // offered for — req.user.language can drift from the URL locale. locale, maxMs: voiceAvailability?.maxRecordingMs ?? null, onExtracted: setVoiceResult, onError: (error) => showError(getUserFacingError(error, tErrors, t('voiceFailed'))), }); /** Absence is the unavailable state — the Add button then renders unsplit. */ const voiceForEditor = voiceAvailability?.enabled && voiceAvailability.locales.includes(locale) ? voice : undefined; const selectedStandalone = useMemo( () => standaloneTreatments.find((t) => t.id === selectedStandaloneId) ?? null, [standaloneTreatments, selectedStandaloneId], ); const liveTreatmentId = selectedStandalone?.id ?? null; const hasLiveContext = Boolean(selectedAppointment || selectedStandalone); const walkInLabel = t('walkIn'); const activePatient = useMemo(() => { if (selectedAppointment) { return { id: selectedAppointment.patientId, firstName: selectedAppointment.patientFirstName, lastName: selectedAppointment.patientLastName, purpose: selectedAppointment.purpose, isWalkIn: false, }; } if (selectedStandalone) { const isWalkIn = Boolean(selectedStandalone.patient?.isWalkIn); return { id: selectedStandalone.patientId, firstName: isWalkIn ? walkInLabel : (selectedStandalone.patient?.firstName ?? ''), lastName: isWalkIn ? '' : (selectedStandalone.patient?.lastName ?? ''), purpose: selectedStandalone.details[0]?.treatmentType, isWalkIn, }; } if (searchedPatient) { return { id: searchedPatient.id, firstName: searchedPatient.firstName, lastName: searchedPatient.lastName, purpose: undefined as string | undefined, isWalkIn: false, }; } return null; }, [selectedAppointment, selectedStandalone, searchedPatient, walkInLabel]); const activePatientId = activePatient?.id ?? null; const activePatientName = activePatient ? `${activePatient.firstName} ${activePatient.lastName}`.trim() : null; const unreadUpdatesCount = unreadLabCases.length; const otherPatientsUnreadCount = useMemo( () => unreadLabCases.filter((item) => item.patientId !== activePatientId).length, [unreadLabCases, activePatientId], ); const displayedLabCases = labCasesScope === 'updates' ? unreadLabCases : patientLabCases; const labCasesListLoading = labCasesScope === 'updates' ? unreadLabCasesLoading && unreadLabCases.length === 0 : patientLabCasesLoading && patientLabCases.length === 0; const showLabShipmentsSection = Boolean(activePatient) || unreadUpdatesCount > 0; const labShipmentsSubtitle = labCasesScope === 'updates' ? t('labShipmentsUpdatesScope') : activePatientName ? t('labShipmentsPatientScope', { patientName: activePatientName }) : t('labShipmentsSubtitle'); useEffect(() => { if (selectedStandalone && searchedPatient?.id === selectedStandalone.patientId) { setSearchedPatient(null); } }, [selectedAppointment, selectedStandalone, searchedPatient?.id]); const isViewingPastDay = useMemo( () => compareLocalDayStart(selectedDay, todayStart) < 0, [selectedDay, todayStart], ); const canEditTreatmentForDay = canEdit && hasLiveContext && !isViewingPastDay && workspaceMode === 'live'; const historyPanelItems = history; const activeDetail = useMemo( () => details.find((d) => d.clientId === activeDetailId) ?? details[0] ?? null, [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( () => 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)); useEffect(() => { if (!selectedStandaloneId || draftHydratingRef.current) return; const nextDetails = details.map(draftToPastDetail); setStandaloneTreatments((prev) => { const current = prev.find((row) => row.id === selectedStandaloneId); if (!current) return prev; const prevColor = current.details.find((d) => d.treatmentType?.trim())?.treatmentType?.trim() ?? ''; const nextColor = nextDetails.find((d) => d.treatmentType?.trim())?.treatmentType?.trim() ?? ''; if ( prevColor === nextColor && areUnscheduledDetailsStripDeletable(current.details) === areUnscheduledDetailsStripDeletable(nextDetails) ) { return prev; } return prev.map((row) => row.id === selectedStandaloneId ? { ...row, details: nextDetails } : row, ); }); }, [selectedStandaloneId, details]); const dayStripItems = useMemo(() => { const timed: DayStripItem[] = appointments.map((a) => ({ kind: 'appointment', id: a.id, patientId: a.patientId, patientFirstName: a.patientFirstName, patientLastName: a.patientLastName, patientIsWalkIn: false, colorCode: a.purpose, timeLabel: formatAppTimeRange(a.startAt, a.endAt, locale), subtitle: treatmentTypeLabelFromCatalog(a.purpose, treatmentCatalog), })); const unscheduled: DayStripItem[] = standaloneTreatments.map((tr) => { const isWalkIn = Boolean(tr.patient?.isWalkIn); const sourceDetails = tr.id === selectedStandaloneId && !draftHydratingRef.current ? details : tr.details; const typedDetail = sourceDetails.find((d) => Boolean(d.treatmentType?.trim())); const colorCode = typedDetail?.treatmentType?.trim() ?? ''; return { kind: 'unscheduled' as const, id: tr.id, patientId: tr.patientId, patientFirstName: isWalkIn ? t('walkIn') : (tr.patient?.firstName ?? ''), patientLastName: isWalkIn ? '' : (tr.patient?.lastName ?? ''), patientIsWalkIn: isWalkIn, colorCode, timeLabel: null, subtitle: t('noAppointment'), canDelete: areUnscheduledDetailsStripDeletable(sourceDetails), }; }); return [...timed, ...unscheduled]; }, [ appointments, standaloneTreatments, selectedStandaloneId, details, locale, treatmentCatalog, t, ]); const selectedStripItemId = selectedAppointmentId ?? selectedStandaloneId; const currentDraftPreview = useMemo(() => { if (selectedAppointment) { return buildWorkspaceSnapshot( { id: selectedAppointment.id, patientId: selectedAppointment.patientId, treatmentAt: selectedAppointment.startAt, appointmentId: selectedAppointment.id, }, details, labCaseDrafts, t('treatmentPlanTitle', { patientName: `${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`, }), 'current-draft', ); } if (selectedStandalone) { const name = selectedStandalone.patient?.isWalkIn ? t('walkIn') : `${selectedStandalone.patient?.firstName ?? ''} ${selectedStandalone.patient?.lastName ?? ''}`.trim(); return buildWorkspaceSnapshot( { id: selectedStandalone.id, patientId: selectedStandalone.patientId, treatmentAt: selectedStandalone.treatmentAt, appointmentId: null, }, details, labCaseDrafts, t('treatmentPlanTitle', { patientName: name || t('walkIn') }), selectedStandalone.id, ); } return null; }, [details, labCaseDrafts, selectedAppointment, selectedStandalone, t]); const previewTreatment = useMemo(() => { if (!selectedPreviewId) return currentDraftPreview; const fromHistory = history.find((item) => item.id === selectedPreviewId) ?? historyPanelItems.find((item) => item.id === selectedPreviewId); if ( workspaceMode === 'live' && currentDraftPreview?.appointmentId && (selectedPreviewId === currentDraftPreview.id || fromHistory?.appointmentId === currentDraftPreview.appointmentId) ) { return currentDraftPreview; } return fromHistory ?? currentDraftPreview; }, [selectedPreviewId, history, historyPanelItems, currentDraftPreview, workspaceMode]); const isBrowsing = selectedPreviewId !== null; const labAttentionItems = useMemo( () => collectLabDispatchAttention( labDependentCodes, currentDraftPreview, history, selectedAppointmentId, ), [labDependentCodes, currentDraftPreview, history, selectedAppointmentId], ); const hydrateFromTreatment = useCallback(( treatment: PastTreatment, options?: { seedBlankIfEmpty?: boolean }, ) => { const mapped = treatment.details.map(mapDetailFromApi); const nextDetails = mapped.length > 0 ? mapped : options?.seedBlankIfEmpty ? [newDetail(defaultTreatmentTypeForAppointment(undefined, treatmentCatalog))] : []; setDetails(nextDetails); setActiveDetailId((prev) => { const stillExists = nextDetails.some((d) => d.clientId === prev); return stillExists ? prev : (nextDetails[0]?.clientId ?? ''); }); setSavedSnapshot(serializeDetails(nextDetails)); const mappedLabCases = withoutEmptyLabCaseDrafts( (treatment.labCases ?? []).map(mapLabCaseDraftFromApi), ); setLabCaseDrafts(mappedLabCases); setActiveLabCaseId(mappedLabCases[0]?.clientId ?? null); setOrganizationSearch(''); setSaveStatus('idle'); }, [treatmentCatalog]); const selectedTeethSet = useMemo(() => new Set(activeDetail?.teeth ?? []), [activeDetail?.teeth]); const connectedSelectedTeeth = useMemo( () => connectedTeethSet(activeDetail?.toothSelectionGroups ?? []), [activeDetail?.toothSelectionGroups], ); const linkedToothEdges = useMemo( () => linkedEdgesFromGroups(activeDetail?.toothSelectionGroups ?? []), [activeDetail?.toothSelectionGroups], ); const rangeAnchorRef = useRef(null); const wholePlanTeethSet = useMemo(() => { const set = new Set(); for (const detail of details) { for (const tooth of detail.teeth) set.add(tooth); } return set; }, [details]); const wholePlanToothColors = useMemo(() => { const colors: Partial> = {}; for (let i = 0; i < details.length; i++) { const detail = details[i]; const catalogIndex = treatmentCatalog.findIndex((e) => e.code === detail.treatmentType); const color = treatmentTypeColor(detail.treatmentType, catalogIndex >= 0 ? catalogIndex : i); for (const tooth of detail.teeth) { if (!(tooth in colors)) colors[tooth] = color; } } return colors; }, [details, treatmentCatalog]); const wholePlanLinkedEdges = useMemo(() => { const edges = new Set(); for (const detail of details) { for (const key of linkedEdgesFromGroups(detail.toothSelectionGroups ?? [])) { edges.add(key); } } return edges; }, [details]); const chartSelectedTeeth = showWholeTreatmentPlan ? wholePlanTeethSet : selectedTeethSet; const chartToothColors = showWholeTreatmentPlan ? wholePlanToothColors : undefined; const chartLinkedEdges = showWholeTreatmentPlan ? wholePlanLinkedEdges : linkedToothEdges; // 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; 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'); } }, [entryStep, showLabWizardStep]); // Sync active lab shipment when the selected treatment detail changes. useEffect(() => { const match = labCaseDrafts.find((lc) => lc.detailClientId === activeDetailId); setActiveLabCaseId(match?.clientId ?? null); }, [activeDetailId, labCaseDrafts]); useEffect(() => { let cancelled = false; setApptsLoading(true); void (async () => { try { const dayStart = startOfLocalDay(selectedDay); const dayEnd = addCalendarDays(dayStart, 1); const [apptsResponse, standaloneResponse] = await Promise.all([ appointmentsApi.list({ from: dayStart.toISOString(), to: dayEnd.toISOString(), }), treatmentsApi.listDayStandalone({ from: dayStart.toISOString(), to: dayEnd.toISOString(), }), ]); if (cancelled) return; const list = apptsResponse.data .filter((a) => a.providerUserId === userId) .map(mapAppointment); setAppointments(list); setStandaloneTreatments(standaloneResponse.data); if (!selectionLockedRef.current) { const pendingId = pendingAppointmentIdRef.current; if (pendingId && list.some((appointment) => appointment.id === pendingId)) { setSelectedAppointmentId(pendingId); setSelectedStandaloneId(null); setSelectionLocked(true); pendingAppointmentIdRef.current = null; router.replace('/treatment', { scroll: false }); } else { pendingAppointmentIdRef.current = null; const autoAppt = pickAutoAppointment(list, selectedDay); if (autoAppt) { setSelectedAppointmentId(autoAppt); setSelectedStandaloneId(null); } else if (standaloneResponse.data[0]) { setSelectedAppointmentId(null); setSelectedStandaloneId(standaloneResponse.data[0].id); } else { setSelectedAppointmentId(null); setSelectedStandaloneId(null); } } } } catch (error: unknown) { if (!cancelled) { showError(getUserFacingError(error, tErrors, t('errorLoadAppointments'))); } } finally { if (!cancelled) setApptsLoading(false); } })(); return () => { cancelled = true; }; }, [userId, selectedDay, showError, t, router]); useEffect(() => { const today = startOfLocalDay(new Date()); if (!isSameLocalCalendarDay(selectedDay, today) || selectionLocked) return; const id = window.setInterval(() => { setSelectedAppointmentId((prev) => { const next = pickAutoAppointment(appointments, selectedDay); return next ?? prev; }); }, 60_000); return () => window.clearInterval(id); }, [selectedDay, appointments, selectionLocked]); useEffect(() => { let cancelled = false; void (async () => { try { const [orgsResponse, catalogResponse, prosthesisResponse, voiceResponse] = await Promise.all([ treatmentsApi.listLinkedOrganizations(), treatmentCatalogApi.list(), prosthesisCatalogApi.list(), // Voice availability comes from the API, not a NEXT_PUBLIC_* var: those are // baked in at build time, so enabling a locale would need a frontend rebuild. // A failure here must not take the whole treatment tab down with it. voiceApi.availability().catch(() => null), ]); if (cancelled) return; setVoiceAvailability(voiceResponse?.data ?? null); setOrgs(orgsResponse.data); setTreatmentCatalog(catalogResponse.data); setProsthesisCatalog(prosthesisResponse.data); setLabDependentCodes( new Set(catalogResponse.data.filter((entry) => entry.labDependent).map((entry) => entry.code)), ); const lastLabId = loadLabDispatchDefaults(currentOrganization?.id).lastLabId; if (lastLabId && orgsResponse.data.some((o) => o.id === lastLabId && o.active)) { setRecentOrganizationIds((prev) => prev.includes(lastLabId) ? prev : [lastLabId, ...prev].slice(0, 10), ); } } catch (error: unknown) { if (!cancelled) { showError(getUserFacingError(error, tErrors, t('errorLoadOrgs'))); } } })(); return () => { cancelled = true; }; }, [showError, t, currentOrganization?.id]); useEffect(() => { if (!activePatientId) { setHistoryPatientId(null); setHistory([]); setHistoryLoading(false); setPatientLabCases([]); setPatientLabCasesLoading(false); setSelectedRailLabCaseId(null); return; } setHistoryPatientId((prev) => { if (prev !== activePatientId) { setHistory([]); setHistoryLoading(true); } return activePatientId; }); }, [activePatientId]); const refreshPatientLabCases = useCallback( async (patientId: string, options?: { silent?: boolean }) => { const silent = options?.silent ?? false; if (!silent) setPatientLabCasesLoading(true); try { const response = await treatmentsApi.listPatientLabCases(patientId); setPatientLabCases(response.data ?? []); } catch { if (!silent) setPatientLabCases([]); } finally { if (!silent) setPatientLabCasesLoading(false); } }, [], ); const refreshUnreadLabCases = useCallback(async (options?: { silent?: boolean }) => { const silent = options?.silent ?? false; if (!silent) setUnreadLabCasesLoading(true); try { const response = await treatmentsApi.listUnreadLabCases(); setUnreadLabCases(response.data ?? []); } catch { if (!silent) setUnreadLabCases([]); } finally { if (!silent) setUnreadLabCasesLoading(false); } }, []); const handleLabCaseMarkedRead = useCallback((labCaseId: string) => { setPatientLabCases((prev) => prev.map((item) => (item.labCaseId === labCaseId ? { ...item, hasUnread: false } : item)), ); setUnreadLabCases((prev) => prev.filter((item) => item.labCaseId !== labCaseId)); }, []); const activeLabCaseSummary = useMemo(() => { if (activeSentLabCaseId) { return patientLabCases.find((item) => item.labCaseId === activeSentLabCaseId) ?? null; } return patientLabCases.find((item) => item.detailClientId === activeDetailId) ?? null; }, [patientLabCases, activeSentLabCaseId, activeDetailId]); const handleLabCaseSummaryChange = useCallback((summary: PatientLabCaseSummary) => { setPatientLabCases((prev) => prev.map((item) => (item.labCaseId === summary.labCaseId ? summary : item)), ); }, []); useEffect(() => { void refreshUnreadLabCases(); }, [refreshUnreadLabCases]); useEffect(() => { if (initialLabCasesScopeSetRef.current) return; if ((tabBadgeCounts.treatment ?? 0) > 0) { setLabCasesScope('updates'); initialLabCasesScopeSetRef.current = true; return; } if (activePatientId) { setLabCasesScope('patient'); initialLabCasesScopeSetRef.current = true; } }, [tabBadgeCounts.treatment, activePatientId]); useEffect(() => { if (unreadLabCases.length === 0 && labCasesScope === 'updates' && activePatientId) { setLabCasesScope('patient'); } if (unreadLabCases.length > 0 && !activePatientId && labCasesScope === 'patient') { setLabCasesScope('updates'); } }, [unreadLabCases.length, labCasesScope, activePatientId]); useEffect(() => { if (!historyPatientId) return; const requestId = ++historyRequestRef.current; setHistoryLoading(true); void (async () => { try { const response = await treatmentsApi.listPatientHistory(historyPatientId, 50); if (requestId !== historyRequestRef.current) return; setHistory(response.data); void refreshPatientLabCases(historyPatientId); } catch (error: unknown) { if (requestId !== historyRequestRef.current) return; showError(getUserFacingError(error, tErrors, t('errorLoadHistory'))); } finally { if (requestId === historyRequestRef.current) { setHistoryLoading(false); } } })(); }, [historyPatientId, refreshPatientLabCases, showError, t, tErrors]); useEffect(() => { const onBadgesChanged = () => { if (historyPatientId) void refreshPatientLabCases(historyPatientId, { silent: true }); void refreshUnreadLabCases({ silent: true }); }; window.addEventListener(tabBadgesChangedEventName(), onBadgesChanged); return () => window.removeEventListener(tabBadgesChangedEventName(), onBadgesChanged); }, [historyPatientId, refreshPatientLabCases, refreshUnreadLabCases]); useEffect(() => { const appointmentId = selectedAppointment?.id; const treatmentId = selectedStandalone?.id; if ((!appointmentId && !treatmentId) || workspaceMode !== 'live') return; if (skipNextGetDraftRef.current) { skipNextGetDraftRef.current = false; return; } let cancelled = false; draftHydratingRef.current = true; if (autosaveTimerRef.current) { clearTimeout(autosaveTimerRef.current); autosaveTimerRef.current = null; } void (async () => { try { const response = appointmentId ? await treatmentsApi.getDraft(appointmentId) : await treatmentsApi.getDraftByTreatment(treatmentId!); if (cancelled) return; if (response.data?.details?.length) { const mapped = response.data.details.map(mapDetailFromApi); setDetails(mapped); setActiveDetailId((prev) => { const stillExists = mapped.some((d) => d.clientId === prev); return stillExists ? prev : mapped[0].clientId; }); setSavedSnapshot(serializeDetails(mapped)); } else if (response.data) { setDetails([]); setActiveDetailId(''); setSavedSnapshot(serializeDetails([])); } else { const first = newDetail( defaultTreatmentTypeForAppointment(selectedAppointment?.purpose, treatmentCatalog), ); setDetails([first]); setActiveDetailId(first.clientId); setSavedSnapshot(serializeDetails([first])); } const mappedLabCases = withoutEmptyLabCaseDrafts( (response.data?.labCases ?? []).map(mapLabCaseDraftFromApi), ); setLabCaseDrafts(mappedLabCases); setActiveLabCaseId(mappedLabCases[0]?.clientId ?? null); setOrganizationSearch(''); setSaveStatus('idle'); } catch (error: unknown) { if (!cancelled) { showError(getUserFacingError(error, tErrors, t('errorLoadDraft'))); } } finally { if (!cancelled) { draftHydratingRef.current = false; } } })(); return () => { cancelled = true; draftHydratingRef.current = false; }; }, [ selectedAppointment?.id, selectedAppointment?.purpose, selectedStandalone?.id, workspaceMode, treatmentCatalog, showError, t, tErrors, ]); const persistDraft = useCallback( async (options?: { force?: boolean }) => { if (!selectedAppointment && !selectedStandalone) throw new Error('No visit selected'); const patientName = selectedAppointment ? `${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}` : selectedStandalone?.patient?.isWalkIn ? t('walkIn') : `${selectedStandalone?.patient?.firstName ?? ''} ${selectedStandalone?.patient?.lastName ?? ''}`.trim(); const patientId = selectedAppointment?.patientId ?? selectedStandalone!.patientId; const treatmentAt = selectedAppointment?.startAt ?? selectedStandalone!.treatmentAt; const currentDetails = detailsRef.current; const dirty = isDetailsDirty(currentDetails, savedSnapshotRef.current); if (!options?.force && !dirty) { return detailsToPreviewTreatment(currentDetails, { title: t('treatmentPlanTitle', { patientName }), patientId, treatmentAt, }); } if (!areDetailsPersistable(currentDetails)) { return detailsToPreviewTreatment(currentDetails, { title: t('treatmentPlanTitle', { patientName }), patientId, treatmentAt, }); } const payload = { details: currentDetails.map( ({ clientId, id, treatmentType, teeth, toothSelectionGroups, comment, attachmentMetas }) => ({ clientId, id, treatmentType, teeth, toothSelectionGroups, comment, attachmentIds: attachmentMetas.map((a) => a.id), }), ), }; const response = selectedAppointment ? await treatmentsApi.saveDraft(selectedAppointment.id, payload) : await treatmentsApi.saveDraftByTreatment(selectedStandalone!.id, payload); const mapped = response.data.details.map(mapDetailFromApi); const sentSnapshot = serializeDetails(currentDetails); const localNow = detailsRef.current; // If the user changed details while this save was in flight (e.g. removed a // detail), do not clobber local state with the stale response. if (serializeDetails(localNow) === sentSnapshot) { setDetails(mapped); setActiveDetailId((prev) => { const stillExists = mapped.some((d) => d.clientId === prev); return stillExists ? prev : (mapped[0]?.clientId ?? ''); }); setSavedSnapshot(serializeDetails(mapped)); } else { const merged = mergeServerIdsIntoDetails(localNow, mapped); detailsRef.current = merged; setDetails(merged); setActiveDetailId((prev) => { const stillExists = merged.some((d) => d.clientId === prev); return stillExists ? prev : (merged[0]?.clientId ?? ''); }); // Keep dirty so the queued autosave persists the newer local state. } return response.data; }, [selectedAppointment, selectedStandalone, t], ); const refreshHistory = useCallback(async (patientId: string, options?: { silentLabCases?: boolean }) => { const requestId = ++historyRequestRef.current; try { const response = await treatmentsApi.listPatientHistory(patientId, 50); if (requestId !== historyRequestRef.current) return; setHistory(response.data); void refreshPatientLabCases(patientId, { silent: options?.silentLabCases ?? false }); } catch (error: unknown) { if (requestId !== historyRequestRef.current) return; showError(getUserFacingError(error, tErrors, t('errorLoadHistory'))); } }, [refreshPatientLabCases, showError, t, tErrors]); const runDraftSave = useCallback(async () => { if (!hasLiveContext || saveInFlightRef.current) { if (saveInFlightRef.current) saveQueuedRef.current = true; return; } if ( !isDetailsDirty(detailsRef.current, savedSnapshotRef.current) || !areDetailsPersistable(detailsRef.current) ) { return; } saveInFlightRef.current = true; setSaveStatus('saving'); try { await persistDraft(); setSaveStatus('saved'); if (historyPatientId) { await refreshHistory(historyPatientId); } } catch (error: unknown) { setSaveStatus('error'); showError(getUserFacingError(error, tErrors, t('errorSaveDraft'))); throw error; } finally { saveInFlightRef.current = false; if (saveQueuedRef.current) { saveQueuedRef.current = false; if (isDetailsDirty(detailsRef.current, savedSnapshotRef.current)) { void runDraftSave(); } } } }, [hasLiveContext, persistDraft, showError, t, historyPatientId, refreshHistory]); const flushDraftSave = useCallback(async (): Promise => { if (autosaveTimerRef.current) { clearTimeout(autosaveTimerRef.current); autosaveTimerRef.current = null; } if (workspaceModeRef.current !== 'live' || !hasLiveContext || !canEditTreatmentForDay) { return true; } while (saveInFlightRef.current) { await new Promise((resolve) => setTimeout(resolve, 50)); } if (!isDetailsDirty(detailsRef.current, savedSnapshotRef.current)) { return true; } try { await runDraftSave(); return true; } catch { return window.confirm(t('confirmDiscard')); } }, [hasLiveContext, canEditTreatmentForDay, runDraftSave, t]); useEffect(() => { if (draftHydratingRef.current || !canEditTreatmentForDay || !hasLiveContext) { return; } if (!isDirty) { return; } setSaveStatus('dirty'); if (autosaveTimerRef.current) clearTimeout(autosaveTimerRef.current); autosaveTimerRef.current = setTimeout(() => { autosaveTimerRef.current = null; void runDraftSave(); }, AUTOSAVE_DEBOUNCE_MS); return () => { if (autosaveTimerRef.current) { clearTimeout(autosaveTimerRef.current); autosaveTimerRef.current = null; } }; }, [details, isDirty, canEditTreatmentForDay, hasLiveContext, runDraftSave]); const resetToLiveContext = useCallback(() => { setWorkspaceMode('live'); setSelectedPreviewId(null); }, []); const onPickAppointment = useCallback( (id: string) => { void (async () => { const ok = await flushDraftSave(); if (!ok) return; draftHydratingRef.current = true; resetToLiveContext(); setSearchedPatient(null); setSelectionLocked(true); setSelectedAppointmentId(id); setSelectedStandaloneId(null); })(); }, [flushDraftSave, resetToLiveContext], ); const onPickStripItem = useCallback( (item: DayStripItem) => { void (async () => { const ok = await flushDraftSave(); if (!ok) return; draftHydratingRef.current = true; resetToLiveContext(); setSearchedPatient(null); setSelectionLocked(true); if (item.kind === 'appointment') { setSelectedAppointmentId(item.id); setSelectedStandaloneId(null); } else { setSelectedAppointmentId(null); setSelectedStandaloneId(item.id); } })(); }, [flushDraftSave, resetToLiveContext], ); const createStandaloneTreatment = useCallback( async (opts: { patientId?: string; walkIn?: boolean }) => { if (creatingStandalone) return; const ok = await flushDraftSave(); if (!ok) return; setCreatingStandalone(true); try { const created = await treatmentsApi.createStandalone({ ...opts, treatmentAt: selectedDay.toISOString(), }); resetToLiveContext(); setSearchedPatient(null); setNewTreatmentPickerOpen(false); setSelectionLocked(true); setSelectedAppointmentId(null); setSelectedStandaloneId(created.data.id); setStandaloneTreatments((prev) => prev.some((row) => row.id === created.data.id) ? prev : [...prev, created.data], ); skipNextGetDraftRef.current = true; draftHydratingRef.current = true; hydrateFromTreatment(created.data, { seedBlankIfEmpty: true }); draftHydratingRef.current = false; if (created.data.patientId && !created.data.patient?.isWalkIn) { await refreshHistory(created.data.patientId); } } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorCreateTreatment'))); } finally { setCreatingStandalone(false); } }, [ creatingStandalone, flushDraftSave, selectedDay, resetToLiveContext, hydrateFromTreatment, refreshHistory, showError, t, tErrors, ], ); const deleteStandaloneTreatment = useCallback( async (item: DayStripItem) => { if (!canEdit || item.kind !== 'unscheduled' || !item.canDelete) return; if (!window.confirm(t('confirmDeleteEmptyTreatment'))) return; if (autosaveTimerRef.current) { clearTimeout(autosaveTimerRef.current); autosaveTimerRef.current = null; } const wasSelected = selectedStandaloneId === item.id; if (wasSelected) { draftHydratingRef.current = true; } try { while (saveInFlightRef.current) { await new Promise((resolve) => setTimeout(resolve, 50)); } // Chip-delete / cleared type can look empty in the strip before autosave // finishes. Persist [] first so DELETE /treatments/:id (empty-only) succeeds. await treatmentsApi.saveDraftByTreatment(item.id, { details: [] }); await treatmentsApi.deleteStandalone(item.id); setStandaloneTreatments((prev) => prev.filter((row) => row.id !== item.id)); if (wasSelected) { resetToLiveContext(); setSelectedStandaloneId(null); setSelectedAppointmentId(null); } } catch (error: unknown) { if (wasSelected) { draftHydratingRef.current = false; } showError(getUserFacingError(error, tErrors, t('errorDeleteTreatment'))); } }, [ canEdit, resetToLiveContext, selectedStandaloneId, showError, t, tErrors, ], ); const canAccessOrganizations = canAccessDashboardRoute(currentOrganization, '/organizations'); const onSelectDay = useCallback( (day: Date) => { void (async () => { const ok = await flushDraftSave(); if (!ok) return; const patientIdToRefresh = historyPatientId; resetToLiveContext(); setSearchedPatient(null); setNewTreatmentPickerOpen(false); setSelectionLocked(false); setSelectedAppointmentId(null); setSelectedStandaloneId(null); setSelectedDay(startOfLocalDay(day)); if (patientIdToRefresh) { await refreshHistory(patientIdToRefresh); } })(); }, [flushDraftSave, resetToLiveContext, historyPatientId, refreshHistory], ); const handleSelectPreviewTreatment = useCallback((treatment: PastTreatment) => { setSelectedPreviewId(treatment.id); }, []); const exitBrowse = useCallback(() => { setSelectedPreviewId(null); }, []); const loadTreatmentIntoWorkspace = useCallback( async ( treatment: PastTreatment, focusDetailClientId?: string, options?: { scrollToLabPanel?: boolean }, ) => { const ok = workspaceModeRef.current === 'live' ? await flushDraftSave() : true; if (!ok) return false; let treatmentToLoad = treatment; try { const draftResponse = treatment.appointmentId ? await treatmentsApi.getDraft(treatment.appointmentId) : await treatmentsApi.getDraftByTreatment(treatment.id); if (draftResponse.data) { treatmentToLoad = draftResponse.data; } } catch { // Fall back to the history snapshot when draft cannot be loaded. } const isHistorical = isTreatmentDayHistorical(treatmentToLoad.treatmentAt, todayStart); setWorkspaceMode(isHistorical ? 'historical' : 'live'); setSelectedPreviewId(null); const nextDay = startOfLocalDay(new Date(treatmentToLoad.treatmentAt)); setSelectedDay((prev) => (compareLocalDayStart(prev, nextDay) === 0 ? prev : nextDay)); setSelectionLocked(true); if (treatmentToLoad.appointmentId ?? treatment.appointmentId) { setSelectedAppointmentId(treatmentToLoad.appointmentId ?? treatment.appointmentId ?? null); setSelectedStandaloneId(null); } else { setSelectedAppointmentId(null); setSelectedStandaloneId(treatmentToLoad.id); setStandaloneTreatments((prev) => prev.some((row) => row.id === treatmentToLoad.id) ? prev : [...prev, treatmentToLoad], ); } skipNextGetDraftRef.current = true; draftHydratingRef.current = true; if (focusDetailClientId && options?.scrollToLabPanel !== false) { pendingEntryStepRef.current = 'lab'; } hydrateFromTreatment(treatmentToLoad); draftHydratingRef.current = false; if (focusDetailClientId) { if (options?.scrollToLabPanel !== false) { pendingEntryStepRef.current = 'lab'; } setActiveDetailId(focusDetailClientId); const mappedLabCases = withoutEmptyLabCaseDrafts( (treatmentToLoad.labCases ?? []).map(mapLabCaseDraftFromApi), ); const linked = mappedLabCases.find( (lc) => !lc.sentAt && lc.detailClientId === focusDetailClientId, ); if (linked) { setActiveLabCaseId(linked.clientId); } if (options?.scrollToLabPanel !== false) { setEntryStep('lab'); requestAnimationFrame(() => { scrollWithinMainScrollContainer(labPanelRef.current); }); } } return true; }, [flushDraftSave, hydrateFromTreatment, todayStart], ); const handleSelectSearchedPatient = useCallback( (patient: Patient) => { void (async () => { const alreadySelected = selectedAppointment?.patientId === patient.id || selectedStandalone?.patientId === patient.id || searchedPatient?.id === patient.id; if (alreadySelected && hasLiveContext) { return; } setPatientSearchBusy(true); setSearchedPatient({ id: patient.id, firstName: patient.firstName, lastName: patient.lastName, }); try { const response = await treatmentsApi.listPatientHistory(patient.id, 1); const latest = response.data[0]; if (latest) { const ok = await loadTreatmentIntoWorkspace(latest); if (!ok) { setSearchedPatient(null); } return; } await refreshHistory(patient.id); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorLoadHistory'))); setSearchedPatient(null); } finally { setPatientSearchBusy(false); } })(); }, [ selectedAppointment?.patientId, selectedStandalone?.patientId, searchedPatient?.id, hasLiveContext, loadTreatmentIntoWorkspace, refreshHistory, showError, t, tErrors, ], ); const handleLoadIntoWorkspace = useCallback(() => { if (!previewTreatment) return; void loadTreatmentIntoWorkspace(previewTreatment); }, [loadTreatmentIntoWorkspace, previewTreatment]); const handleGoToLabDispatch = useCallback( (item: LabDispatchAttentionItem) => { if (item.isCurrentDraft) { exitBrowse(); pendingEntryStepRef.current = 'lab'; setActiveDetailId(item.detailClientId); const linked = labCaseDrafts.find( (lc) => !lc.sentAt && lc.detailClientId === item.detailClientId, ); if (linked) { setActiveLabCaseId(linked.clientId); } setEntryStep('lab'); requestAnimationFrame(() => { scrollWithinMainScrollContainer(labPanelRef.current); }); return; } const treatment = history.find((entry) => entry.id === item.treatmentId) ?? historyPanelItems.find((entry) => entry.id === item.treatmentId); if (!treatment) return; void loadTreatmentIntoWorkspace(treatment, item.detailClientId); }, [exitBrowse, history, historyPanelItems, labCaseDrafts, loadTreatmentIntoWorkspace], ); const handleSelectPatientLabCase = useCallback( (item: PatientLabCaseSummary) => { void (async () => { setSelectedRailLabCaseId(item.labCaseId); // Ensure a patient context is established before we potentially clear the last unread update, // so the rail section doesn't briefly unmount/collapse. if (item.patientId && item.patientId !== activePatientId) { setSearchedPatient({ id: item.patientId, firstName: item.patientFirstName, lastName: item.patientLastName, }); } try { await notificationsApi.markCaseRead(item.labCaseId); notifyTabBadgesChanged(); handleLabCaseMarkedRead(item.labCaseId); } catch { // Non-blocking — workspace navigation still proceeds. } let treatment = history.find((entry) => entry.id === item.treatmentId) ?? historyPanelItems.find((entry) => entry.id === item.treatmentId) ?? (selectedAppointment?.id === item.appointmentId ? currentDraftPreview : null); if (!treatment && item.patientId) { try { const response = await treatmentsApi.listPatientHistory(item.patientId, 50); treatment = response.data.find((entry) => entry.id === item.treatmentId) ?? null; } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorLoadHistory'))); return; } } if (!treatment?.appointmentId) return; if ( selectedAppointmentId === treatment.appointmentId && workspaceMode === 'live' && !isBrowsing ) { pendingEntryStepRef.current = 'lab'; setActiveDetailId(item.detailClientId); const matchingDraft = labCaseDrafts.find((lc) => lc.id === item.labCaseId); if (matchingDraft) { setActiveLabCaseId(matchingDraft.clientId); } setEntryStep('lab'); requestAnimationFrame(() => { scrollWithinMainScrollContainer(labPanelRef.current); }); return; } await loadTreatmentIntoWorkspace(treatment, item.detailClientId, { scrollToLabPanel: true, }); })(); }, [ activePatientId, currentDraftPreview, handleLabCaseMarkedRead, history, historyPanelItems, isBrowsing, labCaseDrafts, loadTreatmentIntoWorkspace, selectedAppointment?.id, selectedAppointmentId, showError, t, tErrors, workspaceMode, ], ); useEffect(() => { const labCaseId = pendingLabCaseIdRef.current; if (!labCaseId) return; const match = unreadLabCases.find((item) => item.labCaseId === labCaseId) ?? patientLabCases.find((item) => item.labCaseId === labCaseId); if (!match) return; pendingLabCaseIdRef.current = null; void handleSelectPatientLabCase(match); }, [unreadLabCases, patientLabCases, handleSelectPatientLabCase]); useEffect(() => { const match = patientLabCases.find((item) => item.detailClientId === activeDetailId); if (match) { setSelectedRailLabCaseId(match.labCaseId); } }, [activeDetailId, patientLabCases]); const uploadForDetail = useCallback( async ( detailClientId: string, files: FileList | File[], onProgress?: (percent: number) => void, ) => { if (!canEditTreatmentForDay || !hasLiveContext) return; const target = detailsRef.current.find((d) => d.clientId === detailClientId); if (target && isDetailLocked(target)) return; const list = files instanceof FileList ? Array.from(files) : files; if (!list.length) return; setUploadBusyDetailId(detailClientId); try { const uploaded = selectedAppointment ? await treatmentsApi.uploadDetailAttachments( selectedAppointment.id, detailClientId, list, onProgress, ) : await treatmentsApi.uploadDetailAttachmentsByTreatment( selectedStandalone!.id, detailClientId, list, onProgress, ); setDetails((prev) => prev.map((d) => d.clientId === detailClientId ? { ...d, attachmentMetas: [...d.attachmentMetas, ...uploaded.data] } : d, ), ); showSuccess(t('successFilesUploaded', { count: uploaded.data.length })); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorUpload'))); } finally { setUploadBusyDetailId(null); } }, [canEditTreatmentForDay, isDetailLocked, hasLiveContext, selectedAppointment, selectedStandalone, showSuccess, showError, t, tErrors], ); const persistLabCases = useCallback( async (savedTreatment: PastTreatment, draftsOverride?: LabCaseDraft[]) => { if (!selectedAppointment && !selectedStandalone) throw new Error('No visit selected'); const drafts = draftsOverride ?? labCaseDrafts; const detailIdByClientId = new Map( savedTreatment.details.map((d) => [d.clientId, d.id]), ); const payload = drafts .map((lc) => { if (!lc.detailClientId) return null; const treatmentDetailId = detailIdByClientId.get(lc.detailClientId); if (!treatmentDetailId) return null; return { clientId: lc.clientId, id: lc.id, destinationOrganizationId: lc.destinationOrganizationId ?? undefined, treatmentDetailId, toothProsthesis: lc.toothProsthesis .map((tp) => { const detailId = detailIdByClientId.get(tp.detailClientId); if (!detailId) return null; return { treatmentDetailId: detailId, tooth: tp.tooth, prosthesisTypeCode: tp.prosthesisTypeCode, selectionGroupId: tp.selectionGroupId ?? '', }; }) .filter( ( row, ): row is { treatmentDetailId: string; tooth: string; prosthesisTypeCode: string; selectionGroupId: string; } => row !== null, ), attachmentIds: lc.attachmentIds, dueDate: lc.dueDate ?? null, }; }) .filter((row): row is NonNullable => row !== null); if (payload.length === 0) { return savedTreatment; } const response = selectedAppointment ? await treatmentsApi.saveLabCases(selectedAppointment.id, { labCases: payload, }) : await treatmentsApi.saveLabCasesByTreatment(selectedStandalone!.id, { labCases: payload, }); const mapped = withoutEmptyLabCaseDrafts(response.data.labCases.map(mapLabCaseDraftFromApi)); setLabCaseDrafts(mapped); setActiveLabCaseId((prev) => { if (prev && mapped.some((lc) => lc.clientId === prev)) return prev; return mapped[0]?.clientId ?? null; }); return response.data; }, [labCaseDrafts, selectedAppointment, selectedStandalone], ); const handleLabCasesChange = useCallback( (next: LabCaseDraft[]) => { const prevCleaned = withoutEmptyLabCaseDrafts(labCaseDrafts); const cleaned = withoutEmptyLabCaseDrafts(next); setLabCaseDrafts(cleaned); if (!cleaned.some((lc) => lc.detailClientId === activeDetailId)) { setActiveLabCaseId((prev) => prev && cleaned.some((lc) => lc.clientId === prev) ? prev : null, ); } const removedPersistedDraft = prevCleaned.some( (lc) => lc.id && !cleaned.some((row) => row.clientId === lc.clientId), ); if ( removedPersistedDraft && hasLiveContext && canEditTreatmentForDay ) { void (async () => { try { const saved = await persistDraft({ force: true }); await persistLabCases(saved, cleaned); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorSaveLabShipments'))); } })(); } }, [ activeDetailId, canEditTreatmentForDay, labCaseDrafts, persistDraft, persistLabCases, selectedAppointment, showError, t, tErrors, ], ); /** * Voice entry. * * Confirm always appends a NEW detail — it never edits an existing one, and never * touches onAddDetail. Nothing is created until this runs, so cancelling or a failed * recording leaves the chip strip untouched. */ const applyVoiceResult = useCallback( (result: VoiceExtractionResult, selection: VoiceApplySelection) => { const detail = newDetail( defaultTreatmentTypeForAppointment(selectedAppointment?.purpose, treatmentCatalog), ); // Ticked rows land on top of the seeded defaults, so unticking the type row leaves // the appointment-purpose default rather than a blank. if (selection.treatmentType && result.treatmentType) { detail.treatmentType = result.treatmentType; } if (selection.teeth) { detail.teeth = [...result.teeth]; detail.toothSelectionGroups = result.toothSelectionGroups.map((group) => ({ ...group, teeth: [...group.teeth], })); } if (selection.comment && result.comment) { detail.comment = result.comment; } const nextDetails = [...detailsRef.current, detail]; setDetails(nextDetails); // persistDraft reads detailsRef, and setDetails has not rendered yet. The codebase // already writes this ref imperatively after a save for the same reason. 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. const wantsLabDraft = (selection.prosthesis && result.prosthesis) || (selection.lab && result.labId) || (selection.dueDate && result.dueDate); if (wantsLabDraft) { const draft = newLabCaseDraft(); draft.detailClientId = detail.clientId; if (selection.lab && result.labId) { draft.destinationOrganizationId = result.labId; } if (selection.dueDate && result.dueDate) { draft.dueDate = result.dueDate; } if (selection.prosthesis && result.prosthesis) { // byTooth keys are plain strings; the group's teeth are FdiToothId. const groupOf = (tooth: string) => result.toothSelectionGroups.find((group) => (group.teeth as readonly string[]).includes(tooth), )?.groupId ?? ''; // Only teeth that actually landed on the detail. Unticking "teeth" while // leaving "prosthesis" ticked would otherwise attach prosthesis rows for teeth // the treatment does not contain — nothing downstream filters them, and they // would reach task generation as work for teeth nobody is treating. const detailTeeth = new Set(detail.teeth); draft.toothProsthesis = Object.entries(result.prosthesis.byTooth) .filter(([tooth]) => detailTeeth.has(tooth)) .map(([tooth, prosthesisTypeCode]) => ({ detailClientId: detail.clientId, tooth, prosthesisTypeCode, selectionGroupId: groupOf(tooth), })); } const updatedLabCases = [...labCaseDrafts, draft]; setLabCaseDrafts(updatedLabCases); // The sheet already showed the clinician exactly what this case would contain, so // the dispatch panel must not top it up with a remembered default afterwards. setVoiceConfirmedLabCaseIds((prev) => new Set(prev).add(draft.clientId)); // Every other path that creates a lab draft persists it immediately, and the // autosave effect only watches `details`. Left in state alone, the destination // lab, the due date and the whole prosthesis map vanish on the next reload — // silently, because the detail itself does survive. void (async () => { try { const saved = await persistDraft({ force: true }); // persistDraft returns a *preview* treatment rather than saving when the // details are not persistable — one blank detail, the kind the workspace opens // with, is enough. A preview's detail id falls back to the client id, so // posting lab cases against it would send the server an id it has never seen // and fail the whole save. Check what came back, not the precondition, so this // holds for every early return persistDraft has. const savedDetail = saved.details.find((d) => d.clientId === detail.clientId); if (!savedDetail?.id || savedDetail.id === detail.clientId) return; await persistLabCases(saved, updatedLabCases); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorSaveLabShipments'))); } })(); } setVoiceResult(null); }, [ labCaseDrafts, persistDraft, persistLabCases, selectedAppointment?.purpose, showError, t, tErrors, treatmentCatalog, ], ); const handleRemoveDetail = useCallback( (detailClientId: string) => { if (!canEditTreatmentForDay) return; const idx = details.findIndex((d) => d.clientId === detailClientId); if (idx < 0) return; const target = details[idx]; if (!target || isDetailLocked(target)) return; if (!window.confirm(t('confirmRemoveDetail'))) return; const nextDetails = details.filter((d) => d.clientId !== detailClientId); const nextActive = activeDetailId === detailClientId ? (nextDetails[Math.min(idx, nextDetails.length - 1)]?.clientId ?? '') : activeDetailId; // Sync ref before any persist triggered by lab-case cleanup (same tick). detailsRef.current = nextDetails; setDetails(nextDetails); setActiveDetailId(nextActive); if (nextDetails.length === 0) setEntryStep('treatment'); if (labCaseDrafts.some((lc) => lc.detailClientId === detailClientId)) { handleLabCasesChange( withoutEmptyLabCaseDrafts( labCaseDrafts.filter((lc) => lc.detailClientId !== detailClientId), ), ); } }, [ activeDetailId, canEditTreatmentForDay, details, handleLabCasesChange, isDetailLocked, labCaseDrafts, setEntryStep, t, ], ); const handleAddLabCase = useCallback(async () => { if (!canEditTreatmentForDay || !hasLiveContext) return; const cleaned = withoutEmptyLabCaseDrafts(labCaseDrafts); const existing = cleaned.find( (lc) => !lc.sentAt && lc.detailClientId === activeDetailId, ); if (existing) { setActiveLabCaseId(existing.clientId); return; } const activeDetail = details.find((d) => d.clientId === activeDetailId); if ( activeDetail && isLabDependentDetailMissingTeeth(activeDetail, labDependentCodes) ) { showError(t('labShipmentBlockedBody')); return; } const shouldIncludeActive = Boolean( activeDetail && isDetailReadyForLabDispatch(activeDetail, labDependentCodes), ); const orphan = cleaned.find((lc) => !lc.sentAt && !lc.detailClientId); if (orphan && shouldIncludeActive) { const updatedLabCases = cleaned.map((lc) => lc.clientId === orphan.clientId ? { ...lc, detailClientId: activeDetailId } : lc, ); setLabCaseDrafts(updatedLabCases); setActiveLabCaseId(orphan.clientId); try { const saved = await persistDraft({ force: true }); await persistLabCases(saved, updatedLabCases); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorSaveLabShipments'))); } return; } const lastLabId = loadLabDispatchDefaults(currentOrganization?.id).lastLabId; const lastLabStillActive = lastLabId ? orgs.some((o) => o.id === lastLabId && o.active) : false; const next: LabCaseDraft = { ...newLabCaseDraft(), detailClientId: shouldIncludeActive ? activeDetailId : null, destinationOrganizationId: lastLabStillActive ? lastLabId! : null, attachmentIds: activeDetail?.attachmentMetas.map((a) => a.id) ?? [], }; const updatedLabCases = [...cleaned, next]; setLabCaseDrafts(updatedLabCases); setActiveLabCaseId(next.clientId); try { const saved = await persistDraft({ force: true }); await persistLabCases(saved, updatedLabCases); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorSaveLabShipments'))); } }, [ activeDetailId, canEditTreatmentForDay, currentOrganization?.id, details, labCaseDrafts, labDependentCodes, orgs, persistDraft, persistLabCases, selectedAppointment, showError, t, tErrors, ]); // Auto-open shipment draft when entering Lab (no manual "Add lab shipment" click). useEffect(() => { if (entryStep !== 'lab') return; if (!canEditTreatmentForDay || !hasLiveContext) return; const activeDetail = details.find((d) => d.clientId === activeDetailId); if (!activeDetail || !isDetailReadyForLabDispatch(activeDetail, labDependentCodes)) return; if (isLabDependentDetailMissingTeeth(activeDetail, labDependentCodes)) 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; const hasDraft = labCaseDrafts.some((lc) => lc.detailClientId === activeDetailId); if (hasDraft) return; void handleAddLabCase(); }, [ activeDetailId, canEditTreatmentForDay, details, entryStep, handleAddLabCase, isDetailLocked, labCaseDrafts, labDependentCodes, selectedAppointment, ]); const handleSendLabCase = useCallback( async (labCase: LabCaseDraft, comment?: string) => { if (!canEditTreatmentForDay || !hasLiveContext) return; if (!labCase.destinationOrganizationId) { showError(t('errorChooseOrg')); return; } if (!labCase.detailClientId) { showError(t('errorLabCaseNeedsDetails')); return; } setSendBusyId(labCase.clientId); try { if (autosaveTimerRef.current) { clearTimeout(autosaveTimerRef.current); autosaveTimerRef.current = null; } while (saveInFlightRef.current) { await new Promise((resolve) => setTimeout(resolve, 50)); } const saved = await persistDraft({ force: true }); const afterLabCases = await persistLabCases(saved); const refreshedLabCase = afterLabCases.labCases.find( (lc) => lc.clientId === labCase.clientId || lc.id === labCase.id, ); if (!refreshedLabCase?.id) throw new Error(t('errorSendCase')); const trimmedComment = comment?.trim(); if (trimmedComment) { await treatmentsApi.addLabCaseComment(refreshedLabCase.id, { body: trimmedComment }); } const response = await treatmentsApi.sendLabCase(refreshedLabCase.id); const draftResponse = selectedAppointment ? await treatmentsApi.getDraft(selectedAppointment.id) : await treatmentsApi.getDraftByTreatment(selectedStandalone!.id); if (draftResponse.data?.details?.length) { const mapped = draftResponse.data.details.map(mapDetailFromApi); detailsRef.current = mapped; setDetails(mapped); setActiveDetailId((prev) => { const stillExists = mapped.some((d) => d.clientId === prev); return stillExists ? prev : (mapped[0]?.clientId ?? ''); }); setSavedSnapshot(serializeDetails(mapped)); } else { const sentDetailClientId = labCase.detailClientId; setDetails((prev) => { const next = prev.map((detail) => { if (detail.clientId !== sentDetailClientId) return detail; return { ...detail, labCaseId: response.data.id, sentAt: response.data.sentAt, sends: response.data.sends, sendToOrganizationIds: response.data.destinationOrganizationId ? [response.data.destinationOrganizationId] : detail.sendToOrganizationIds, }; }); detailsRef.current = next; return next; }); } setLabCaseDrafts((prev) => prev.map((lc) => lc.clientId === labCase.clientId ? { ...lc, id: response.data.id, sentAt: response.data.sentAt, destinationOrganizationId: response.data.destinationOrganizationId, sends: response.data.sends, taskProgress: response.data.taskProgress ?? lc.taskProgress, } : lc, ), ); setRecentOrganizationIds((prev) => { const orgId = labCase.destinationOrganizationId!; rememberLastLab(currentOrganization?.id, orgId); return [orgId, ...prev.filter((id) => id !== orgId)].slice(0, 10); }); showSuccess(t('successCaseSent')); notifyTabBadgesChanged(); if (selectedAppointment?.patientId) { // Silent: avoid rail/history loading flicker while staying on Lab step. void refreshHistory(selectedAppointment.patientId, { silentLabCases: true }); } else if (selectedStandalone?.patientId) { void refreshHistory(selectedStandalone.patientId, { silentLabCases: true }); } } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorSendCase'))); } finally { setSendBusyId(null); } }, [ canEditTreatmentForDay, selectedAppointment, persistDraft, persistLabCases, refreshHistory, showSuccess, showError, t, tErrors, currentOrganization?.id, ], ); if (!canView) { return (

{t('noPermissionTitle')}

{t('noPermissionBody')}

); } return (

{t('title')}

{!canEdit ? (

{t('subtitleReadOnly')}

) : null}
setStripHidden((s) => !s)} selectedDay={selectedDay} onSelectDay={onSelectDay} items={dayStripItems} selectedItemId={selectedStripItemId} onSelectItem={onPickStripItem} onDeleteUnscheduled={canEdit ? deleteStandaloneTreatment : undefined} treatmentCatalog={treatmentCatalog} loading={apptsLoading} /> {workspaceMode === 'historical' && (

{t('historicalReadonlyNotice')}

)} {isViewingPastDay && workspaceMode === 'live' && (

{t('pastDayNotice')}

)}
{canEdit && !isViewingPastDay ? (
{newTreatmentPickerOpen ? ( createStandaloneTreatment({ walkIn: true })} onSelectPatient={(patient) => createStandaloneTreatment({ patientId: patient.id }) } onCancel={() => setNewTreatmentPickerOpen(false)} /> ) : null}
) : null} {activePatient ? (

{t('selectedPatient')}

{activePatientName}

{activePatient.purpose ? (

{t('purposeLabel')}{' '} {treatmentTypeLabelFromCatalog(activePatient.purpose, treatmentCatalog)}

) : null}
) : (

{apptsLoading ? t('loadingAppointments') : t('selectDayWithAppointment')}

)}
{isBrowsing && previewTreatment ? ( <>

{t('browseBanner', { date: formatAppDate(previewTreatment.treatmentAt, locale, APP_DATE.withWeekday), })}

) : null} {labAttentionItems.length > 0 ? ( ) : null} {activePatient ? ( ) : null} {showLabShipmentsSection ? ( ) : null}
{ const next = newDetail( defaultTreatmentTypeForAppointment(selectedAppointment?.purpose, treatmentCatalog), ); setDetails((prev) => [...prev, next]); setActiveDetailId(next.clientId); setEntryStep('treatment'); }} onRemoveDetail={handleRemoveDetail} onUploadFiles={(files, onProgress) => uploadForDetail(activeDetailId, files, onProgress) } onRemoveAttachment={(attachmentId) => { setDetails((prev) => prev.map((d) => d.clientId === activeDetailId ? { ...d, attachmentMetas: d.attachmentMetas.filter((a) => a.id !== attachmentId), } : d, ), ); setLabCaseDrafts((prev) => prev.map((lc) => lc.detailClientId === activeDetailId && !lc.sentAt ? { ...lc, attachmentIds: lc.attachmentIds.filter((id) => id !== attachmentId), } : lc, ), ); }} showChrome showFields={entryStep === 'treatment'} voice={voiceForEditor} chartLocked={ entryStep === 'treatment' && !activeTypeSelected && !showWholeTreatmentPlan } chartLockMessage={t('selectTypeBeforeTeeth')} chart={ entryStep === 'treatment' ? ( 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')} } onToggle={(fdi, event) => { 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); let nextGroups: ReturnType | null = null; if (event.shiftKey) { const anchor = rangeAnchorRef.current; if (!anchor || anchor === fdi) { rangeAnchorRef.current = fdi; return; } nextGroups = applyShiftRange(currentGroups, anchor, fdi); rangeAnchorRef.current = fdi; if (!nextGroups) return; } else { nextGroups = toggleToothInGroups(currentGroups, fdi); rangeAnchorRef.current = fdi; } 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!, ), }; }), ); }} onToggleLink={(a, b) => { 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); 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, ), }; }), ); }} disabled={ !canEditTreatmentForDay || activeLocked || !activeTypeSelected } /> ) : undefined } stepper={ showLabWizardStep ? (
{ 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); }} />
) : undefined } /> {entryStep === 'lab' ? (
{showLabShipmentBlocked ? : null} {showLabDispatchPanel ? ( { if (historyPatientId) { void refreshPatientLabCases(historyPatientId, { silent: true }); } }} activeLabCaseId={activeLabCaseId} previewConfirmedCaseIds={voiceConfirmedLabCaseIds} onLabCasesChange={handleLabCasesChange} disabled={!canEditTreatmentForDay} canEdit={canEdit} orgs={orgs} organizationSearch={organizationSearch} onOrganizationSearchChange={setOrganizationSearch} recentOrganizationIds={recentOrganizationIds} onRecentOrganizationPick={(orgId) => { setLabCaseDrafts((prev) => { const targetId = activeLabCaseId ?? prev.find((lc) => !lc.sentAt && lc.detailClientId === activeDetailId) ?.clientId; if (!targetId) return prev; return prev.map((lc) => lc.clientId === targetId && !lc.sentAt ? { ...lc, destinationOrganizationId: orgId } : lc, ); }); }} sendBusyId={sendBusyId} onSendLabCase={(lc, comment) => handleSendLabCase(lc, comment)} onCommentError={showError} canInviteLab={canAccessOrganizations} onInviteLab={() => router.push('/organizations?action=invite-lab')} /> ) : (

{t('entryStepLabUnavailable')}

)}
) : null}
{voiceResult ? ( applyVoiceResult(applied, selection)} onDiscard={() => setVoiceResult(null)} /> ) : null}
); }