Merge branch 'master' into feature/cases
This commit is contained in:
@@ -43,29 +43,36 @@ function isTreatmentDayHistorical(treatmentAt: string, todayStart: Date): boolea
|
||||
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) => ({
|
||||
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 ?? [],
|
||||
}));
|
||||
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,
|
||||
}
|
||||
: null,
|
||||
sends: lc.sends ?? [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function enrichDetailsWithLabSendState(
|
||||
@@ -74,7 +81,7 @@ function enrichDetailsWithLabSendState(
|
||||
): TreatmentDetailDraft[] {
|
||||
return details.map((detail) => {
|
||||
const sentLabCase = labCaseDrafts.find(
|
||||
(lc) => lc.sentAt && lc.detailClientIds.includes(detail.clientId),
|
||||
(lc) => lc.sentAt && lc.detailClientId === detail.clientId,
|
||||
);
|
||||
if (!sentLabCase) return detail;
|
||||
return {
|
||||
@@ -142,7 +149,7 @@ function newLabCaseDraft(): LabCaseDraft {
|
||||
? crypto.randomUUID()
|
||||
: `lab-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||
destinationOrganizationId: null,
|
||||
detailClientIds: [],
|
||||
detailClientId: null,
|
||||
toothProsthesis: [],
|
||||
attachmentIds: [],
|
||||
sentAt: null,
|
||||
@@ -179,16 +186,13 @@ function mapDetailFromApi(d: PastTreatmentCase): TreatmentDetailDraft {
|
||||
}
|
||||
|
||||
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),
|
||||
detailClientId: lc.detail?.clientId ?? null,
|
||||
toothProsthesis: (lc.toothProsthesis ?? []).map((tp) => ({
|
||||
detailClientId:
|
||||
detailClientById.get(tp.treatmentDetailId) ?? tp.treatmentDetailId,
|
||||
detailClientId: lc.detail?.clientId ?? tp.treatmentDetailId,
|
||||
tooth: tp.tooth,
|
||||
prosthesisTypeCode: tp.prosthesisTypeCode,
|
||||
})),
|
||||
@@ -313,7 +317,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
|
||||
const isDetailLocked = useCallback(
|
||||
(detail: TreatmentDetailDraft) =>
|
||||
labCaseDrafts.some((lc) => lc.sentAt && lc.detailClientIds.includes(detail.clientId)),
|
||||
labCaseDrafts.some((lc) => lc.sentAt && lc.detailClientId === detail.clientId),
|
||||
[labCaseDrafts],
|
||||
);
|
||||
|
||||
@@ -388,7 +392,9 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
return stillExists ? prev : mapped[0]?.clientId ?? prev;
|
||||
});
|
||||
setSavedSnapshot(serializeDetails(mapped));
|
||||
const mappedLabCases = (treatment.labCases ?? []).map(mapLabCaseDraftFromApi);
|
||||
const mappedLabCases = withoutEmptyLabCaseDrafts(
|
||||
(treatment.labCases ?? []).map(mapLabCaseDraftFromApi),
|
||||
);
|
||||
setLabCaseDrafts(mappedLabCases);
|
||||
setActiveLabCaseId(mappedLabCases[0]?.clientId ?? null);
|
||||
setOrganizationSearch('');
|
||||
@@ -433,7 +439,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
|
||||
// Sync active lab shipment when the selected treatment detail changes.
|
||||
useEffect(() => {
|
||||
const match = labCaseDrafts.find((lc) => lc.detailClientIds.includes(activeDetailId));
|
||||
const match = labCaseDrafts.find((lc) => lc.detailClientId === activeDetailId);
|
||||
setActiveLabCaseId(match?.clientId ?? null);
|
||||
}, [activeDetailId, labCaseDrafts]);
|
||||
|
||||
@@ -581,7 +587,9 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
setSavedSnapshot(serializeDetails([first]));
|
||||
}
|
||||
|
||||
const mappedLabCases = (response.data?.labCases ?? []).map(mapLabCaseDraftFromApi);
|
||||
const mappedLabCases = withoutEmptyLabCaseDrafts(
|
||||
(response.data?.labCases ?? []).map(mapLabCaseDraftFromApi),
|
||||
);
|
||||
setLabCaseDrafts(mappedLabCases);
|
||||
setActiveLabCaseId(mappedLabCases[0]?.clientId ?? null);
|
||||
setOrganizationSearch('');
|
||||
@@ -848,26 +856,35 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
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,
|
||||
}));
|
||||
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,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(row): row is { treatmentDetailId: string; tooth: string; prosthesisTypeCode: string } =>
|
||||
row !== null,
|
||||
),
|
||||
attachmentIds: lc.attachmentIds,
|
||||
};
|
||||
})
|
||||
.filter((row): row is NonNullable<typeof row> => row !== null);
|
||||
|
||||
if (payload.length === 0) {
|
||||
return savedTreatment;
|
||||
@@ -876,7 +893,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
const response = await treatmentsApi.saveLabCases(selectedAppointment.id, {
|
||||
labCases: payload,
|
||||
});
|
||||
const mapped = response.data.labCases.map(mapLabCaseDraftFromApi);
|
||||
const mapped = withoutEmptyLabCaseDrafts(response.data.labCases.map(mapLabCaseDraftFromApi));
|
||||
setLabCaseDrafts(mapped);
|
||||
setActiveLabCaseId((prev) => {
|
||||
if (prev && mapped.some((lc) => lc.clientId === prev)) return prev;
|
||||
@@ -887,18 +904,86 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
[labCaseDrafts, selectedAppointment],
|
||||
);
|
||||
|
||||
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 &&
|
||||
selectedAppointment &&
|
||||
canEditTreatmentForDay
|
||||
) {
|
||||
void (async () => {
|
||||
try {
|
||||
const saved = await persistDraft({ force: true });
|
||||
await persistLabCases(saved, cleaned);
|
||||
} catch (error: unknown) {
|
||||
showError(formatApiErrorMessage(error, t('errorSaveLabShipments')));
|
||||
}
|
||||
})();
|
||||
}
|
||||
},
|
||||
[
|
||||
activeDetailId,
|
||||
canEditTreatmentForDay,
|
||||
labCaseDrafts,
|
||||
persistDraft,
|
||||
persistLabCases,
|
||||
selectedAppointment,
|
||||
showError,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
const handleAddLabCase = useCallback(async () => {
|
||||
if (!canEditTreatmentForDay || !selectedAppointment) 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);
|
||||
const shouldIncludeActive =
|
||||
Boolean(activeDetail && labDependentCodes.has(activeDetail.treatmentType));
|
||||
|
||||
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(formatApiErrorMessage(error, t('errorSaveLabShipments')));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const next: LabCaseDraft = {
|
||||
...newLabCaseDraft(),
|
||||
detailClientIds:
|
||||
activeDetail && labDependentCodes.has(activeDetail.treatmentType)
|
||||
? [activeDetailId]
|
||||
: [],
|
||||
detailClientId: shouldIncludeActive ? activeDetailId : null,
|
||||
};
|
||||
const updatedLabCases = [...labCaseDrafts, next];
|
||||
const updatedLabCases = [...cleaned, next];
|
||||
setLabCaseDrafts(updatedLabCases);
|
||||
setActiveLabCaseId(next.clientId);
|
||||
|
||||
@@ -928,7 +1013,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
showError(t('errorChooseOrg'));
|
||||
return;
|
||||
}
|
||||
if (labCase.detailClientIds.length === 0) {
|
||||
if (!labCase.detailClientId) {
|
||||
showError(t('errorLabCaseNeedsDetails'));
|
||||
return;
|
||||
}
|
||||
@@ -958,10 +1043,10 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
|
||||
const response = await treatmentsApi.sendLabCase(refreshedLabCase.id);
|
||||
|
||||
const sentDetailClientIds = new Set(labCase.detailClientIds);
|
||||
const sentDetailClientId = labCase.detailClientId;
|
||||
setDetails((prev) =>
|
||||
prev.map((detail) => {
|
||||
if (!sentDetailClientIds.has(detail.clientId)) return detail;
|
||||
if (detail.clientId !== sentDetailClientId) return detail;
|
||||
return {
|
||||
...detail,
|
||||
labCaseId: response.data.id,
|
||||
@@ -1155,8 +1240,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
labDependentCodes={labDependentCodes}
|
||||
treatmentCatalog={treatmentCatalog}
|
||||
activeLabCaseId={activeLabCaseId}
|
||||
onActiveLabCaseChange={setActiveLabCaseId}
|
||||
onLabCasesChange={setLabCaseDrafts}
|
||||
onLabCasesChange={handleLabCasesChange}
|
||||
disabled={!canEditTreatmentForDay}
|
||||
canEdit={canEdit}
|
||||
orgs={orgs}
|
||||
@@ -1164,14 +1248,18 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
onOrganizationSearchChange={setOrganizationSearch}
|
||||
recentOrganizationIds={recentOrganizationIds}
|
||||
onRecentOrganizationPick={(orgId) => {
|
||||
if (!activeLabCaseId) return;
|
||||
setLabCaseDrafts((prev) =>
|
||||
prev.map((lc) =>
|
||||
lc.clientId === activeLabCaseId && !lc.sentAt
|
||||
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}
|
||||
onAddLabCase={() => void handleAddLabCase()}
|
||||
|
||||
Reference in New Issue
Block a user