'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslations } from 'next-intl'; import { AppointmentsStrip } from '@/components/ui/treatment/AppointmentsStrip'; import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart'; import { LabCasesDispatchPanel } from '@/components/ui/treatment/LabCasesDispatchPanel'; import { PastTreatmentsPanel } from '@/components/ui/treatment/PastTreatmentsPanel'; import { TreatmentDetailsEditor } from '@/components/ui/treatment/TreatmentDetailsEditor'; import { TreatmentPreviewCard } from '@/components/ui/treatment/TreatmentPreviewCard'; import { ToastStack } from '@/components/ui/shared/Toast'; import { treatmentTypeLabelFromCatalog, treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay'; import { addCalendarDays, compareLocalDayStart, isSameLocalCalendarDay, startOfLocalDay, } from '@/components/appointments/appointmentTime'; import { appointmentsApi } from '@/lib/api/appointments'; import { treatmentCatalogApi } from '@/lib/api/treatment-catalog'; import { treatmentsApi } from '@/lib/api/treatments'; import { pickAutoAppointment } from '@/components/shared/treatmentSelection'; import { canEditTreatment, canViewTreatment } from '@/components/shared/permissions'; import { formatApiErrorMessage } from '@/components/shared/formatApiError'; import { useToast } from '@/lib/hooks/useToast'; import type { Organization } from '@/types/organization'; import type { AppointmentRecord } from '@/types/appointment'; import type { TreatmentCatalogEntry } from '@/types/treatment-catalog'; import type { FdiToothId, LabCaseDraft, LinkedOrganizationOption, PastLabCase, PastTreatment, PastTreatmentCase, TreatmentAppointment, TreatmentDetailDraft, } from '@/types/treatment'; type WorkspaceMode = 'live' | 'historical'; function isTreatmentDayHistorical(treatmentAt: string, todayStart: Date): boolean { return compareLocalDayStart(new Date(treatmentAt), todayStart) < 0; } function labCaseDraftsToPast( labCaseDrafts: LabCaseDraft[], details: TreatmentDetailDraft[], ): PastLabCase[] { return labCaseDrafts.map((lc) => ({ id: lc.id ?? lc.clientId, clientId: lc.clientId, destinationOrganizationId: lc.destinationOrganizationId, sentAt: lc.sentAt ?? null, treatmentDetailIds: lc.detailClientIds .map((cid) => details.find((d) => d.clientId === cid)?.id) .filter((id): id is string => Boolean(id)), details: lc.detailClientIds.map((cid) => { const d = details.find((x) => x.clientId === cid); return { id: d?.id ?? cid, clientId: cid, treatmentType: d?.treatmentType ?? 'consultation', teeth: d?.teeth ?? [], }; }), sends: lc.sends ?? [], })); } function enrichDetailsWithLabSendState( details: TreatmentDetailDraft[], labCaseDrafts: LabCaseDraft[], ): TreatmentDetailDraft[] { return details.map((detail) => { const sentLabCase = labCaseDrafts.find( (lc) => lc.sentAt && lc.detailClientIds.includes(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( appointment: TreatmentAppointment, details: TreatmentDetailDraft[], labCaseDrafts: LabCaseDraft[], title: string, id?: string, ): PastTreatment { const detailsForPreview = enrichDetailsWithLabSendState(details, labCaseDrafts); return { ...detailsToPreviewTreatment(detailsForPreview, { id: id ?? `preview-${appointment.id}`, title, patientId: appointment.patientId, treatmentAt: appointment.startAt, }), appointmentId: appointment.id, labCases: labCaseDraftsToPast(labCaseDrafts, details), }; } function newDetail(): TreatmentDetailDraft { return { clientId: typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `detail-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, treatmentType: 'restoration', teeth: [], 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, detailClientIds: [], 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 { return { clientId: d.clientId, id: d.id, treatmentType: d.treatmentType, teeth: d.teeth, comment: d.notes ?? '', attachmentMetas: d.attachmentMetas ?? [], labCaseId: d.labCaseId ?? null, sendToOrganizationIds: d.destinationOrganizationId ? [d.destinationOrganizationId] : [], sends: d.sends ?? [], sentAt: d.sentAt ?? null, }; } function mapLabCaseDraftFromApi(lc: PastLabCase): LabCaseDraft { const detailClientById = new Map(lc.details.map((d) => [d.id, d.clientId])); return { clientId: lc.clientId, id: lc.id, destinationOrganizationId: lc.destinationOrganizationId, detailClientIds: lc.details.map((d) => d.clientId), toothProsthesis: (lc.toothProsthesis ?? []).map((tp) => ({ detailClientId: detailClientById.get(tp.treatmentDetailId) ?? tp.treatmentDetailId, tooth: tp.tooth, prosthesisTypeCode: tp.prosthesisTypeCode, })), attachmentIds: (lc.attachments ?? []).map((a) => a.id), sentAt: lc.sentAt ?? null, sends: lc.sends ?? [], }; } function serializeDetails(details: TreatmentDetailDraft[]) { return JSON.stringify( details.map((d) => ({ clientId: d.clientId, id: d.id, treatmentType: d.treatmentType, teeth: d.teeth, comment: d.comment, attachmentMetas: d.attachmentMetas, })), ); } function isDetailsDirty( details: TreatmentDetailDraft[], savedSnapshot: string | null, ): boolean { if (savedSnapshot === null) { return details.length !== 1 || details[0].comment !== '' || details[0].teeth.length > 0; } return serializeDetails(details) !== savedSnapshot; } 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, notes: d.comment || null, attachmentMetas: d.attachmentMetas, labCaseId: d.labCaseId ?? null, destinationOrganizationId: d.sendToOrganizationIds[0] ?? null, sends: d.sends ?? [], sentAt: d.sentAt ?? null, })), labCases: [], documents: [], }; } interface TreatmentWorkspaceProps { userId: string; currentOrganization: Organization | null; } export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWorkspaceProps) { const t = useTranslations('treatment'); const { showError, showSuccess, messages: toastMessages } = useToast(); const canView = canViewTreatment(currentOrganization); const canEdit = canEditTreatment(currentOrganization); const [stripHidden, setStripHidden] = useState(false); const todayStart = useMemo(() => startOfLocalDay(new Date()), []); const [selectedDay, setSelectedDay] = useState(() => startOfLocalDay(new Date())); const [appointments, setAppointments] = useState([]); const [apptsLoading, setApptsLoading] = useState(false); const [selectionLocked, setSelectionLocked] = useState(false); const [selectedAppointmentId, setSelectedAppointmentId] = useState(null); const [history, setHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [historyPatientId, setHistoryPatientId] = useState(null); const [orgs, setOrgs] = useState([]); const [labDependentCodes, setLabDependentCodes] = useState>(new Set()); const [treatmentCatalog, setTreatmentCatalog] = useState([]); const treatmentDropdownCatalog = useMemo( () => treatmentCatalog.filter((entry) => entry.availableInTreatment), [treatmentCatalog], ); const [details, setDetails] = useState(() => [newDetail()]); const [labCaseDrafts, setLabCaseDrafts] = useState([]); 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 [sendBusyId, setSendBusyId] = useState(null); const [uploadBusyDetailId, setUploadBusyDetailId] = useState(null); const [organizationSearch, setOrganizationSearch] = useState(''); const [recentOrganizationIds, setRecentOrganizationIds] = useState([]); const [showWholeTreatmentPlan, setShowWholeTreatmentPlan] = useState(false); const isDetailLocked = useCallback( (detail: TreatmentDetailDraft) => labCaseDrafts.some((lc) => lc.sentAt && lc.detailClientIds.includes(detail.clientId)), [labCaseDrafts], ); 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 isViewingPastDay = useMemo( () => compareLocalDayStart(selectedDay, todayStart) < 0, [selectedDay, todayStart], ); const canEditTreatmentForDay = canEdit && Boolean(selectedAppointment) && !isViewingPastDay && workspaceMode === 'live'; const historyPanelItems = useMemo(() => { return history.filter((item) => { if ( workspaceMode === 'live' && selectedAppointmentId && item.appointmentId === selectedAppointmentId ) { return false; } return true; }); }, [history, selectedAppointmentId, workspaceMode]); const currentDraftPreview = useMemo(() => { if (!selectedAppointment) return null; return buildWorkspaceSnapshot( selectedAppointment, details, labCaseDrafts, t('treatmentPlanTitle', { patientName: `${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`, }), 'current-draft', ); }, [details, labCaseDrafts, selectedAppointment, t]); const previewTreatment = useMemo(() => { if (!selectedPreviewId) return currentDraftPreview; return historyPanelItems.find((item) => item.id === selectedPreviewId) ?? currentDraftPreview; }, [selectedPreviewId, historyPanelItems, currentDraftPreview]); const isPreviewAlreadyOpen = useMemo(() => { if (!previewTreatment?.appointmentId || !selectedAppointmentId) return false; if (selectedAppointmentId !== previewTreatment.appointmentId) return false; if (workspaceMode === 'historical') return true; if (workspaceMode === 'live' && selectedPreviewId === null) return true; if (workspaceMode === 'live' && selectedPreviewId === previewTreatment.id) return true; return false; }, [previewTreatment, selectedAppointmentId, workspaceMode, selectedPreviewId]); const hydrateFromTreatment = useCallback((treatment: PastTreatment) => { const mapped = treatment.details.map(mapDetailFromApi); setDetails(mapped); setActiveDetailId((prev) => { const stillExists = mapped.some((d) => d.clientId === prev); return stillExists ? prev : mapped[0]?.clientId ?? prev; }); setSavedSnapshot(serializeDetails(mapped)); const mappedLabCases = (treatment.labCases ?? []).map(mapLabCaseDraftFromApi); setLabCaseDrafts(mappedLabCases); setActiveLabCaseId(mappedLabCases[0]?.clientId ?? null); setOrganizationSearch(''); setSaveStatus('idle'); }, []); const activeDetail = useMemo( () => details.find((d) => d.clientId === activeDetailId) ?? details[0], [details, activeDetailId], ); const selectedTeethSet = useMemo(() => new Set(activeDetail?.teeth ?? []), [activeDetail?.teeth]); 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 chartSelectedTeeth = showWholeTreatmentPlan ? wholePlanTeethSet : selectedTeethSet; const chartToothColors = showWholeTreatmentPlan ? wholePlanToothColors : undefined; // Reset whole-plan overview when switching details. useEffect(() => { setShowWholeTreatmentPlan(false); }, [activeDetailId]); // Sync active lab shipment when the selected treatment detail changes. useEffect(() => { const match = labCaseDrafts.find((lc) => lc.detailClientIds.includes(activeDetailId)); setActiveLabCaseId(match?.clientId ?? null); }, [activeDetailId, labCaseDrafts]); useEffect(() => { setSelectionLocked(false); }, [selectedDay]); useEffect(() => { let cancelled = false; setApptsLoading(true); void (async () => { try { const dayStart = startOfLocalDay(selectedDay); const dayEnd = addCalendarDays(dayStart, 1); const response = await appointmentsApi.list({ from: dayStart.toISOString(), to: dayEnd.toISOString(), }); if (cancelled) return; const list = response.data .filter((a) => a.providerUserId === userId) .map(mapAppointment); setAppointments(list); if (!selectionLockedRef.current) { setSelectedAppointmentId(pickAutoAppointment(list, selectedDay)); } } catch (error: unknown) { if (!cancelled) { showError(formatApiErrorMessage(error, t('errorLoadAppointments'))); } } finally { if (!cancelled) setApptsLoading(false); } })(); return () => { cancelled = true; }; }, [userId, selectedDay, showError, t]); 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] = await Promise.all([ treatmentsApi.listLinkedOrganizations(), treatmentCatalogApi.list(), ]); if (cancelled) return; setOrgs(orgsResponse.data); setTreatmentCatalog(catalogResponse.data); setLabDependentCodes( new Set(catalogResponse.data.filter((entry) => entry.labDependent).map((entry) => entry.code)), ); } catch (error: unknown) { if (!cancelled) { showError(formatApiErrorMessage(error, t('errorLoadOrgs'))); } } })(); return () => { cancelled = true; }; }, [showError, t]); useEffect(() => { if (selectedAppointment?.patientId) { setHistoryPatientId(selectedAppointment.patientId); } }, [selectedAppointment?.patientId]); useEffect(() => { if (!historyPatientId) return; let cancelled = false; setHistoryLoading(true); void (async () => { try { const response = await treatmentsApi.listPatientHistory(historyPatientId); if (!cancelled) setHistory(response.data); } catch (error: unknown) { if (!cancelled) { showError(formatApiErrorMessage(error, t('errorLoadHistory'))); } } finally { if (!cancelled) setHistoryLoading(false); } })(); return () => { cancelled = true; }; }, [historyPatientId, showError, t]); useEffect(() => { const appointmentId = selectedAppointment?.id; if (!appointmentId || 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 = await treatmentsApi.getDraft(appointmentId); 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 { const first = newDetail(); setDetails([first]); setActiveDetailId(first.clientId); setSavedSnapshot(serializeDetails([first])); } const mappedLabCases = (response.data?.labCases ?? []).map(mapLabCaseDraftFromApi); setLabCaseDrafts(mappedLabCases); setActiveLabCaseId(mappedLabCases[0]?.clientId ?? null); setOrganizationSearch(''); setSaveStatus('idle'); } catch (error: unknown) { if (!cancelled) { showError(formatApiErrorMessage(error, t('errorLoadDraft'))); } } finally { if (!cancelled) { draftHydratingRef.current = false; } } })(); return () => { cancelled = true; draftHydratingRef.current = false; }; }, [selectedAppointment?.id, workspaceMode, showError, t]); const persistDraft = useCallback( async (options?: { force?: boolean }) => { if (!selectedAppointment) throw new Error('No appointment selected'); const currentDetails = detailsRef.current; const dirty = isDetailsDirty(currentDetails, savedSnapshotRef.current); if (!options?.force && !dirty) { return detailsToPreviewTreatment(currentDetails, { title: t('treatmentPlanTitle', { patientName: `${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`, }), patientId: selectedAppointment.patientId, treatmentAt: selectedAppointment.startAt, }); } const response = await treatmentsApi.saveDraft(selectedAppointment.id, { details: currentDetails.map(({ clientId, id, treatmentType, teeth, comment, attachmentMetas }) => ({ clientId, id, treatmentType, teeth, comment, attachmentIds: attachmentMetas.map((a) => a.id), })), }); 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 ?? prev; }); setSavedSnapshot(serializeDetails(mapped)); return response.data; }, [selectedAppointment, t], ); const runDraftSave = useCallback(async () => { if (!selectedAppointment || saveInFlightRef.current) { if (saveInFlightRef.current) saveQueuedRef.current = true; return; } if ( !isDetailsDirty(detailsRef.current, savedSnapshotRef.current) ) { return; } saveInFlightRef.current = true; setSaveStatus('saving'); try { await persistDraft(); setSaveStatus('saved'); } catch (error: unknown) { setSaveStatus('error'); showError(formatApiErrorMessage(error, t('errorSaveDraft'))); throw error; } finally { saveInFlightRef.current = false; if (saveQueuedRef.current) { saveQueuedRef.current = false; if (isDetailsDirty(detailsRef.current, savedSnapshotRef.current)) { void runDraftSave(); } } } }, [selectedAppointment, persistDraft, showError, t]); const refreshHistory = useCallback(async (patientId: string) => { try { const response = await treatmentsApi.listPatientHistory(patientId); setHistory(response.data); } catch (error: unknown) { showError(formatApiErrorMessage(error, t('errorLoadHistory'))); } }, [showError, t]); const flushDraftSave = useCallback(async (): Promise => { if (autosaveTimerRef.current) { clearTimeout(autosaveTimerRef.current); autosaveTimerRef.current = null; } if (workspaceModeRef.current !== 'live' || !selectedAppointment || !canEditTreatmentForDay) { return true; } while (saveInFlightRef.current) { await new Promise((resolve) => setTimeout(resolve, 50)); } if (!isDetailsDirty(detailsRef.current, savedSnapshotRef.current)) { return true; } try { await runDraftSave(); if (historyPatientId) { await refreshHistory(historyPatientId); } return true; } catch { return window.confirm(t('confirmDiscard')); } }, [selectedAppointment, canEditTreatmentForDay, runDraftSave, historyPatientId, refreshHistory, t]); useEffect(() => { if (draftHydratingRef.current || !canEditTreatmentForDay || !selectedAppointment?.id) { 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, selectedAppointment?.id, runDraftSave]); const resetToLiveContext = useCallback(() => { setWorkspaceMode('live'); setSelectedPreviewId(null); }, []); const onPickAppointment = useCallback( (id: string) => { void (async () => { const ok = await flushDraftSave(); if (!ok) return; resetToLiveContext(); setSelectionLocked(true); setSelectedAppointmentId(id); })(); }, [flushDraftSave, resetToLiveContext], ); const onSelectDay = useCallback( (day: Date) => { void (async () => { const ok = await flushDraftSave(); if (!ok) return; const patientIdToRefresh = historyPatientId; resetToLiveContext(); setSelectedDay(day); if (patientIdToRefresh) { await refreshHistory(patientIdToRefresh); } })(); }, [flushDraftSave, resetToLiveContext, historyPatientId, refreshHistory], ); const handleSelectPreviewTreatment = useCallback((treatment: PastTreatment) => { setSelectedPreviewId(treatment.id); }, []); const handleOpenTreatment = useCallback(() => { void (async () => { const treatment = previewTreatment; if (!treatment?.appointmentId) { showError(t('errorNoAppointmentForTreatment')); return; } if (isPreviewAlreadyOpen) return; const ok = workspaceModeRef.current === 'live' ? await flushDraftSave() : true; if (!ok) return; const isHistorical = isTreatmentDayHistorical(treatment.treatmentAt, todayStart); setWorkspaceMode(isHistorical ? 'historical' : 'live'); setSelectedPreviewId(treatment.id); setSelectedDay(startOfLocalDay(new Date(treatment.treatmentAt))); setSelectionLocked(true); setSelectedAppointmentId(treatment.appointmentId); skipNextGetDraftRef.current = true; draftHydratingRef.current = true; hydrateFromTreatment(treatment); draftHydratingRef.current = false; })(); }, [ previewTreatment, isPreviewAlreadyOpen, flushDraftSave, hydrateFromTreatment, showError, t, todayStart, ]); const uploadForDetail = useCallback( async (detailClientId: string, files: FileList | File[]) => { if (!canEditTreatmentForDay || !selectedAppointment) return; const list = files instanceof FileList ? Array.from(files) : files; if (!list.length) return; setUploadBusyDetailId(detailClientId); try { const uploaded = await treatmentsApi.uploadCaseAttachments( selectedAppointment.id, detailClientId, list, ); 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(formatApiErrorMessage(error, t('errorUpload'))); } finally { setUploadBusyDetailId(null); } }, [canEditTreatmentForDay, selectedAppointment, showSuccess, showError, t], ); const persistLabCases = useCallback( async (savedTreatment: PastTreatment, draftsOverride?: LabCaseDraft[]) => { if (!selectedAppointment) throw new Error('No appointment selected'); const drafts = draftsOverride ?? labCaseDrafts; const detailIdByClientId = new Map( savedTreatment.details.map((d) => [d.clientId, d.id]), ); const payload = drafts.map((lc) => ({ clientId: lc.clientId, id: lc.id, destinationOrganizationId: lc.destinationOrganizationId ?? undefined, treatmentDetailIds: lc.detailClientIds .map((clientId) => detailIdByClientId.get(clientId)) .filter((id): id is string => Boolean(id)), toothProsthesis: lc.toothProsthesis .map((tp) => { const detailId = detailIdByClientId.get(tp.detailClientId); if (!detailId) return null; return { treatmentDetailId: detailId, tooth: tp.tooth, prosthesisTypeCode: tp.prosthesisTypeCode, }; }) .filter((row): row is { treatmentDetailId: string; tooth: string; prosthesisTypeCode: string } => row !== null), attachmentIds: lc.attachmentIds, })); if (payload.length === 0) { return savedTreatment; } const response = await treatmentsApi.saveLabCases(selectedAppointment.id, { labCases: payload, }); const mapped = 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], ); const handleAddLabCase = useCallback(async () => { if (!canEditTreatmentForDay || !selectedAppointment) return; const activeDetail = details.find((d) => d.clientId === activeDetailId); const next: LabCaseDraft = { ...newLabCaseDraft(), detailClientIds: activeDetail && labDependentCodes.has(activeDetail.treatmentType) ? [activeDetailId] : [], }; const updatedLabCases = [...labCaseDrafts, next]; setLabCaseDrafts(updatedLabCases); setActiveLabCaseId(next.clientId); try { const saved = await persistDraft({ force: true }); await persistLabCases(saved, updatedLabCases); } catch (error: unknown) { showError(formatApiErrorMessage(error, t('errorSaveLabShipments'))); } }, [ activeDetailId, canEditTreatmentForDay, details, labCaseDrafts, labDependentCodes, persistDraft, persistLabCases, selectedAppointment, showError, t, ]); const handleSendLabCase = useCallback( async (labCase: LabCaseDraft) => { if (!canEditTreatmentForDay || !selectedAppointment) return; if (!labCase.destinationOrganizationId) { showError(t('errorChooseOrg')); return; } if (labCase.detailClientIds.length === 0) { 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 response = await treatmentsApi.sendLabCase(refreshedLabCase.id); const sentDetailClientIds = new Set(labCase.detailClientIds); setDetails((prev) => prev.map((detail) => { if (!sentDetailClientIds.has(detail.clientId)) 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, }; }), ); 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, } : lc, ), ); setRecentOrganizationIds((prev) => { const orgId = labCase.destinationOrganizationId!; return [orgId, ...prev.filter((id) => id !== orgId)].slice(0, 10); }); showSuccess(t('successCaseSent')); } catch (error: unknown) { showError(formatApiErrorMessage(error, t('errorSendCase'))); } finally { setSendBusyId(null); } }, [ canEditTreatmentForDay, selectedAppointment, persistDraft, persistLabCases, showSuccess, showError, t, ], ); if (!canView) { return (

{t('noPermissionTitle')}

{t('noPermissionBody')}

); } return (

{t('title')}

{canEdit ? t('subtitleEditPhase4') : t('subtitleReadOnly')}

setStripHidden((s) => !s)} selectedDay={selectedDay} onSelectDay={onSelectDay} appointments={appointments} selectedAppointmentId={selectedAppointmentId} onSelectAppointment={onPickAppointment} treatmentCatalog={treatmentCatalog} loading={apptsLoading} /> {workspaceMode === 'historical' && (

{t('historicalReadonlyNotice')}

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

{t('pastDayNotice')}

)}
{selectedAppointment ? (

{t('selectedPatient')}

{selectedAppointment.patientFirstName} {selectedAppointment.patientLastName}

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

) : (
{apptsLoading ? t('loadingAppointments') : t('selectDayWithAppointment')}
)}
1 ? ( ) : undefined } onToggle={(fdi) => { if (!canEditTreatmentForDay || isDetailLocked(activeDetail) || showWholeTreatmentPlan) return; setDetails((prev) => prev.map((d) => { if (d.clientId !== activeDetailId) return d; const set = new Set(d.teeth); if (set.has(fdi)) set.delete(fdi); else set.add(fdi); return { ...d, teeth: [...set].sort() as FdiToothId[] }; }), ); }} disabled={!canEditTreatmentForDay || isDetailLocked(activeDetail)} /> { const next = newDetail(); setDetails((prev) => [...prev, next]); setActiveDetailId(next.clientId); }} onUploadFiles={(files) => void uploadForDetail(activeDetailId, files ?? [])} /> { if (!activeLabCaseId) return; setLabCaseDrafts((prev) => prev.map((lc) => lc.clientId === activeLabCaseId && !lc.sentAt ? { ...lc, destinationOrganizationId: orgId } : lc, ), ); }} sendBusyId={sendBusyId} onAddLabCase={() => void handleAddLabCase()} onSendLabCase={(lc) => void handleSendLabCase(lc)} onCommentError={showError} />
); }