improvement: treatment UX fully overhauled.
This commit is contained in:
@@ -3,9 +3,13 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRouter } from '@/i18n/navigation';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
||||
import { AppointmentsStrip } from '@/components/ui/treatment/AppointmentsStrip';
|
||||
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';
|
||||
@@ -20,7 +24,17 @@ 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 {
|
||||
areDetailsPersistable,
|
||||
defaultTreatmentTypeForAppointment,
|
||||
isDetailReadyForLabDispatch,
|
||||
isEmptyDraftDetail,
|
||||
isLabDependentDetailMissingTeeth,
|
||||
} from '@/components/treatment/treatmentDetailRules';
|
||||
import type { LabDispatchAttentionItem } from '@/components/treatment/labDispatchAttention';
|
||||
import { collectLabDispatchAttention } from '@/components/treatment/labDispatchAttention';
|
||||
import { canEditTreatment, canViewTreatment, canAccessDashboardRoute } from '@/components/shared/permissions';
|
||||
import { scrollWithinMainScrollContainer } from '@/components/shared/scrollWithinMain';
|
||||
import { getUserFacingError } from '@/components/shared/formatApiError';
|
||||
import { useToast } from '@/lib/hooks/useToast';
|
||||
import type { Organization } from '@/types/organization';
|
||||
@@ -116,24 +130,13 @@ function buildWorkspaceSnapshot(
|
||||
};
|
||||
}
|
||||
|
||||
function defaultTreatmentTypeForAppointment(
|
||||
purpose: string | undefined,
|
||||
catalog: TreatmentCatalogEntry[],
|
||||
): TreatmentDetailDraft['treatmentType'] {
|
||||
const treatmentOptions = catalog.filter((entry) => entry.availableInTreatment);
|
||||
if (purpose && treatmentOptions.some((entry) => entry.code === purpose)) {
|
||||
return purpose as TreatmentDetailDraft['treatmentType'];
|
||||
}
|
||||
return (treatmentOptions[0]?.code ?? 'restoration') as TreatmentDetailDraft['treatmentType'];
|
||||
}
|
||||
|
||||
function newDetail(defaultTreatmentType?: TreatmentDetailDraft['treatmentType']): TreatmentDetailDraft {
|
||||
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 ?? 'restoration',
|
||||
treatmentType: defaultTreatmentType ?? '',
|
||||
teeth: [],
|
||||
comment: '',
|
||||
attachmentMetas: [],
|
||||
@@ -179,6 +182,7 @@ function mapDetailFromApi(d: PastTreatmentCase): TreatmentDetailDraft {
|
||||
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,
|
||||
@@ -220,7 +224,7 @@ function isDetailsDirty(
|
||||
savedSnapshot: string | null,
|
||||
): boolean {
|
||||
if (savedSnapshot === null) {
|
||||
return details.length !== 1 || details[0].comment !== '' || details[0].teeth.length > 0;
|
||||
return details.some((d) => !isEmptyDraftDetail(d)) || details.length > 1;
|
||||
}
|
||||
return serializeDetails(details) !== savedSnapshot;
|
||||
}
|
||||
@@ -242,6 +246,7 @@ function detailsToPreviewTreatment(
|
||||
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,
|
||||
@@ -316,6 +321,8 @@ export function TreatmentWorkspace({
|
||||
labCaseDraftsRef.current = labCaseDrafts;
|
||||
const skipNextGetDraftRef = useRef(false);
|
||||
const pendingAppointmentIdRef = useRef<string | null>(initialAppointmentId);
|
||||
const labPanelRef = useRef<HTMLDivElement>(null);
|
||||
const historyRequestRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
pendingAppointmentIdRef.current = initialAppointmentId;
|
||||
@@ -360,18 +367,25 @@ export function TreatmentWorkspace({
|
||||
!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 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],
|
||||
);
|
||||
|
||||
const showLabShipmentBlocked = useMemo(
|
||||
() =>
|
||||
Boolean(
|
||||
activeDetail && isLabDependentDetailMissingTeeth(activeDetail, labDependentCodes),
|
||||
),
|
||||
[activeDetail, labDependentCodes],
|
||||
);
|
||||
|
||||
const currentDraftPreview = useMemo<PastTreatment | null>(() => {
|
||||
if (!selectedAppointment) return null;
|
||||
@@ -388,17 +402,29 @@ export function TreatmentWorkspace({
|
||||
|
||||
const previewTreatment = useMemo(() => {
|
||||
if (!selectedPreviewId) return currentDraftPreview;
|
||||
return historyPanelItems.find((item) => item.id === selectedPreviewId) ?? currentDraftPreview;
|
||||
}, [selectedPreviewId, historyPanelItems, currentDraftPreview]);
|
||||
return (
|
||||
history.find((item) => item.id === selectedPreviewId) ??
|
||||
historyPanelItems.find((item) => item.id === selectedPreviewId) ??
|
||||
currentDraftPreview
|
||||
);
|
||||
}, [selectedPreviewId, history, 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 isBrowsing = selectedPreviewId !== null;
|
||||
|
||||
const previewHeading = isBrowsing
|
||||
? t('previewBrowsingTitle')
|
||||
: t('previewCurrentDraft');
|
||||
|
||||
const labAttentionItems = useMemo(
|
||||
() =>
|
||||
collectLabDispatchAttention(
|
||||
labDependentCodes,
|
||||
currentDraftPreview,
|
||||
history,
|
||||
selectedAppointmentId,
|
||||
),
|
||||
[labDependentCodes, currentDraftPreview, history, selectedAppointmentId],
|
||||
);
|
||||
|
||||
const hydrateFromTreatment = useCallback((treatment: PastTreatment) => {
|
||||
const mapped = treatment.details.map(mapDetailFromApi);
|
||||
@@ -417,11 +443,6 @@ export function TreatmentWorkspace({
|
||||
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(() => {
|
||||
@@ -459,10 +480,6 @@ export function TreatmentWorkspace({
|
||||
setActiveLabCaseId(match?.clientId ?? null);
|
||||
}, [activeDetailId, labCaseDrafts]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectionLocked(false);
|
||||
}, [selectedDay]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setApptsLoading(true);
|
||||
@@ -550,28 +567,34 @@ export function TreatmentWorkspace({
|
||||
setHistoryLoading(false);
|
||||
return;
|
||||
}
|
||||
setHistoryPatientId(selectedAppointment.patientId);
|
||||
const nextPatientId = selectedAppointment.patientId;
|
||||
setHistoryPatientId((prev) => {
|
||||
if (prev !== nextPatientId) {
|
||||
setHistory([]);
|
||||
setHistoryLoading(true);
|
||||
}
|
||||
return nextPatientId;
|
||||
});
|
||||
}, [selectedAppointment?.patientId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!historyPatientId) return;
|
||||
let cancelled = false;
|
||||
const requestId = ++historyRequestRef.current;
|
||||
setHistoryLoading(true);
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await treatmentsApi.listPatientHistory(historyPatientId);
|
||||
if (!cancelled) setHistory(response.data);
|
||||
const response = await treatmentsApi.listPatientHistory(historyPatientId, 50);
|
||||
if (requestId !== historyRequestRef.current) return;
|
||||
setHistory(response.data);
|
||||
} catch (error: unknown) {
|
||||
if (!cancelled) {
|
||||
showError(getUserFacingError(error, tErrors, t('errorLoadHistory')));
|
||||
}
|
||||
if (requestId !== historyRequestRef.current) return;
|
||||
showError(getUserFacingError(error, tErrors, t('errorLoadHistory')));
|
||||
} finally {
|
||||
if (!cancelled) setHistoryLoading(false);
|
||||
if (requestId === historyRequestRef.current) {
|
||||
setHistoryLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [historyPatientId, showError, t]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -652,6 +675,16 @@ export function TreatmentWorkspace({
|
||||
});
|
||||
}
|
||||
|
||||
if (!areDetailsPersistable(currentDetails)) {
|
||||
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,
|
||||
@@ -674,6 +707,18 @@ export function TreatmentWorkspace({
|
||||
[selectedAppointment, t],
|
||||
);
|
||||
|
||||
const refreshHistory = useCallback(async (patientId: string) => {
|
||||
const requestId = ++historyRequestRef.current;
|
||||
try {
|
||||
const response = await treatmentsApi.listPatientHistory(patientId, 50);
|
||||
if (requestId !== historyRequestRef.current) return;
|
||||
setHistory(response.data);
|
||||
} catch (error: unknown) {
|
||||
if (requestId !== historyRequestRef.current) return;
|
||||
showError(getUserFacingError(error, tErrors, t('errorLoadHistory')));
|
||||
}
|
||||
}, [showError, t]);
|
||||
|
||||
const runDraftSave = useCallback(async () => {
|
||||
if (!selectedAppointment || saveInFlightRef.current) {
|
||||
if (saveInFlightRef.current) saveQueuedRef.current = true;
|
||||
@@ -681,7 +726,8 @@ export function TreatmentWorkspace({
|
||||
}
|
||||
|
||||
if (
|
||||
!isDetailsDirty(detailsRef.current, savedSnapshotRef.current)
|
||||
!isDetailsDirty(detailsRef.current, savedSnapshotRef.current) ||
|
||||
!areDetailsPersistable(detailsRef.current)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -691,6 +737,9 @@ export function TreatmentWorkspace({
|
||||
try {
|
||||
await persistDraft();
|
||||
setSaveStatus('saved');
|
||||
if (historyPatientId) {
|
||||
await refreshHistory(historyPatientId);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
setSaveStatus('error');
|
||||
showError(getUserFacingError(error, tErrors, t('errorSaveDraft')));
|
||||
@@ -704,16 +753,7 @@ export function TreatmentWorkspace({
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectedAppointment, persistDraft, showError, t]);
|
||||
|
||||
const refreshHistory = useCallback(async (patientId: string) => {
|
||||
try {
|
||||
const response = await treatmentsApi.listPatientHistory(patientId);
|
||||
setHistory(response.data);
|
||||
} catch (error: unknown) {
|
||||
showError(getUserFacingError(error, tErrors, t('errorLoadHistory')));
|
||||
}
|
||||
}, [showError, t]);
|
||||
}, [selectedAppointment, persistDraft, showError, t, historyPatientId, refreshHistory]);
|
||||
|
||||
const flushDraftSave = useCallback(async (): Promise<boolean> => {
|
||||
if (autosaveTimerRef.current) {
|
||||
@@ -735,14 +775,11 @@ export function TreatmentWorkspace({
|
||||
|
||||
try {
|
||||
await runDraftSave();
|
||||
if (historyPatientId) {
|
||||
await refreshHistory(historyPatientId);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return window.confirm(t('confirmDiscard'));
|
||||
}
|
||||
}, [selectedAppointment, canEditTreatmentForDay, runDraftSave, historyPatientId, refreshHistory, t]);
|
||||
}, [selectedAppointment, canEditTreatmentForDay, runDraftSave, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (draftHydratingRef.current || !canEditTreatmentForDay || !selectedAppointment?.id) {
|
||||
@@ -787,6 +824,8 @@ export function TreatmentWorkspace({
|
||||
[flushDraftSave, resetToLiveContext],
|
||||
);
|
||||
|
||||
const canAccessOrganizations = canAccessDashboardRoute(currentOrganization, '/organizations');
|
||||
|
||||
const onSelectDay = useCallback(
|
||||
(day: Date) => {
|
||||
void (async () => {
|
||||
@@ -794,7 +833,8 @@ export function TreatmentWorkspace({
|
||||
if (!ok) return;
|
||||
const patientIdToRefresh = historyPatientId;
|
||||
resetToLiveContext();
|
||||
setSelectedDay(day);
|
||||
setSelectionLocked(false);
|
||||
setSelectedDay(startOfLocalDay(day));
|
||||
if (patientIdToRefresh) {
|
||||
await refreshHistory(patientIdToRefresh);
|
||||
}
|
||||
@@ -807,22 +847,23 @@ export function TreatmentWorkspace({
|
||||
setSelectedPreviewId(treatment.id);
|
||||
}, []);
|
||||
|
||||
const handleOpenTreatment = useCallback(() => {
|
||||
void (async () => {
|
||||
const treatment = previewTreatment;
|
||||
if (!treatment?.appointmentId) {
|
||||
const exitBrowse = useCallback(() => {
|
||||
setSelectedPreviewId(null);
|
||||
}, []);
|
||||
|
||||
const loadTreatmentIntoWorkspace = useCallback(
|
||||
async (treatment: PastTreatment, focusDetailClientId?: string) => {
|
||||
if (!treatment.appointmentId) {
|
||||
showError(t('errorNoAppointmentForTreatment'));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPreviewAlreadyOpen) return;
|
||||
|
||||
const ok = workspaceModeRef.current === 'live' ? await flushDraftSave() : true;
|
||||
if (!ok) return;
|
||||
if (!ok) return false;
|
||||
|
||||
const isHistorical = isTreatmentDayHistorical(treatment.treatmentAt, todayStart);
|
||||
setWorkspaceMode(isHistorical ? 'historical' : 'live');
|
||||
setSelectedPreviewId(treatment.id);
|
||||
setSelectedPreviewId(null);
|
||||
setSelectedDay(startOfLocalDay(new Date(treatment.treatmentAt)));
|
||||
setSelectionLocked(true);
|
||||
setSelectedAppointmentId(treatment.appointmentId);
|
||||
@@ -831,16 +872,58 @@ export function TreatmentWorkspace({
|
||||
draftHydratingRef.current = true;
|
||||
hydrateFromTreatment(treatment);
|
||||
draftHydratingRef.current = false;
|
||||
})();
|
||||
}, [
|
||||
previewTreatment,
|
||||
isPreviewAlreadyOpen,
|
||||
flushDraftSave,
|
||||
hydrateFromTreatment,
|
||||
showError,
|
||||
t,
|
||||
todayStart,
|
||||
]);
|
||||
|
||||
if (focusDetailClientId) {
|
||||
setActiveDetailId(focusDetailClientId);
|
||||
const mappedLabCases = withoutEmptyLabCaseDrafts(
|
||||
(treatment.labCases ?? []).map(mapLabCaseDraftFromApi),
|
||||
);
|
||||
const linked = mappedLabCases.find(
|
||||
(lc) => !lc.sentAt && lc.detailClientId === focusDetailClientId,
|
||||
);
|
||||
if (linked) {
|
||||
setActiveLabCaseId(linked.clientId);
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
scrollWithinMainScrollContainer(labPanelRef.current);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[flushDraftSave, hydrateFromTreatment, showError, t, todayStart],
|
||||
);
|
||||
|
||||
const handleLoadIntoWorkspace = useCallback(() => {
|
||||
if (!previewTreatment) return;
|
||||
void loadTreatmentIntoWorkspace(previewTreatment);
|
||||
}, [loadTreatmentIntoWorkspace, previewTreatment]);
|
||||
|
||||
const handleGoToLabDispatch = useCallback(
|
||||
(item: LabDispatchAttentionItem) => {
|
||||
if (item.isCurrentDraft) {
|
||||
exitBrowse();
|
||||
setActiveDetailId(item.detailClientId);
|
||||
const linked = labCaseDrafts.find(
|
||||
(lc) => !lc.sentAt && lc.detailClientId === item.detailClientId,
|
||||
);
|
||||
if (linked) {
|
||||
setActiveLabCaseId(linked.clientId);
|
||||
}
|
||||
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 uploadForDetail = useCallback(
|
||||
async (detailClientId: string, files: FileList | File[]) => {
|
||||
@@ -984,8 +1067,17 @@ export function TreatmentWorkspace({
|
||||
}
|
||||
|
||||
const activeDetail = details.find((d) => d.clientId === activeDetailId);
|
||||
const shouldIncludeActive =
|
||||
Boolean(activeDetail && labDependentCodes.has(activeDetail.treatmentType));
|
||||
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) {
|
||||
@@ -1068,21 +1160,28 @@ export function TreatmentWorkspace({
|
||||
|
||||
const response = await treatmentsApi.sendLabCase(refreshedLabCase.id);
|
||||
|
||||
const sentDetailClientId = labCase.detailClientId;
|
||||
setDetails((prev) =>
|
||||
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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const draftResponse = await treatmentsApi.getDraft(selectedAppointment.id);
|
||||
if (draftResponse.data?.details?.length) {
|
||||
const mapped = draftResponse.data.details.map(mapDetailFromApi);
|
||||
setDetails(mapped);
|
||||
setSavedSnapshot(serializeDetails(mapped));
|
||||
} else {
|
||||
const sentDetailClientId = labCase.detailClientId;
|
||||
setDetails((prev) =>
|
||||
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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setLabCaseDrafts((prev) =>
|
||||
prev.map((lc) =>
|
||||
@@ -1183,18 +1282,59 @@ export function TreatmentWorkspace({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LabDispatchAttentionPanel
|
||||
items={labAttentionItems}
|
||||
treatmentCatalog={treatmentCatalog}
|
||||
labDependentCodes={labDependentCodes}
|
||||
orgs={orgs}
|
||||
onGoToDispatch={handleGoToLabDispatch}
|
||||
/>
|
||||
|
||||
{isBrowsing && previewTreatment ? (
|
||||
<div className="rounded-[var(--radius-md)] border border-primary/40 bg-primary/5 px-3 py-3 space-y-3">
|
||||
<p className="text-sm text-text-primary">
|
||||
{t('browseBanner', {
|
||||
date: new Date(previewTreatment.treatmentAt).toLocaleDateString(undefined, {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}),
|
||||
})}
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
|
||||
<Button type="button" variant="primary" onClick={handleLoadIntoWorkspace}>
|
||||
{t('loadIntoWorkspace')}
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" onClick={exitBrowse}>
|
||||
{t('backToCurrentDraft')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<TreatmentPreviewCard
|
||||
treatment={previewTreatment}
|
||||
heading={previewHeading}
|
||||
labDependentCodes={labDependentCodes}
|
||||
treatmentCatalog={treatmentCatalog}
|
||||
orgs={orgs}
|
||||
openDisabled={isPreviewAlreadyOpen}
|
||||
onOpen={handleOpenTreatment}
|
||||
/>
|
||||
|
||||
<PastTreatmentsPanel
|
||||
items={historyPanelItems}
|
||||
currentDraft={
|
||||
workspaceMode === 'live' && !isBrowsing ? currentDraftPreview : null
|
||||
}
|
||||
patientName={
|
||||
selectedAppointment
|
||||
? `${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`
|
||||
: undefined
|
||||
}
|
||||
currentAppointmentId={selectedAppointmentId}
|
||||
treatmentCatalog={treatmentCatalog}
|
||||
labDependentCodes={labDependentCodes}
|
||||
orgs={orgs}
|
||||
loading={historyLoading}
|
||||
selectedPreviewId={selectedPreviewId}
|
||||
onSelectTreatment={handleSelectPreviewTreatment}
|
||||
@@ -1208,15 +1348,12 @@ export function TreatmentWorkspace({
|
||||
readOnly={showWholeTreatmentPlan}
|
||||
headerControl={
|
||||
details.length > 1 ? (
|
||||
<label className="flex items-center gap-2 text-[11px] text-text-muted cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showWholeTreatmentPlan}
|
||||
onChange={(e) => setShowWholeTreatmentPlan(e.target.checked)}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
{t('toothChartWholePlan')}
|
||||
</label>
|
||||
<Checkbox
|
||||
checked={showWholeTreatmentPlan}
|
||||
onChange={setShowWholeTreatmentPlan}
|
||||
label={t('toothChartWholePlan')}
|
||||
className="text-[11px] [&_span:last-child]:text-[11px] [&_span:last-child]:text-text-muted"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
onToggle={(fdi) => {
|
||||
@@ -1254,8 +1391,12 @@ export function TreatmentWorkspace({
|
||||
setActiveDetailId(next.clientId);
|
||||
}}
|
||||
onUploadFiles={(files) => void uploadForDetail(activeDetailId, files ?? [])}
|
||||
onCommentError={showError}
|
||||
/>
|
||||
|
||||
<div ref={labPanelRef}>
|
||||
{showLabShipmentBlocked ? <LabShipmentBlockedNotice /> : null}
|
||||
{showLabDispatchPanel ? (
|
||||
<LabCasesDispatchPanel
|
||||
details={details}
|
||||
activeDetailId={activeDetailId}
|
||||
@@ -1288,7 +1429,11 @@ export function TreatmentWorkspace({
|
||||
onAddLabCase={() => void handleAddLabCase()}
|
||||
onSendLabCase={(lc, comment) => void handleSendLabCase(lc, comment)}
|
||||
onCommentError={showError}
|
||||
canInviteLab={canAccessOrganizations}
|
||||
onInviteLab={() => router.push('/organizations?action=invite-lab')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user