TreatmentType and ProsthesisType database shcema and data updated. Lab dispatch wired through new data.

This commit is contained in:
2026-07-06 20:40:19 +03:30
parent 3e81c110a3
commit 667b08ed0c
48 changed files with 1813 additions and 275 deletions

View File

@@ -10,6 +10,7 @@ import { join } from 'path';
import { randomUUID } from 'crypto';
import { PrismaService } from '../../../prisma/prisma.service';
import { generateLabCaseTasks } from '../cases/lab-case-task.generator';
import { ProsthesisCatalogService } from '../prosthesis-catalog/prosthesis-catalog.service';
import { TreatmentCatalogService } from '../treatment-catalog/treatment-catalog.service';
import {
SaveTreatmentDraftDto,
@@ -19,6 +20,7 @@ import {
generateTreatmentTitle,
normalizeTeeth,
} from './treatment.utils';
import { assertCompleteToothProsthesisMap } from './lab-case-send.validation';
const treatmentInclude = {
details: {
@@ -53,6 +55,7 @@ const treatmentInclude = {
orderBy: [{ sentAt: 'asc' as const }],
include: { organization: { select: { id: true, name: true } } },
},
toothProsthesis: true,
},
},
};
@@ -64,6 +67,7 @@ export class TreatmentsService {
constructor(
private readonly prisma: PrismaService,
private readonly treatmentCatalog: TreatmentCatalogService,
private readonly prosthesisCatalog: ProsthesisCatalogService,
) {}
getOrganizationIdFromUser(user: { organizationId?: string }) {
@@ -326,7 +330,7 @@ export class TreatmentsService {
const details = await this.prisma.treatmentDetail.findMany({
where: { treatmentId: treatment.id, id: { in: detailIds } },
select: { id: true, treatmentType: true },
select: { id: true, treatmentType: true, teeth: true },
});
if (details.length !== uniqueDetailIds.size) {
throw new BadRequestException('One or more treatment details were not found');
@@ -336,12 +340,30 @@ export class TreatmentsService {
this.treatmentCatalog.assertLabDependentTreatmentType(detail.treatmentType);
}
const detailById = new Map(details.map((d) => [d.id, d]));
const linkedOrgIds = await this.getActiveLinkedOrganizationIds(organizationId);
for (const lc of dto.labCases) {
if (lc.destinationOrganizationId && !linkedOrgIds.has(lc.destinationOrganizationId)) {
throw new BadRequestException('Destination organization is not an active linked counterpart');
}
for (const row of lc.toothProsthesis ?? []) {
if (!lc.treatmentDetailIds.includes(row.treatmentDetailId)) {
throw new BadRequestException(
'Tooth prosthesis must reference a detail included in this lab case',
);
}
const detail = detailById.get(row.treatmentDetailId);
if (!detail) {
throw new BadRequestException('Tooth prosthesis references an unknown treatment detail');
}
const teeth = normalizeTeeth(detail.teeth);
if (!teeth.includes(row.tooth)) {
throw new BadRequestException(`Tooth ${row.tooth} is not on the selected treatment detail`);
}
this.prosthesisCatalog.assertKnownProsthesisType(row.prosthesisTypeCode);
}
}
const saved = await this.prisma.$transaction(async (tx) => {
@@ -395,6 +417,18 @@ export class TreatmentsService {
treatmentDetailId,
})),
});
await tx.labCaseToothProsthesis.deleteMany({ where: { labCaseId: row.id } });
if (lc.toothProsthesis?.length) {
await tx.labCaseToothProsthesis.createMany({
data: lc.toothProsthesis.map((tp) => ({
labCaseId: row.id,
treatmentDetailId: tp.treatmentDetailId,
tooth: tp.tooth,
prosthesisTypeCode: tp.prosthesisTypeCode,
})),
});
}
}
return tx.treatment.findUniqueOrThrow({
@@ -410,6 +444,7 @@ export class TreatmentsService {
labCaseId: string,
organizationId: string,
actorUserId: string,
actorLanguage?: string | null,
) {
await this.assertCanEditTreatment(actorUserId, organizationId);
@@ -421,7 +456,12 @@ export class TreatmentsService {
include: {
treatment: { select: { providerUserId: true } },
sends: { select: { organizationId: true } },
details: { select: { treatmentDetailId: true } },
details: {
include: {
detail: { select: { id: true, treatmentType: true, teeth: true } },
},
},
toothProsthesis: true,
},
});
@@ -437,6 +477,8 @@ export class TreatmentsService {
throw new BadRequestException('Lab case must include at least one treatment detail');
}
assertCompleteToothProsthesisMap(labCase);
if (labCase.treatment.providerUserId !== actorUserId) {
const membership = await this.getMembership(actorUserId, organizationId);
if (!membership?.isOwner) {
@@ -473,7 +515,7 @@ export class TreatmentsService {
});
}
await generateLabCaseTasks(tx, labCaseId);
await generateLabCaseTasks(tx, labCaseId, actorLanguage);
});
const refreshed = await this.prisma.labCase.findUniqueOrThrow({
@@ -490,6 +532,7 @@ export class TreatmentsService {
orderBy: [{ sentAt: 'asc' }],
include: { organization: { select: { id: true, name: true } } },
},
toothProsthesis: true,
},
});
@@ -722,6 +765,11 @@ export class TreatmentsService {
sentAt: Date;
organization?: { id: string; name: string };
}>;
toothProsthesis?: Array<{
treatmentDetailId: string;
tooth: string;
prosthesisTypeCode: string;
}>;
}) {
return {
id: lc.id,
@@ -736,6 +784,11 @@ export class TreatmentsService {
treatmentType: d.detail?.treatmentType ?? '',
teeth: d.detail ? normalizeTeeth(d.detail.teeth) : [],
})),
toothProsthesis: (lc.toothProsthesis ?? []).map((tp) => ({
treatmentDetailId: tp.treatmentDetailId,
tooth: tp.tooth,
prosthesisTypeCode: tp.prosthesisTypeCode,
})),
sends:
lc.sends?.map((s) => ({
organizationId: s.organizationId,