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

@@ -12,7 +12,7 @@ import { Type } from 'class-transformer';
const TREATMENT_TYPES = ['consultation', 'filling', 'endo', 'visit', 'hygiene'] as const;
export class SaveTreatmentCaseDto {
export class SaveTreatmentDetailDto {
@IsString()
@MaxLength(64)
clientId: string;
@@ -43,15 +43,39 @@ export class SaveTreatmentDraftDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => SaveTreatmentCaseDto)
cases: SaveTreatmentCaseDto[];
@Type(() => SaveTreatmentDetailDto)
details: SaveTreatmentDetailDto[];
}
export class SendTreatmentCaseDto {
export class SaveLabCaseDto {
@IsString()
@MaxLength(64)
clientId: string;
@IsOptional()
@IsUUID()
id?: string;
@IsOptional()
@IsUUID()
destinationOrganizationId?: string;
@IsOptional()
@IsString()
@MaxLength(5000)
labComment?: string;
@IsArray()
@ArrayMinSize(1)
@IsUUID(undefined, { each: true })
organizationIds: string[];
treatmentDetailIds: string[];
}
export class SaveTreatmentLabCasesDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => SaveLabCaseDto)
labCases: SaveLabCaseDto[];
}
export class ListPatientTreatmentHistoryDto {

View File

@@ -19,7 +19,10 @@ import { memoryStorage } from 'multer';
import type { Response } from 'express';
import { ClinicOrgGuard } from '../../common/guards/clinic-org.guard';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { SaveTreatmentDraftDto, SendTreatmentCaseDto } from './dto/treatment.dto';
import {
SaveTreatmentDraftDto,
SaveTreatmentLabCasesDto,
} from './dto/treatment.dto';
import { TreatmentsService } from './treatments.service';
@ApiTags('treatments')
@@ -67,7 +70,7 @@ export class TreatmentsController {
}
@Put('appointments/:appointmentId/draft')
@ApiOperation({ summary: 'Save draft treatment for an appointment (TAB_TREATMENT_EDIT)' })
@ApiOperation({ summary: 'Save draft treatment details for an appointment (TAB_TREATMENT_EDIT)' })
saveDraft(
@Param('appointmentId') appointmentId: string,
@Body() dto: SaveTreatmentDraftDto,
@@ -82,8 +85,24 @@ export class TreatmentsController {
);
}
@Post('appointments/:appointmentId/cases/:caseClientKey/attachments')
@ApiOperation({ summary: 'Upload attachments for a draft case (TAB_TREATMENT_EDIT)' })
@Put('appointments/:appointmentId/lab-cases')
@ApiOperation({ summary: 'Save lab case groupings for a draft treatment (TAB_TREATMENT_EDIT)' })
saveLabCases(
@Param('appointmentId') appointmentId: string,
@Body() dto: SaveTreatmentLabCasesDto,
@Req() req: { user: { id: string; organizationId?: string } },
) {
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
return this.treatmentsService.saveLabCasesForAppointment(
appointmentId,
dto,
organizationId,
req.user.id,
);
}
@Post('appointments/:appointmentId/details/:detailClientKey/attachments')
@ApiOperation({ summary: 'Upload attachments for a draft treatment detail (TAB_TREATMENT_EDIT)' })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
@@ -101,14 +120,39 @@ export class TreatmentsController {
storage: memoryStorage(),
}),
)
uploadAttachments(
uploadDetailAttachments(
@Param('appointmentId') appointmentId: string,
@Param('detailClientKey') detailClientKey: string,
@UploadedFiles() files: Express.Multer.File[],
@Req() req: { user: { id: string; organizationId?: string } },
) {
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
return this.treatmentsService.uploadDetailAttachments(
appointmentId,
detailClientKey,
files,
organizationId,
req.user.id,
);
}
/** @deprecated Use details/:detailClientKey/attachments */
@Post('appointments/:appointmentId/cases/:caseClientKey/attachments')
@ApiOperation({ summary: 'Legacy alias for detail attachment upload' })
@ApiConsumes('multipart/form-data')
@UseInterceptors(
FilesInterceptor('files', 20, {
storage: memoryStorage(),
}),
)
uploadDetailAttachmentsLegacy(
@Param('appointmentId') appointmentId: string,
@Param('caseClientKey') caseClientKey: string,
@UploadedFiles() files: Express.Multer.File[],
@Req() req: { user: { id: string; organizationId?: string } },
) {
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
return this.treatmentsService.uploadCaseAttachments(
return this.treatmentsService.uploadDetailAttachments(
appointmentId,
caseClientKey,
files,
@@ -136,14 +180,13 @@ export class TreatmentsController {
file.stream.pipe(res);
}
@Post('cases/:caseId/send')
@ApiOperation({ summary: 'Send a treatment case to linked organizations (TAB_TREATMENT_EDIT)' })
sendCase(
@Param('caseId') caseId: string,
@Body() dto: SendTreatmentCaseDto,
@Post('lab-cases/:labCaseId/send')
@ApiOperation({ summary: 'Send a lab case to its destination organization (TAB_TREATMENT_EDIT)' })
sendLabCase(
@Param('labCaseId') labCaseId: string,
@Req() req: { user: { id: string; organizationId?: string } },
) {
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
return this.treatmentsService.sendCase(caseId, dto, organizationId, req.user.id);
return this.treatmentsService.sendLabCase(labCaseId, organizationId, req.user.id);
}
}

View File

@@ -9,7 +9,10 @@ import { createReadStream, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { randomUUID } from 'crypto';
import { PrismaService } from '../../../prisma/prisma.service';
import { SaveTreatmentDraftDto, SendTreatmentCaseDto } from './dto/treatment.dto';
import {
SaveTreatmentDraftDto,
SaveTreatmentLabCasesDto,
} from './dto/treatment.dto';
import {
generateTreatmentTitle,
isTreatmentType,
@@ -18,10 +21,34 @@ import {
} from './treatment.utils';
const treatmentInclude = {
cases: {
details: {
orderBy: [{ sortOrder: 'asc' as const }],
include: {
attachments: { orderBy: [{ createdAt: 'asc' as const }] },
labCaseLink: {
include: {
labCase: {
include: {
sends: {
orderBy: [{ sentAt: 'asc' as const }],
include: { organization: { select: { id: true, name: true } } },
},
},
},
},
},
},
},
labCases: {
orderBy: [{ sortOrder: 'asc' as const }],
include: {
details: {
include: {
detail: {
select: { id: true, clientKey: true, treatmentType: true, teeth: true },
},
},
},
sends: {
orderBy: [{ sentAt: 'asc' as const }],
include: { organization: { select: { id: true, name: true } } },
@@ -80,7 +107,7 @@ export class TreatmentsService {
limit = 20,
) {
await this.assertCanReadTreatment(actorUserId, organizationId);
await this.ensurePatientInOrg(patientId, organizationId);
await this.ensurePatientExists(patientId);
const items = await this.prisma.treatment.findMany({
where: {
@@ -135,22 +162,22 @@ export class TreatmentsService {
true,
);
for (const c of dto.cases) {
if (!isTreatmentType(c.treatmentType)) {
throw new BadRequestException(`Invalid treatment type: ${c.treatmentType}`);
for (const d of dto.details) {
if (!isTreatmentType(d.treatmentType)) {
throw new BadRequestException(`Invalid treatment type: ${d.treatmentType}`);
}
}
const normalizedCases = dto.cases.map((c, index) => ({
...c,
const normalizedDetails = dto.details.map((d, index) => ({
...d,
sortOrder: index,
teeth: normalizeTeeth(c.teeth),
comment: c.comment?.trim() || null,
attachmentIds: c.attachmentIds ?? [],
teeth: normalizeTeeth(d.teeth),
comment: d.comment?.trim() || null,
attachmentIds: d.attachmentIds ?? [],
}));
const title = generateTreatmentTitle(
normalizedCases.map((c) => ({ treatmentType: c.treatmentType, teeth: c.teeth })),
normalizedDetails.map((d) => ({ treatmentType: d.treatmentType, teeth: d.teeth })),
);
const treatment = await this.prisma.$transaction(async (tx) => {
@@ -182,77 +209,80 @@ export class TreatmentsService {
},
});
const keepCaseIds = normalizedCases.map((c) => c.id).filter(Boolean) as string[];
const existingCases = existing
? await tx.treatmentCase.findMany({
const keepDetailIds = normalizedDetails.map((d) => d.id).filter(Boolean) as string[];
const existingDetails = existing
? await tx.treatmentDetail.findMany({
where: { treatmentId: saved.id },
select: { id: true, sentAt: true },
select: { id: true, labCaseLink: { select: { labCase: { select: { sentAt: true } } } } },
})
: [];
const sentCaseIds = new Set(
existingCases.filter((c) => c.sentAt).map((c) => c.id),
const lockedDetailIds = new Set(
existingDetails
.filter((d) => d.labCaseLink?.labCase.sentAt)
.map((d) => d.id),
);
const removableCaseIds = existingCases
.filter((c) => !keepCaseIds.includes(c.id) && !c.sentAt)
.map((c) => c.id);
const removableDetailIds = existingDetails
.filter((d) => !keepDetailIds.includes(d.id) && !lockedDetailIds.has(d.id))
.map((d) => d.id);
if (removableCaseIds.length > 0) {
await tx.treatmentCase.deleteMany({
where: { id: { in: removableCaseIds }, treatmentId: saved.id },
if (removableDetailIds.length > 0) {
await tx.treatmentDetail.deleteMany({
where: { id: { in: removableDetailIds }, treatmentId: saved.id },
});
}
for (const c of normalizedCases) {
if (c.id && sentCaseIds.has(c.id)) {
for (const d of normalizedDetails) {
if (d.id && lockedDetailIds.has(d.id)) {
continue;
}
const row = c.id
? await tx.treatmentCase.update({
where: { id: c.id },
const row = d.id
? await tx.treatmentDetail.update({
where: { id: d.id },
data: {
clientKey: c.clientId,
sortOrder: c.sortOrder,
treatmentType: c.treatmentType,
teeth: c.teeth,
comment: c.comment,
clientKey: d.clientId,
sortOrder: d.sortOrder,
treatmentType: d.treatmentType,
teeth: d.teeth,
comment: d.comment,
},
})
: await tx.treatmentCase.create({
: await tx.treatmentDetail.create({
data: {
treatmentId: saved.id,
clientKey: c.clientId,
sortOrder: c.sortOrder,
treatmentType: c.treatmentType,
teeth: c.teeth,
comment: c.comment,
clientKey: d.clientId,
sortOrder: d.sortOrder,
treatmentType: d.treatmentType,
teeth: d.teeth,
comment: d.comment,
},
});
const allowedAttachmentIds = new Set(c.attachmentIds);
const pendingAttachments = await tx.treatmentCaseAttachment.findMany({
const allowedAttachmentIds = new Set(d.attachmentIds);
const pendingAttachments = await tx.treatmentDetailAttachment.findMany({
where: {
appointmentId: appointment.id,
caseClientKey: c.clientId,
detailClientKey: d.clientId,
},
});
for (const attachment of pendingAttachments) {
if (!allowedAttachmentIds.has(attachment.id)) {
await tx.treatmentCaseAttachment.delete({ where: { id: attachment.id } });
await tx.treatmentDetailAttachment.delete({ where: { id: attachment.id } });
} else {
await tx.treatmentCaseAttachment.update({
await tx.treatmentDetailAttachment.update({
where: { id: attachment.id },
data: { caseId: row.id, appointmentId: null, caseClientKey: null },
data: { detailId: row.id, appointmentId: null, detailClientKey: null },
});
}
}
await tx.treatmentCaseAttachment.deleteMany({
await tx.treatmentDetailAttachment.deleteMany({
where: {
caseId: row.id,
detailId: row.id,
id: { notIn: [...allowedAttachmentIds] },
},
});
@@ -267,74 +297,191 @@ export class TreatmentsService {
return { success: true, data: this.mapTreatment(treatment) };
}
async sendCase(
caseId: string,
dto: SendTreatmentCaseDto,
async saveLabCasesForAppointment(
appointmentId: string,
dto: SaveTreatmentLabCasesDto,
organizationId: string,
actorUserId: string,
) {
await this.assertCanEditTreatment(actorUserId, organizationId);
const appointment = await this.ensureAppointmentProvider(
appointmentId,
organizationId,
actorUserId,
true,
);
const treatment = await this.prisma.treatment.findFirst({
where: { appointmentId: appointment.id, organizationId, status: TreatmentStatus.DRAFT },
select: { id: true },
});
if (!treatment) {
throw new NotFoundException('Save treatment details before creating lab cases');
}
const detailIds = dto.labCases.flatMap((lc) => lc.treatmentDetailIds);
const uniqueDetailIds = new Set(detailIds);
if (uniqueDetailIds.size !== detailIds.length) {
throw new BadRequestException('Each treatment detail can belong to only one lab case');
}
const details = await this.prisma.treatmentDetail.findMany({
where: { treatmentId: treatment.id, id: { in: detailIds } },
select: { id: true },
});
if (details.length !== uniqueDetailIds.size) {
throw new BadRequestException('One or more treatment details were not found');
}
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');
}
}
const saved = await this.prisma.$transaction(async (tx) => {
const existingLabCases = await tx.labCase.findMany({
where: { treatmentId: treatment.id },
select: { id: true, sentAt: true },
});
const sentLabCaseIds = new Set(existingLabCases.filter((lc) => lc.sentAt).map((lc) => lc.id));
const keepLabCaseIds = dto.labCases.map((lc) => lc.id).filter(Boolean) as string[];
const removableLabCaseIds = existingLabCases
.filter((lc) => !keepLabCaseIds.includes(lc.id) && !lc.sentAt)
.map((lc) => lc.id);
if (removableLabCaseIds.length > 0) {
await tx.labCase.deleteMany({
where: { id: { in: removableLabCaseIds }, treatmentId: treatment.id },
});
}
for (const [index, lc] of dto.labCases.entries()) {
if (lc.id && sentLabCaseIds.has(lc.id)) {
continue;
}
const row = lc.id
? await tx.labCase.update({
where: { id: lc.id },
data: {
clientKey: lc.clientId,
sortOrder: index,
destinationOrganizationId: lc.destinationOrganizationId ?? null,
labComment: lc.labComment?.trim() || null,
},
})
: await tx.labCase.create({
data: {
treatmentId: treatment.id,
clientKey: lc.clientId,
sortOrder: index,
destinationOrganizationId: lc.destinationOrganizationId ?? null,
labComment: lc.labComment?.trim() || null,
},
});
await tx.labCaseDetail.deleteMany({ where: { labCaseId: row.id } });
await tx.labCaseDetail.createMany({
data: lc.treatmentDetailIds.map((treatmentDetailId) => ({
labCaseId: row.id,
treatmentDetailId,
})),
});
}
return tx.treatment.findUniqueOrThrow({
where: { id: treatment.id },
include: treatmentInclude,
});
});
return { success: true, data: this.mapTreatment(saved) };
}
async sendLabCase(
labCaseId: string,
organizationId: string,
actorUserId: string,
) {
await this.assertCanEditTreatment(actorUserId, organizationId);
const treatmentCase = await this.prisma.treatmentCase.findFirst({
const labCase = await this.prisma.labCase.findFirst({
where: {
id: caseId,
id: labCaseId,
treatment: { organizationId },
},
include: {
treatment: { select: { providerUserId: true, appointmentId: true } },
treatment: { select: { providerUserId: true } },
sends: { select: { organizationId: true } },
details: { select: { treatmentDetailId: true } },
},
});
if (!treatmentCase) {
throw new NotFoundException('Treatment case not found');
if (!labCase) {
throw new NotFoundException('Lab case not found');
}
if (treatmentCase.treatment.providerUserId !== actorUserId) {
if (!labCase.destinationOrganizationId) {
throw new BadRequestException('Lab case has no destination organization');
}
if (labCase.details.length === 0) {
throw new BadRequestException('Lab case must include at least one treatment detail');
}
if (labCase.treatment.providerUserId !== actorUserId) {
const membership = await this.getMembership(actorUserId, organizationId);
if (!membership?.isOwner) {
throw new ForbiddenException('Only the appointment provider can send this case');
throw new ForbiddenException('Only the appointment provider can send this lab case');
}
}
const linkedOrgIds = await this.getActiveLinkedOrganizationIds(organizationId);
const uniqueTargets = [...new Set(dto.organizationIds)];
for (const orgId of uniqueTargets) {
if (!linkedOrgIds.has(orgId)) {
throw new BadRequestException('One or more organizations are not active linked counterparts');
}
if (!linkedOrgIds.has(labCase.destinationOrganizationId)) {
throw new BadRequestException('Destination organization is not an active linked counterpart');
}
const alreadySent = new Set(treatmentCase.sends.map((s) => s.organizationId));
const newTargets = uniqueTargets.filter((id) => !alreadySent.has(id));
if (newTargets.length === 0) {
throw new BadRequestException('Case was already sent to all selected organizations');
const alreadySent = labCase.sends.some(
(s) => s.organizationId === labCase.destinationOrganizationId,
);
if (alreadySent) {
throw new BadRequestException('Lab case was already sent to the destination organization');
}
const now = new Date();
await this.prisma.$transaction(async (tx) => {
await tx.treatmentCaseSend.createMany({
data: newTargets.map((organizationId) => ({
caseId,
organizationId,
})),
await tx.labCaseSend.create({
data: {
labCaseId,
organizationId: labCase.destinationOrganizationId!,
},
});
if (!treatmentCase.sentAt) {
await tx.treatmentCase.update({
where: { id: caseId },
if (!labCase.sentAt) {
await tx.labCase.update({
where: { id: labCaseId },
data: { sentAt: now },
});
}
});
const refreshed = await this.prisma.treatmentCase.findUniqueOrThrow({
where: { id: caseId },
const refreshed = await this.prisma.labCase.findUniqueOrThrow({
where: { id: labCaseId },
include: {
attachments: { orderBy: [{ createdAt: 'asc' }] },
details: {
include: {
detail: {
select: { id: true, clientKey: true, treatmentType: true, teeth: true },
},
},
},
sends: {
orderBy: [{ sentAt: 'asc' }],
include: { organization: { select: { id: true, name: true } } },
@@ -342,12 +489,12 @@ export class TreatmentsService {
},
});
return { success: true, data: this.mapCase(refreshed) };
return { success: true, data: this.mapLabCase(refreshed) };
}
async uploadCaseAttachments(
async uploadDetailAttachments(
appointmentId: string,
caseClientKey: string,
detailClientKey: string,
files: Express.Multer.File[],
organizationId: string,
actorUserId: string,
@@ -355,8 +502,8 @@ export class TreatmentsService {
await this.assertCanEditTreatment(actorUserId, organizationId);
await this.ensureAppointmentProvider(appointmentId, organizationId, actorUserId, true);
if (!caseClientKey?.trim()) {
throw new BadRequestException('caseClientKey is required');
if (!detailClientKey?.trim()) {
throw new BadRequestException('detailClientKey is required');
}
if (!files?.length) {
@@ -379,10 +526,10 @@ export class TreatmentsService {
const { writeFileSync } = await import('fs');
writeFileSync(storagePath, file.buffer);
const attachment = await this.prisma.treatmentCaseAttachment.create({
const attachment = await this.prisma.treatmentDetailAttachment.create({
data: {
appointmentId,
caseClientKey,
detailClientKey,
fileName: file.originalname,
mimeType: file.mimetype || 'application/octet-stream',
sizeBytes: file.size,
@@ -403,16 +550,16 @@ export class TreatmentsService {
) {
await this.assertCanReadTreatment(actorUserId, organizationId);
const attachment = await this.prisma.treatmentCaseAttachment.findFirst({
const attachment = await this.prisma.treatmentDetailAttachment.findFirst({
where: {
id: attachmentId,
OR: [
{ case: { treatment: { organizationId } } },
{ detail: { treatment: { organizationId } } },
{ appointmentId: { not: null } },
],
},
include: {
case: { select: { treatment: { select: { organizationId: true } } } },
detail: { select: { treatment: { select: { organizationId: true } } } },
},
});
@@ -420,11 +567,11 @@ export class TreatmentsService {
throw new NotFoundException('Attachment not found');
}
if (attachment.case && attachment.case.treatment.organizationId !== organizationId) {
if (attachment.detail && attachment.detail.treatment.organizationId !== organizationId) {
throw new NotFoundException('Attachment not found');
}
if (!attachment.case && attachment.appointmentId) {
if (!attachment.detail && attachment.appointmentId) {
const appointment = await this.prisma.appointment.findFirst({
where: { id: attachment.appointmentId, organizationId },
select: { id: true },
@@ -452,24 +599,51 @@ export class TreatmentsService {
title: string;
status: TreatmentStatus;
treatmentAt: Date;
cases: Array<{
details: Array<{
id: string;
clientKey: string | null;
treatmentType: string;
teeth: unknown;
comment: string | null;
sentAt: Date | null;
attachments: Array<{
id: string;
fileName: string;
mimeType: string;
sizeBytes: number;
}>;
sends: Array<{ organizationId: string; sentAt: Date; organization: { id: string; name: string } }>;
labCaseLink?: {
labCase: {
id: string;
sentAt: Date | null;
destinationOrganizationId: string | null;
sends: Array<{
organizationId: string;
sentAt: Date;
organization: { id: string; name: string };
}>;
};
} | null;
}>;
labCases: Array<{
id: string;
clientKey: string | null;
sortOrder: number;
destinationOrganizationId: string | null;
labComment: string | null;
sentAt: Date | null;
details: Array<{
treatmentDetailId: string;
detail: { id: string; clientKey: string | null; treatmentType: string; teeth: unknown };
}>;
sends: Array<{
organizationId: string;
sentAt: Date;
organization: { id: string; name: string };
}>;
}>;
}) {
const documents = treatment.cases.flatMap((c) =>
c.attachments.map((a) => this.mapAttachment(a)),
const documents = treatment.details.flatMap((d) =>
d.attachments.map((a) => this.mapAttachment(a)),
);
return {
@@ -479,41 +653,93 @@ export class TreatmentsService {
title: treatment.title,
treatmentAt: treatment.treatmentAt.toISOString(),
status: mapTreatmentStatusForApi(treatment.status),
cases: treatment.cases.map((c) => this.mapCase(c)),
details: treatment.details.map((d) => this.mapDetail(d)),
labCases: treatment.labCases.map((lc) => this.mapLabCase(lc)),
documents,
};
}
private mapCase(c: {
private mapDetail(d: {
id: string;
clientKey?: string | null;
treatmentType: string;
teeth: unknown;
comment?: string | null;
sentAt?: Date | null;
attachments?: Array<{
id: string;
fileName: string;
mimeType: string;
sizeBytes: number;
}>;
sends?: Array<{ organizationId: string; sentAt: Date; organization?: { id: string; name: string } }>;
labCaseLink?: {
labCase: {
id: string;
sentAt: Date | null;
destinationOrganizationId: string | null;
sends: Array<{
organizationId: string;
sentAt: Date;
organization?: { id: string; name: string };
}>;
};
} | null;
}) {
const labCase = d.labCaseLink?.labCase;
return {
id: c.id,
clientId: c.clientKey ?? c.id,
treatmentType: c.treatmentType,
teeth: normalizeTeeth(c.teeth),
notes: c.comment ?? null,
sentAt: c.sentAt?.toISOString() ?? null,
sendToOrganizationIds: c.sends?.map((s) => s.organizationId) ?? [],
id: d.id,
clientId: d.clientKey ?? d.id,
treatmentType: d.treatmentType,
teeth: normalizeTeeth(d.teeth),
notes: d.comment ?? null,
attachmentMetas: (d.attachments ?? []).map((a) => this.mapAttachment(a)),
labCaseId: labCase?.id ?? null,
sentAt: labCase?.sentAt?.toISOString() ?? null,
destinationOrganizationId: labCase?.destinationOrganizationId ?? null,
sends:
c.sends?.map((s) => ({
labCase?.sends.map((s) => ({
organizationId: s.organizationId,
organizationName: s.organization?.name ?? 'Unknown organization',
sentAt: s.sentAt.toISOString(),
})) ?? [],
};
}
private mapLabCase(lc: {
id: string;
clientKey?: string | null;
sortOrder?: number;
destinationOrganizationId?: string | null;
labComment?: string | null;
sentAt?: Date | null;
details?: Array<{
treatmentDetailId: string;
detail?: { id: string; clientKey: string | null; treatmentType: string; teeth: unknown };
}>;
sends?: Array<{
organizationId: string;
sentAt: Date;
organization?: { id: string; name: string };
}>;
}) {
return {
id: lc.id,
clientId: lc.clientKey ?? lc.id,
destinationOrganizationId: lc.destinationOrganizationId ?? null,
labComment: lc.labComment ?? null,
sentAt: lc.sentAt?.toISOString() ?? null,
treatmentDetailIds: lc.details?.map((d) => d.treatmentDetailId) ?? [],
details: (lc.details ?? []).map((d) => ({
id: d.detail?.id ?? d.treatmentDetailId,
clientId: d.detail?.clientKey ?? d.treatmentDetailId,
treatmentType: d.detail?.treatmentType ?? '',
teeth: d.detail ? normalizeTeeth(d.detail.teeth) : [],
})),
sends:
lc.sends?.map((s) => ({
organizationId: s.organizationId,
organizationName: s.organization?.name ?? 'Unknown organization',
sentAt: s.sentAt.toISOString(),
})) ?? [],
attachmentMetas: (c.attachments ?? []).map((a) => this.mapAttachment(a)),
};
}
@@ -549,7 +775,7 @@ export class TreatmentsService {
]);
}
private async ensurePatientInOrg(patientId: string, _organizationId: string) {
private async ensurePatientExists(patientId: string) {
const patient = await this.prisma.patient.findUnique({
where: { id: patientId },
select: { id: true },