improvement: treatment plan now save on debounce. save treatment button removed.
This commit is contained in:
@@ -463,6 +463,7 @@
|
||||
"sendThisCase": "Send this case",
|
||||
"labDispatchTitle": "Lab dispatch",
|
||||
"labDispatchSubtitle": "Group lab-dependent details into shipments and send them to linked labs.",
|
||||
"labDispatchSendHint": "Shipment grouping is saved when you send to the lab.",
|
||||
"addLabShipment": "Add lab shipment",
|
||||
"labShipmentLabel": "Shipment {n}",
|
||||
"includeDetails": "Include treatment details",
|
||||
@@ -482,6 +483,9 @@
|
||||
"saveDraft": "Save treatment draft",
|
||||
"unsavedChanges": "Unsaved changes",
|
||||
"draftSaved": "Draft saved",
|
||||
"saveStatusSaving": "Saving…",
|
||||
"saveStatusSaved": "All changes saved",
|
||||
"saveStatusError": "Could not save — check your connection",
|
||||
"sendSavesFirst": "Sending is per case and saves first automatically.",
|
||||
"historyTitle": "Previous treatments",
|
||||
"historySubtitle": "Completed treatments for this patient. Each case is listed separately.",
|
||||
|
||||
@@ -463,6 +463,7 @@
|
||||
"sendThisCase": "ارسال این پرونده",
|
||||
"labDispatchTitle": "ارسال به لابراتوار",
|
||||
"labDispatchSubtitle": "جزئیات وابسته به لاب را در محمولهها گروهبندی کرده و به لابراتوارهای متصل ارسال کنید.",
|
||||
"labDispatchSendHint": "گروهبندی محموله هنگام ارسال به لاب ذخیره میشود.",
|
||||
"addLabShipment": "افزودن محموله لاب",
|
||||
"labShipmentLabel": "محموله {n}",
|
||||
"includeDetails": "شامل جزئیات درمان",
|
||||
@@ -482,6 +483,9 @@
|
||||
"saveDraft": "ذخیره پیشنویس درمان",
|
||||
"unsavedChanges": "تغییرات ذخیرهنشده",
|
||||
"draftSaved": "پیشنویس ذخیره شد",
|
||||
"saveStatusSaving": "در حال ذخیره…",
|
||||
"saveStatusSaved": "همه تغییرات ذخیره شد",
|
||||
"saveStatusError": "ذخیره ناموفق بود — اتصال را بررسی کنید",
|
||||
"sendSavesFirst": "ارسال برای هر پرونده به صورت جداگانه است و ابتدا به طور خودکار ذخیره میکند.",
|
||||
"historyTitle": "درمانهای قبلی",
|
||||
"historySubtitle": "درمانهای تکمیل شده برای این بیمار. هر پرونده به طور جداگانه فهرست شده است.",
|
||||
|
||||
@@ -463,6 +463,7 @@
|
||||
"sendThisCase": "Verzend deze case",
|
||||
"labDispatchTitle": "Lab-dispatch",
|
||||
"labDispatchSubtitle": "Groepeer lab-afhankelijke details in zendingen en stuur ze naar gekoppelde labs.",
|
||||
"labDispatchSendHint": "Groepering wordt opgeslagen wanneer u naar het lab verzendt.",
|
||||
"addLabShipment": "Labzending toevoegen",
|
||||
"labShipmentLabel": "Zending {n}",
|
||||
"includeDetails": "Behandeldetails opnemen",
|
||||
@@ -482,6 +483,9 @@
|
||||
"saveDraft": "Behandelconcept opslaan",
|
||||
"unsavedChanges": "Niet-opgeslagen wijzigingen",
|
||||
"draftSaved": "Concept opgeslagen",
|
||||
"saveStatusSaving": "Opslaan…",
|
||||
"saveStatusSaved": "Alle wijzigingen opgeslagen",
|
||||
"saveStatusError": "Opslaan mislukt — controleer uw verbinding",
|
||||
"sendSavesFirst": "Verzenden is per case en slaat eerst automatisch op.",
|
||||
"historyTitle": "Eerdere behandelingen",
|
||||
"historySubtitle": "Voltooide behandelingen voor deze patiënt. Elke case wordt afzonderlijk weergegeven.",
|
||||
|
||||
@@ -25,9 +25,7 @@ interface LabCasesDispatchPanelProps {
|
||||
recentOrganizationIds: string[];
|
||||
onRecentOrganizationPick: (orgId: string) => void;
|
||||
sendBusyId: string | null;
|
||||
saveLabBusy: boolean;
|
||||
onAddLabCase: () => void;
|
||||
onSaveLabCases: () => void;
|
||||
onSendLabCase: (labCase: LabCaseDraft) => void;
|
||||
}
|
||||
|
||||
@@ -46,9 +44,7 @@ export function LabCasesDispatchPanel({
|
||||
recentOrganizationIds,
|
||||
onRecentOrganizationPick,
|
||||
sendBusyId,
|
||||
saveLabBusy,
|
||||
onAddLabCase,
|
||||
onSaveLabCases,
|
||||
onSendLabCase,
|
||||
}: LabCasesDispatchPanelProps) {
|
||||
const t = useTranslations('treatment');
|
||||
@@ -133,7 +129,9 @@ export function LabCasesDispatchPanel({
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-primary">{t('labDispatchTitle')}</h3>
|
||||
<p className="text-xs text-text-muted mt-0.5">{t('labDispatchSubtitle')}</p>
|
||||
<p className="text-xs text-text-muted mt-0.5">
|
||||
{t('labDispatchSubtitle')} {t('labDispatchSendHint')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -299,20 +297,6 @@ export function LabCasesDispatchPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canEdit && (
|
||||
<div className="flex flex-wrap gap-3 pt-2 border-t border-border/60">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={disabled || saveLabBusy}
|
||||
isLoading={saveLabBusy}
|
||||
onClick={onSaveLabCases}
|
||||
>
|
||||
{t('saveLabShipments')}
|
||||
</Button>
|
||||
<p className="text-xs text-text-muted self-center">{t('labDispatchSaveHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,12 +15,10 @@ interface TreatmentDetailsEditorProps {
|
||||
isDetailLocked: (detail: TreatmentDetailDraft) => boolean;
|
||||
disabled: boolean;
|
||||
canEdit: boolean;
|
||||
isDirty: boolean;
|
||||
saveBusy: boolean;
|
||||
saveStatus: 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
|
||||
uploadBusy: boolean;
|
||||
onAddDetail: () => void;
|
||||
onPreview: () => void;
|
||||
onSave: () => void;
|
||||
onUploadFiles: (files: FileList | null) => void;
|
||||
}
|
||||
|
||||
@@ -32,12 +30,10 @@ export function TreatmentDetailsEditor({
|
||||
isDetailLocked,
|
||||
disabled,
|
||||
canEdit,
|
||||
isDirty,
|
||||
saveBusy,
|
||||
saveStatus,
|
||||
uploadBusy,
|
||||
onAddDetail,
|
||||
onPreview,
|
||||
onSave,
|
||||
onUploadFiles,
|
||||
}: TreatmentDetailsEditorProps) {
|
||||
const t = useTranslations('treatment');
|
||||
@@ -175,21 +171,19 @@ export function TreatmentDetailsEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canEdit && (
|
||||
<div className="flex flex-wrap gap-3 pt-2 border-t border-border/60">
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={disabled || saveBusy}
|
||||
isLoading={saveBusy}
|
||||
onClick={onSave}
|
||||
>
|
||||
{t('saveDraft')}
|
||||
</Button>
|
||||
<p className="text-xs text-text-muted self-center">
|
||||
{isDirty ? t('unsavedChanges') : t('draftSaved')}. {t('detailsSaveHint')}
|
||||
</p>
|
||||
</div>
|
||||
{canEdit && saveStatus !== 'idle' && (
|
||||
<p
|
||||
className={`text-xs pt-2 border-t border-border/60 ${
|
||||
saveStatus === 'error' ? 'text-red-500' : 'text-text-muted'
|
||||
}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{saveStatus === 'dirty' && t('unsavedChanges')}
|
||||
{saveStatus === 'saving' && t('saveStatusSaving')}
|
||||
{saveStatus === 'saved' && t('saveStatusSaved')}
|
||||
{saveStatus === 'error' && t('saveStatusError')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -122,6 +122,16 @@ function serializeDetails(details: TreatmentDetailDraft[]) {
|
||||
);
|
||||
}
|
||||
|
||||
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; status: string; id?: string },
|
||||
@@ -180,12 +190,20 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
const [activeDetailId, setActiveDetailId] = useState<string>(() => details[0].clientId);
|
||||
const [activeLabCaseId, setActiveLabCaseId] = useState<string | null>(null);
|
||||
const [savedSnapshot, setSavedSnapshot] = useState<string | null>(null);
|
||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'dirty' | 'saving' | 'saved' | 'error'>('idle');
|
||||
|
||||
const selectionLockedRef = useRef(selectionLocked);
|
||||
selectionLockedRef.current = selectionLocked;
|
||||
|
||||
const [saveBusy, setSaveBusy] = useState(false);
|
||||
const [saveLabBusy, setSaveLabBusy] = useState(false);
|
||||
const detailsRef = useRef(details);
|
||||
detailsRef.current = details;
|
||||
const savedSnapshotRef = useRef(savedSnapshot);
|
||||
savedSnapshotRef.current = savedSnapshot;
|
||||
const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const saveInFlightRef = useRef(false);
|
||||
const saveQueuedRef = useRef(false);
|
||||
const draftHydratingRef = useRef(false);
|
||||
|
||||
const [sendBusyId, setSendBusyId] = useState<string | null>(null);
|
||||
const [uploadBusyDetailId, setUploadBusyDetailId] = useState<string | null>(null);
|
||||
const [organizationSearch, setOrganizationSearch] = useState('');
|
||||
@@ -201,12 +219,12 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
[labCaseDrafts],
|
||||
);
|
||||
|
||||
const isDirty = useMemo(() => {
|
||||
if (savedSnapshot === null) {
|
||||
return details.length !== 1 || details[0].comment !== '' || details[0].teeth.length > 0;
|
||||
}
|
||||
return serializeDetails(details) !== savedSnapshot;
|
||||
}, [details, savedSnapshot]);
|
||||
const isDirty = useMemo(
|
||||
() => isDetailsDirty(details, savedSnapshot),
|
||||
[details, savedSnapshot],
|
||||
);
|
||||
|
||||
const AUTOSAVE_DEBOUNCE_MS = 600;
|
||||
|
||||
const selectedAppointment = useMemo(
|
||||
() => appointments.find((a) => a.id === selectedAppointmentId) ?? null,
|
||||
@@ -342,6 +360,12 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
if (!appointmentId) 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);
|
||||
@@ -366,37 +390,165 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
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, showError, t]);
|
||||
|
||||
const confirmDiscardIfDirty = useCallback(() => {
|
||||
if (!isDirty) return true;
|
||||
return window.confirm(t('confirmDiscard'));
|
||||
}, [isDirty, 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('draftTitle', {
|
||||
patientName: `${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`,
|
||||
}),
|
||||
patientId: selectedAppointment.patientId,
|
||||
treatmentAt: selectedAppointment.startAt,
|
||||
status: 'draft',
|
||||
});
|
||||
}
|
||||
|
||||
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 flushDraftSave = useCallback(async (): Promise<boolean> => {
|
||||
if (autosaveTimerRef.current) {
|
||||
clearTimeout(autosaveTimerRef.current);
|
||||
autosaveTimerRef.current = null;
|
||||
}
|
||||
|
||||
if (!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();
|
||||
return true;
|
||||
} catch {
|
||||
return window.confirm(t('confirmDiscard'));
|
||||
}
|
||||
}, [selectedAppointment, canEditTreatmentForDay, runDraftSave, 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 onPickAppointment = useCallback(
|
||||
(id: string) => {
|
||||
if (!confirmDiscardIfDirty()) return;
|
||||
setSelectionLocked(true);
|
||||
setSelectedAppointmentId(id);
|
||||
void (async () => {
|
||||
const ok = await flushDraftSave();
|
||||
if (!ok) return;
|
||||
setSelectionLocked(true);
|
||||
setSelectedAppointmentId(id);
|
||||
})();
|
||||
},
|
||||
[confirmDiscardIfDirty],
|
||||
[flushDraftSave],
|
||||
);
|
||||
|
||||
const onSelectDay = useCallback(
|
||||
(day: Date) => {
|
||||
if (!confirmDiscardIfDirty()) return;
|
||||
setSelectedDay(day);
|
||||
void (async () => {
|
||||
const ok = await flushDraftSave();
|
||||
if (!ok) return;
|
||||
setSelectedDay(day);
|
||||
})();
|
||||
},
|
||||
[confirmDiscardIfDirty],
|
||||
[flushDraftSave],
|
||||
);
|
||||
|
||||
const uploadForDetail = useCallback(
|
||||
@@ -429,29 +581,6 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
[canEditTreatmentForDay, selectedAppointment, showSuccess, showError, t],
|
||||
);
|
||||
|
||||
const persistDraft = useCallback(async () => {
|
||||
if (!selectedAppointment) throw new Error('No appointment selected');
|
||||
|
||||
const response = await treatmentsApi.saveDraft(selectedAppointment.id, {
|
||||
details: details.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;
|
||||
}, [details, selectedAppointment]);
|
||||
|
||||
const persistLabCases = useCallback(
|
||||
async (savedTreatment: PastTreatment) => {
|
||||
if (!selectedAppointment) throw new Error('No appointment selected');
|
||||
@@ -488,41 +617,6 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
[labCaseDrafts, selectedAppointment],
|
||||
);
|
||||
|
||||
const handleSaveAll = useCallback(async () => {
|
||||
if (!canEditTreatmentForDay || !selectedAppointment) return;
|
||||
setSaveBusy(true);
|
||||
try {
|
||||
await persistDraft();
|
||||
showSuccess(t('successDraftSaved'));
|
||||
} catch (error: unknown) {
|
||||
showError(formatApiErrorMessage(error, t('errorSaveDraft')));
|
||||
} finally {
|
||||
setSaveBusy(false);
|
||||
}
|
||||
}, [canEditTreatmentForDay, selectedAppointment, persistDraft, showSuccess, showError, t]);
|
||||
|
||||
const handleSaveLabCases = useCallback(async () => {
|
||||
if (!canEditTreatmentForDay || !selectedAppointment) return;
|
||||
setSaveLabBusy(true);
|
||||
try {
|
||||
const saved = await persistDraft();
|
||||
await persistLabCases(saved);
|
||||
showSuccess(t('successLabShipmentsSaved'));
|
||||
} catch (error: unknown) {
|
||||
showError(formatApiErrorMessage(error, t('errorSaveLabShipments')));
|
||||
} finally {
|
||||
setSaveLabBusy(false);
|
||||
}
|
||||
}, [
|
||||
canEditTreatmentForDay,
|
||||
selectedAppointment,
|
||||
persistDraft,
|
||||
persistLabCases,
|
||||
showSuccess,
|
||||
showError,
|
||||
t,
|
||||
]);
|
||||
|
||||
const handleSendLabCase = useCallback(
|
||||
async (labCase: LabCaseDraft) => {
|
||||
if (!canEditTreatmentForDay || !selectedAppointment) return;
|
||||
@@ -537,7 +631,15 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
|
||||
setSendBusyId(labCase.clientId);
|
||||
try {
|
||||
const saved = await persistDraft();
|
||||
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(
|
||||
@@ -693,8 +795,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
isDetailLocked={isDetailLocked}
|
||||
disabled={!canEditTreatmentForDay}
|
||||
canEdit={canEdit}
|
||||
isDirty={isDirty}
|
||||
saveBusy={saveBusy}
|
||||
saveStatus={saveStatus}
|
||||
uploadBusy={uploadBusyDetailId === activeDetailId}
|
||||
onAddDetail={() => {
|
||||
const next = newDetail();
|
||||
@@ -702,7 +803,6 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
setActiveDetailId(next.clientId);
|
||||
}}
|
||||
onPreview={openCurrentDraftPreview}
|
||||
onSave={() => void handleSaveAll()}
|
||||
onUploadFiles={(files) => void uploadForDetail(activeDetailId, files ?? [])}
|
||||
/>
|
||||
|
||||
@@ -730,13 +830,11 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
);
|
||||
}}
|
||||
sendBusyId={sendBusyId}
|
||||
saveLabBusy={saveLabBusy}
|
||||
onAddLabCase={() => {
|
||||
const next = newLabCaseDraft();
|
||||
setLabCaseDrafts((prev) => [...prev, next]);
|
||||
setActiveLabCaseId(next.clientId);
|
||||
}}
|
||||
onSaveLabCases={() => void handleSaveLabCases()}
|
||||
onSendLabCase={(lc) => void handleSendLabCase(lc)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user