feature: Phase 2- splitting the treatment schema into TreatmentDetail and LabCase.
This commit is contained in:
@@ -2,21 +2,26 @@
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { formatCaseSentLines } from '@/components/treatment/caseSendLabel';
|
||||
import type { LinkedOrganizationOption, PastTreatmentCase, TreatmentCaseDraft } from '@/types/treatment';
|
||||
import type { LabCaseSendInfo, LinkedOrganizationOption } from '@/types/treatment';
|
||||
|
||||
interface CaseSentLabelProps {
|
||||
treatmentCase: Pick<
|
||||
PastTreatmentCase | TreatmentCaseDraft,
|
||||
'sends' | 'sendToOrganizationIds' | 'sentAt'
|
||||
>;
|
||||
treatmentCase: {
|
||||
sends?: LabCaseSendInfo[];
|
||||
sendToOrganizationIds?: string[];
|
||||
destinationOrganizationId?: string | null;
|
||||
sentAt?: string | null;
|
||||
};
|
||||
orgs?: LinkedOrganizationOption[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CaseSentLabel({ treatmentCase, orgs, className = 'text-xs text-text-muted' }: CaseSentLabelProps) {
|
||||
const t = useTranslations('treatment');
|
||||
const organizationIds =
|
||||
treatmentCase.sendToOrganizationIds ??
|
||||
(treatmentCase.destinationOrganizationId ? [treatmentCase.destinationOrganizationId] : []);
|
||||
const lines = formatCaseSentLines(treatmentCase.sends, {
|
||||
organizationIds: treatmentCase.sendToOrganizationIds ?? [],
|
||||
organizationIds,
|
||||
sentAt: treatmentCase.sentAt ?? null,
|
||||
orgs,
|
||||
}, t);
|
||||
|
||||
@@ -64,7 +64,7 @@ export function PastTreatmentsPanel({
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
{treatment.cases.map((c, idx) => {
|
||||
{treatment.details.map((c, idx) => {
|
||||
const attachments = c.attachmentMetas ?? [];
|
||||
const typeKey = TREATMENT_TYPE_KEYS[c.treatmentType as keyof typeof TREATMENT_TYPE_KEYS];
|
||||
const typeLabel = typeKey ? t(typeKey) : c.treatmentType;
|
||||
|
||||
@@ -22,7 +22,7 @@ export function TreatmentPreviewCard({ draft, disabled, onPreview }: TreatmentPr
|
||||
const t = useTranslations('treatment');
|
||||
|
||||
const attachmentCount = draft
|
||||
? draft.cases.reduce((n, c) => n + (c.attachmentMetas?.length ?? 0), 0)
|
||||
? draft.details.reduce((n, c) => n + (c.attachmentMetas?.length ?? 0), 0)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
@@ -42,11 +42,11 @@ export function TreatmentPreviewCard({ draft, disabled, onPreview }: TreatmentPr
|
||||
<span className="text-xs text-text-muted tabular-nums shrink-0 capitalize">{draft.status}</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-secondary">
|
||||
{t('caseCount', { n: draft.cases.length })} ·{' '}
|
||||
{t('caseCount', { n: draft.details.length })} ·{' '}
|
||||
{t('attachmentCount', { n: attachmentCount })}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{draft.cases.slice(0, 2).map((c, idx) => {
|
||||
{draft.details.slice(0, 2).map((c, idx) => {
|
||||
const typeKey = TREATMENT_TYPE_KEYS[c.treatmentType as keyof typeof TREATMENT_TYPE_KEYS];
|
||||
const typeLabel = typeKey ? t(typeKey) : c.treatmentType;
|
||||
return (
|
||||
@@ -65,8 +65,8 @@ export function TreatmentPreviewCard({ draft, disabled, onPreview }: TreatmentPr
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{draft.cases.length > 2 && (
|
||||
<p className="text-xs text-text-muted">{t('moreCases', { n: draft.cases.length - 2 })}</p>
|
||||
{draft.details.length > 2 && (
|
||||
<p className="text-xs text-text-muted">{t('moreCases', { n: draft.details.length - 2 })}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,18 +94,20 @@ export function TreatmentPreviewDialog({
|
||||
{t('statusLabel')} {treatment.status}
|
||||
</p>
|
||||
|
||||
{treatment.cases.length === 0 ? (
|
||||
{treatment.details.length === 0 ? (
|
||||
<p className="text-sm text-text-muted">{t('noCases')}</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{treatment.cases.map((c, idx) => {
|
||||
{treatment.details.map((c, idx) => {
|
||||
const key = caseKey(c);
|
||||
const attachments = c.attachmentMetas ?? [];
|
||||
const latestAttachment =
|
||||
attachments.length > 0 ? attachments[attachments.length - 1] : null;
|
||||
const sent = Boolean(c.sentAt);
|
||||
const actionsEnabled = editable && !sent;
|
||||
const selectedOrgIds = getCaseOrgIds?.(key) ?? c.sendToOrganizationIds ?? [];
|
||||
const selectedOrgIds =
|
||||
getCaseOrgIds?.(key) ??
|
||||
(c.destinationOrganizationId ? [c.destinationOrganizationId] : []);
|
||||
const sendExpanded = expandedSendCaseId === key;
|
||||
const comment = c.notes?.trim() ?? '';
|
||||
const attachBusy = uploadBusyCaseId === key;
|
||||
|
||||
@@ -71,17 +71,18 @@ function mapAppointment(record: AppointmentRecord): TreatmentAppointment {
|
||||
};
|
||||
}
|
||||
|
||||
function mapCaseFromApi(c: PastTreatmentCase): TreatmentCaseDraft {
|
||||
function mapDetailFromApi(d: PastTreatmentCase): TreatmentCaseDraft {
|
||||
return {
|
||||
clientId: c.clientId,
|
||||
id: c.id,
|
||||
treatmentType: c.treatmentType,
|
||||
teeth: c.teeth,
|
||||
comment: c.notes ?? '',
|
||||
attachmentMetas: c.attachmentMetas ?? [],
|
||||
sendToOrganizationIds: c.sendToOrganizationIds ?? [],
|
||||
sends: c.sends ?? [],
|
||||
sentAt: c.sentAt ?? null,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,16 +111,19 @@ function casesToPreviewTreatment(
|
||||
title: meta.title,
|
||||
treatmentAt: meta.treatmentAt,
|
||||
status: meta.status,
|
||||
cases: cases.map((c, idx) => ({
|
||||
details: cases.map((c, idx) => ({
|
||||
id: c.id ?? c.clientId ?? `draft-${idx + 1}`,
|
||||
clientId: c.clientId,
|
||||
treatmentType: c.treatmentType,
|
||||
teeth: c.teeth,
|
||||
notes: c.comment || null,
|
||||
attachmentMetas: c.attachmentMetas,
|
||||
sendToOrganizationIds: c.sendToOrganizationIds,
|
||||
labCaseId: c.labCaseId ?? null,
|
||||
destinationOrganizationId: c.sendToOrganizationIds[0] ?? null,
|
||||
sends: c.sends ?? [],
|
||||
sentAt: c.sentAt ?? null,
|
||||
})),
|
||||
labCases: [],
|
||||
documents: [],
|
||||
};
|
||||
}
|
||||
@@ -305,8 +309,8 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
const response = await treatmentsApi.getDraft(appointmentId);
|
||||
if (cancelled) return;
|
||||
|
||||
if (response.data?.cases?.length) {
|
||||
const mapped = response.data.cases.map(mapCaseFromApi);
|
||||
if (response.data?.details?.length) {
|
||||
const mapped = response.data.details.map(mapDetailFromApi);
|
||||
setCases(mapped);
|
||||
setActiveCaseId((prev) => {
|
||||
const stillExists = mapped.some((c) => c.clientId === prev);
|
||||
@@ -387,7 +391,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
if (!selectedAppointment) throw new Error('No appointment selected');
|
||||
|
||||
const response = await treatmentsApi.saveDraft(selectedAppointment.id, {
|
||||
cases: cases.map(({ clientId, id, treatmentType, teeth, comment, attachmentMetas }) => ({
|
||||
details: cases.map(({ clientId, id, treatmentType, teeth, comment, attachmentMetas }) => ({
|
||||
clientId,
|
||||
id,
|
||||
treatmentType,
|
||||
@@ -396,7 +400,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
attachmentIds: attachmentMetas.map((a) => a.id),
|
||||
})),
|
||||
});
|
||||
const mapped = response.data.cases.map(mapCaseFromApi);
|
||||
const mapped = response.data.details.map(mapDetailFromApi);
|
||||
setCases(mapped);
|
||||
setActiveCaseId((prev) => {
|
||||
const stillExists = mapped.some((c) => c.clientId === prev);
|
||||
@@ -422,28 +426,57 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
const handleSendCase = useCallback(
|
||||
async (treatmentCase: TreatmentCaseDraft) => {
|
||||
if (!canEditTreatmentForDay || !selectedAppointment) return;
|
||||
const targets = treatmentCase.sendToOrganizationIds.filter((id) =>
|
||||
const destinationOrgId = treatmentCase.sendToOrganizationIds.find((id) =>
|
||||
orgs.some((o) => o.id === id && o.active),
|
||||
);
|
||||
if (targets.length === 0) {
|
||||
if (!destinationOrgId) {
|
||||
showError(t('errorChooseOrg'));
|
||||
return;
|
||||
}
|
||||
setSendBusyId(treatmentCase.clientId);
|
||||
try {
|
||||
const saved = await persistDraft();
|
||||
const serverCase = saved.cases.find((c) => c.clientId === treatmentCase.clientId);
|
||||
if (!serverCase?.id) throw new Error(t('errorCaseMustSave'));
|
||||
const serverDetail = saved.details.find((c) => c.clientId === treatmentCase.clientId);
|
||||
if (!serverDetail?.id) throw new Error(t('errorCaseMustSave'));
|
||||
|
||||
const labCaseClientId = treatmentCase.labCaseId
|
||||
? saved.labCases.find((lc) => lc.id === treatmentCase.labCaseId)?.clientId
|
||||
: `lab-${treatmentCase.clientId}`;
|
||||
|
||||
const existingLabCase = saved.labCases.find(
|
||||
(lc) =>
|
||||
lc.treatmentDetailIds.includes(serverDetail.id) &&
|
||||
!lc.sentAt,
|
||||
);
|
||||
|
||||
const withLabCases = await treatmentsApi.saveLabCases(selectedAppointment.id, {
|
||||
labCases: [
|
||||
{
|
||||
clientId: existingLabCase?.clientId ?? labCaseClientId ?? `lab-${treatmentCase.clientId}`,
|
||||
id: existingLabCase?.id ?? treatmentCase.labCaseId ?? undefined,
|
||||
destinationOrganizationId: destinationOrgId,
|
||||
treatmentDetailIds: [serverDetail.id],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const labCase = withLabCases.data.labCases.find((lc) =>
|
||||
lc.treatmentDetailIds.includes(serverDetail.id),
|
||||
);
|
||||
if (!labCase?.id) throw new Error(t('errorSendCase'));
|
||||
|
||||
const response = await treatmentsApi.sendLabCase(labCase.id);
|
||||
|
||||
const response = await treatmentsApi.sendCase(serverCase.id, { organizationIds: targets });
|
||||
setCases((prev) => {
|
||||
const next = prev.map((c) =>
|
||||
c.clientId === treatmentCase.clientId
|
||||
? {
|
||||
...c,
|
||||
id: response.data.id,
|
||||
labCaseId: response.data.id,
|
||||
sentAt: response.data.sentAt,
|
||||
sendToOrganizationIds: response.data.sendToOrganizationIds,
|
||||
sendToOrganizationIds: response.data.destinationOrganizationId
|
||||
? [response.data.destinationOrganizationId]
|
||||
: [],
|
||||
sends: response.data.sends,
|
||||
}
|
||||
: c,
|
||||
@@ -452,7 +485,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
|
||||
return next;
|
||||
});
|
||||
setRecentOrganizationIds((prev) => {
|
||||
const next = [...targets.filter((id) => !prev.includes(id)), ...prev];
|
||||
const next = [destinationOrgId, ...prev.filter((id) => id !== destinationOrgId)];
|
||||
return next.slice(0, 10);
|
||||
});
|
||||
showSuccess(t('successCaseSent'));
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
LabCaseResponse,
|
||||
LinkedOrganizationOption,
|
||||
PastTreatment,
|
||||
SaveTreatmentPayload,
|
||||
SendTreatmentCasePayload,
|
||||
TreatmentAttachmentMeta,
|
||||
TreatmentCaseSendInfo,
|
||||
SaveLabCasePayload,
|
||||
SavedTreatmentDetailPayload,
|
||||
} from '@/types/treatment';
|
||||
|
||||
export const treatmentsApi = {
|
||||
@@ -33,34 +32,53 @@ export const treatmentsApi = {
|
||||
|
||||
saveDraft: async (
|
||||
appointmentId: string,
|
||||
payload: Pick<SaveTreatmentPayload, 'cases'>,
|
||||
payload: { details: SavedTreatmentDetailPayload[] },
|
||||
): Promise<{ success: boolean; data: PastTreatment }> => {
|
||||
const response = await apiClient.put(`/treatments/appointments/${appointmentId}/draft`, payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
uploadCaseAttachments: async (
|
||||
saveLabCases: async (
|
||||
appointmentId: string,
|
||||
caseClientId: string,
|
||||
payload: { labCases: SaveLabCasePayload[] },
|
||||
): Promise<{ success: boolean; data: PastTreatment }> => {
|
||||
const response = await apiClient.put(
|
||||
`/treatments/appointments/${appointmentId}/lab-cases`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
uploadDetailAttachments: async (
|
||||
appointmentId: string,
|
||||
detailClientId: string,
|
||||
files: File[],
|
||||
): Promise<{ success: boolean; data: TreatmentAttachmentMeta[] }> => {
|
||||
): Promise<{ success: boolean; data: import('@/types/treatment').TreatmentAttachmentMeta[] }> => {
|
||||
const form = new FormData();
|
||||
for (const file of files) {
|
||||
form.append('files', file);
|
||||
}
|
||||
const response = await apiClient.post(
|
||||
`/treatments/appointments/${appointmentId}/cases/${encodeURIComponent(caseClientId)}/attachments`,
|
||||
`/treatments/appointments/${appointmentId}/details/${encodeURIComponent(detailClientId)}/attachments`,
|
||||
form,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' }, timeout: 120_000 },
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
sendCase: async (
|
||||
caseId: string,
|
||||
payload: SendTreatmentCasePayload,
|
||||
): Promise<{ success: boolean; data: PastTreatmentCaseResponse }> => {
|
||||
const response = await apiClient.post(`/treatments/cases/${caseId}/send`, payload);
|
||||
/** @deprecated Use uploadDetailAttachments */
|
||||
uploadCaseAttachments: async (
|
||||
appointmentId: string,
|
||||
detailClientId: string,
|
||||
files: File[],
|
||||
) => {
|
||||
return treatmentsApi.uploadDetailAttachments(appointmentId, detailClientId, files);
|
||||
},
|
||||
|
||||
sendLabCase: async (
|
||||
labCaseId: string,
|
||||
): Promise<{ success: boolean; data: LabCaseResponse }> => {
|
||||
const response = await apiClient.post(`/treatments/lab-cases/${labCaseId}/send`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -72,15 +90,3 @@ export const treatmentsApi = {
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export interface PastTreatmentCaseResponse {
|
||||
id: string;
|
||||
clientId: string;
|
||||
treatmentType: string;
|
||||
teeth: string[];
|
||||
notes: string | null;
|
||||
sentAt: string | null;
|
||||
sendToOrganizationIds: string[];
|
||||
sends: TreatmentCaseSendInfo[];
|
||||
attachmentMetas: TreatmentAttachmentMeta[];
|
||||
}
|
||||
|
||||
@@ -61,24 +61,47 @@ export const TREATMENT_TYPES = [
|
||||
|
||||
export type TreatmentType = (typeof TREATMENT_TYPES)[number];
|
||||
|
||||
export interface TreatmentCaseSendInfo {
|
||||
export interface LabCaseSendInfo {
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
sentAt: string;
|
||||
}
|
||||
|
||||
export interface PastTreatmentCase {
|
||||
/** @deprecated Use LabCaseSendInfo */
|
||||
export type TreatmentCaseSendInfo = LabCaseSendInfo;
|
||||
|
||||
export interface PastTreatmentDetail {
|
||||
id: string;
|
||||
clientId: string;
|
||||
treatmentType: TreatmentType;
|
||||
teeth: FdiToothId[];
|
||||
notes?: string | null;
|
||||
attachmentMetas?: TreatmentAttachmentMeta[];
|
||||
sendToOrganizationIds?: string[];
|
||||
sends?: TreatmentCaseSendInfo[];
|
||||
labCaseId?: string | null;
|
||||
destinationOrganizationId?: string | null;
|
||||
sends?: LabCaseSendInfo[];
|
||||
sentAt?: string | null;
|
||||
}
|
||||
|
||||
/** @deprecated Use PastTreatmentDetail */
|
||||
export type PastTreatmentCase = PastTreatmentDetail;
|
||||
|
||||
export interface PastLabCase {
|
||||
id: string;
|
||||
clientId: string;
|
||||
destinationOrganizationId: string | null;
|
||||
labComment?: string | null;
|
||||
sentAt?: string | null;
|
||||
treatmentDetailIds: string[];
|
||||
details: Array<{
|
||||
id: string;
|
||||
clientId: string;
|
||||
treatmentType: string;
|
||||
teeth: FdiToothId[];
|
||||
}>;
|
||||
sends?: LabCaseSendInfo[];
|
||||
}
|
||||
|
||||
export interface PastTreatment {
|
||||
id: string;
|
||||
patientId: string;
|
||||
@@ -86,7 +109,8 @@ export interface PastTreatment {
|
||||
title: string;
|
||||
treatmentAt: string;
|
||||
status: string;
|
||||
cases: PastTreatmentCase[];
|
||||
details: PastTreatmentDetail[];
|
||||
labCases: PastLabCase[];
|
||||
documents: TreatmentAttachmentMeta[];
|
||||
}
|
||||
|
||||
@@ -96,19 +120,23 @@ export interface LinkedOrganizationOption {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface TreatmentCaseDraft {
|
||||
export interface TreatmentDetailDraft {
|
||||
clientId: string;
|
||||
id?: string;
|
||||
treatmentType: TreatmentType;
|
||||
teeth: FdiToothId[];
|
||||
comment: string;
|
||||
attachmentMetas: TreatmentAttachmentMeta[];
|
||||
labCaseId?: string | null;
|
||||
sendToOrganizationIds: string[];
|
||||
sends?: TreatmentCaseSendInfo[];
|
||||
sends?: LabCaseSendInfo[];
|
||||
sentAt?: string | null;
|
||||
}
|
||||
|
||||
export type SavedTreatmentCasePayload = {
|
||||
/** @deprecated Use TreatmentDetailDraft — kept for editor components until Phase 4 rename */
|
||||
export type TreatmentCaseDraft = TreatmentDetailDraft;
|
||||
|
||||
export type SavedTreatmentDetailPayload = {
|
||||
clientId: string;
|
||||
id?: string;
|
||||
treatmentType: TreatmentType;
|
||||
@@ -117,12 +145,35 @@ export type SavedTreatmentCasePayload = {
|
||||
attachmentIds: string[];
|
||||
};
|
||||
|
||||
/** @deprecated Use SavedTreatmentDetailPayload */
|
||||
export type SavedTreatmentCasePayload = SavedTreatmentDetailPayload;
|
||||
|
||||
export interface SaveLabCasePayload {
|
||||
clientId: string;
|
||||
id?: string;
|
||||
destinationOrganizationId?: string;
|
||||
labComment?: string;
|
||||
treatmentDetailIds: string[];
|
||||
}
|
||||
|
||||
export interface SaveTreatmentPayload {
|
||||
appointmentId: string;
|
||||
patientId: string;
|
||||
cases: SavedTreatmentCasePayload[];
|
||||
details: SavedTreatmentDetailPayload[];
|
||||
}
|
||||
|
||||
export interface SendTreatmentCasePayload {
|
||||
organizationIds: string[];
|
||||
export interface LabCaseResponse {
|
||||
id: string;
|
||||
clientId: string;
|
||||
destinationOrganizationId: string | null;
|
||||
labComment: string | null;
|
||||
sentAt: string | null;
|
||||
treatmentDetailIds: string[];
|
||||
details: Array<{
|
||||
id: string;
|
||||
clientId: string;
|
||||
treatmentType: string;
|
||||
teeth: string[];
|
||||
}>;
|
||||
sends: LabCaseSendInfo[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user