improvement: treatment plan turned into a wizard. shift+click control added to FDI tooth chart for cunnected prosthesises.

This commit is contained in:
2026-07-17 00:39:32 +03:30
parent cf07b4d8a8
commit 68fc9d5d6d
28 changed files with 1461 additions and 477 deletions

View File

@@ -30,6 +30,7 @@ import { LabCaseActivityService } from '../notifications/lab-case-activity.servi
import {
generateTreatmentTitle,
normalizeTeeth,
normalizeToothSelectionGroups,
} from './treatment.utils';
import { assertCompleteToothProsthesisMap } from './lab-case-send.validation';
import { hasEffectivePermission } from '../../common/membership-permissions';
@@ -51,7 +52,7 @@ const sentLabCaseInclude = {
details: {
include: {
detail: {
select: { clientKey: true, treatmentType: true, teeth: true },
select: { clientKey: true, treatmentType: true, teeth: true, toothSelectionGroups: true },
},
},
},
@@ -59,9 +60,9 @@ const sentLabCaseInclude = {
orderBy: [{ sentAt: 'asc' as const }],
include: { organization: { select: { id: true, name: true } } },
},
tasks: { select: { status: true } },
tasks: { select: { status: true, selectionGroupId: true, prosthesisTypeCode: true, teeth: true } },
toothProsthesis: {
select: { tooth: true, prosthesisTypeCode: true },
select: { tooth: true, prosthesisTypeCode: true, selectionGroupId: true },
},
} satisfies Prisma.LabCaseInclude;
@@ -93,7 +94,13 @@ const treatmentInclude = {
details: {
include: {
detail: {
select: { id: true, clientKey: true, treatmentType: true, teeth: true },
select: {
id: true,
clientKey: true,
treatmentType: true,
teeth: true,
toothSelectionGroups: true,
},
},
},
},
@@ -283,17 +290,36 @@ export class TreatmentsService {
const taskProgress = this.mapTaskProgress(lc.tasks);
const labOrg = lc.sends[lc.sends.length - 1]?.organization ?? null;
const detailTeeth = detail ? normalizeTeeth(detail.teeth) : [];
const selectionGroups = normalizeToothSelectionGroups(
detail?.toothSelectionGroups,
detailTeeth,
);
const connectedGroupIds = new Set(
selectionGroups.filter((g) => g.kind === 'connected').map((g) => g.groupId),
);
const prosthesisByCode = new Map<string, string[]>();
const prosthesisByGroup = new Map<
string,
{ prosthesisTypeCode: string; teeth: string[]; selectionGroupId: string; connected: boolean }
>();
for (const row of lc.toothProsthesis ?? []) {
const list = prosthesisByCode.get(row.prosthesisTypeCode) ?? [];
list.push(row.tooth);
prosthesisByCode.set(row.prosthesisTypeCode, list);
const selectionGroupId = row.selectionGroupId || `legacy-${row.prosthesisTypeCode}`;
const key = `${selectionGroupId}::${row.prosthesisTypeCode}`;
const entry = prosthesisByGroup.get(key) ?? {
prosthesisTypeCode: row.prosthesisTypeCode,
teeth: [],
selectionGroupId,
connected: connectedGroupIds.has(row.selectionGroupId),
};
entry.teeth.push(row.tooth);
prosthesisByGroup.set(key, entry);
}
const prosthesisGroups = [...prosthesisByCode.entries()]
.map(([prosthesisTypeCode, teeth]) => ({
prosthesisTypeCode,
teeth: [...new Set(teeth)].sort(),
const prosthesisGroups = [...prosthesisByGroup.values()]
.map((group) => ({
prosthesisTypeCode: group.prosthesisTypeCode,
teeth: [...new Set(group.teeth)].sort(),
connected: group.connected,
selectionGroupId: group.selectionGroupId,
}))
.sort((a, b) => a.prosthesisTypeCode.localeCompare(b.prosthesisTypeCode));
@@ -377,13 +403,21 @@ export class TreatmentsService {
this.treatmentCatalog.assertKnownTreatmentType(d.treatmentType);
}
const normalizedDetails = dto.details.map((d, index) => ({
...d,
sortOrder: index,
teeth: normalizeTeeth(d.teeth),
comment: d.comment?.trim() || null,
attachmentIds: d.attachmentIds ?? [],
}));
const normalizedDetails = dto.details.map((d, index) => {
const teeth = normalizeTeeth(d.teeth);
const toothSelectionGroups = normalizeToothSelectionGroups(
d.toothSelectionGroups,
teeth,
);
return {
...d,
sortOrder: index,
teeth,
toothSelectionGroups,
comment: d.comment?.trim() || null,
attachmentIds: d.attachmentIds ?? [],
};
});
const title = generateTreatmentTitle(
normalizedDetails.map((d) => ({ treatmentType: d.treatmentType, teeth: d.teeth })),
@@ -454,6 +488,7 @@ export class TreatmentsService {
sortOrder: d.sortOrder,
treatmentType: d.treatmentType,
teeth: d.teeth,
toothSelectionGroups: d.toothSelectionGroups,
comment: d.comment,
},
})
@@ -464,6 +499,7 @@ export class TreatmentsService {
sortOrder: d.sortOrder,
treatmentType: d.treatmentType,
teeth: d.teeth,
toothSelectionGroups: d.toothSelectionGroups,
comment: d.comment,
},
});
@@ -642,6 +678,7 @@ export class TreatmentsService {
treatmentDetailId: tp.treatmentDetailId,
tooth: tp.tooth,
prosthesisTypeCode: tp.prosthesisTypeCode,
selectionGroupId: tp.selectionGroupId?.trim() || '',
})),
});
}
@@ -1091,6 +1128,7 @@ export class TreatmentsService {
clientKey?: string | null;
treatmentType: string;
teeth: unknown;
toothSelectionGroups?: unknown;
comment?: string | null;
attachments?: Array<{
id: string;
@@ -1114,11 +1152,13 @@ export class TreatmentsService {
}) {
const labCase = d.labCaseLink?.labCase;
const taskProgress = this.mapTaskProgress(labCase?.tasks ?? []);
const teeth = normalizeTeeth(d.teeth);
return {
id: d.id,
clientId: d.clientKey ?? d.id,
treatmentType: d.treatmentType,
teeth: normalizeTeeth(d.teeth),
teeth,
toothSelectionGroups: normalizeToothSelectionGroups(d.toothSelectionGroups, teeth),
notes: d.comment ?? null,
attachmentMetas: (d.attachments ?? []).map((a) => this.mapAttachment(a)),
labCaseId: labCase?.id ?? null,
@@ -1143,7 +1183,13 @@ export class TreatmentsService {
dueDate?: Date | null;
details?: Array<{
treatmentDetailId: string;
detail?: { id: string; clientKey: string | null; treatmentType: string; teeth: unknown };
detail?: {
id: string;
clientKey: string | null;
treatmentType: string;
teeth: unknown;
toothSelectionGroups?: unknown;
};
}>;
sends?: Array<{
organizationId: string;
@@ -1154,6 +1200,7 @@ export class TreatmentsService {
treatmentDetailId: string;
tooth: string;
prosthesisTypeCode: string;
selectionGroupId?: string;
}>;
attachments?: Array<{
attachment: {
@@ -1167,6 +1214,9 @@ export class TreatmentsService {
tasks?: Array<{ id: string; status: LabTaskStatus }>;
}) {
const taskProgress = this.mapTaskProgress(lc.tasks ?? []);
const detailTeeth = lc.details?.[0]?.detail
? normalizeTeeth(lc.details[0].detail.teeth)
: [];
return {
id: lc.id,
clientId: lc.clientKey ?? lc.id,
@@ -1180,13 +1230,18 @@ export class TreatmentsService {
id: lc.details[0].detail?.id ?? lc.details[0].treatmentDetailId,
clientId: lc.details[0].detail?.clientKey ?? lc.details[0].treatmentDetailId,
treatmentType: lc.details[0].detail?.treatmentType ?? '',
teeth: lc.details[0].detail ? normalizeTeeth(lc.details[0].detail.teeth) : [],
teeth: detailTeeth,
toothSelectionGroups: normalizeToothSelectionGroups(
lc.details[0].detail?.toothSelectionGroups,
detailTeeth,
),
}
: null,
toothProsthesis: (lc.toothProsthesis ?? []).map((tp) => ({
treatmentDetailId: tp.treatmentDetailId,
tooth: tp.tooth,
prosthesisTypeCode: tp.prosthesisTypeCode,
selectionGroupId: tp.selectionGroupId ?? '',
})),
attachments: (lc.attachments ?? []).map((row) => ({
id: row.attachment.id,