feature: Phase 2- splitting the treatment schema into TreatmentDetail and LabCase.

This commit is contained in:
2026-06-28 15:34:56 +03:30
parent dc965b2528
commit 8b4ef6195d
12 changed files with 749 additions and 232 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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'));