improvement: pdf generation added to cases feature.

This commit is contained in:
2026-07-18 22:00:44 +03:30
parent 408b2c3329
commit 538121d653
23 changed files with 1206 additions and 63 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "lab_cases" ADD COLUMN "externalCode" TEXT;

View File

@@ -221,6 +221,8 @@ model LabCase {
sentAt DateTime?
dueDate DateTime?
isImportant Boolean @default(false)
/// Optional code from an external lab app (e.g. exocad) for print/matching.
externalCode String?
accessToken String? @unique
treatment Treatment @relation(fields: [treatmentId], references: [id], onDelete: Cascade)

View File

@@ -14,7 +14,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { LabOrgGuard } from '../../common/guards/lab-org.guard';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CasesService } from './cases.service';
import { ListLabCasesDto, UpdateLabCaseImportantDto, AssignLabCaseTaskDto } from './dto/cases.dto';
import { ListLabCasesDto, UpdateLabCaseImportantDto, UpdateLabCaseExternalCodeDto, AssignLabCaseTaskDto } from './dto/cases.dto';
@ApiTags('cases')
@ApiBearerAuth('JWT-auth')
@@ -82,6 +82,17 @@ export class CasesController {
return this.casesService.setCaseImportant(id, dto, organizationId, req.user.id, req.user.language);
}
@Patch(':id/external-code')
@ApiOperation({ summary: 'Set or clear the optional external order code for a case' })
setCaseExternalCode(
@Param('id') id: string,
@Body() dto: UpdateLabCaseExternalCodeDto,
@Req() req,
) {
const organizationId = this.casesService.getOrganizationIdFromUser(req.user);
return this.casesService.setCaseExternalCode(id, dto, organizationId, req.user.id, req.user.language);
}
@Patch(':caseId/tasks/:taskId/assign')
@ApiOperation({ summary: 'Assign or unassign a task to lab staff' })
assignTask(

View File

@@ -14,7 +14,7 @@ import {
} from '../catalog/catalog-label.service';
import { ProsthesisCatalogService } from '../prosthesis-catalog/prosthesis-catalog.service';
import { normalizeTeeth } from '../treatments/treatment.utils';
import { ListLabCasesDto, UpdateLabCaseImportantDto, AssignLabCaseTaskDto } from './dto/cases.dto';
import { ListLabCasesDto, UpdateLabCaseImportantDto, UpdateLabCaseExternalCodeDto, AssignLabCaseTaskDto } from './dto/cases.dto';
import {
isLabCaseOverdue,
} from '../../common/lab-case-due-date';
@@ -131,7 +131,15 @@ export class CasesService {
patient: { select: { id: true, firstName: true, lastName: true, mobile: true } },
},
},
tasks: { select: { id: true, status: true, prosthesisTypeCode: true, teeth: true } },
tasks: {
select: {
id: true,
status: true,
prosthesisTypeCode: true,
teeth: true,
selectionGroupId: true,
},
},
},
orderBy: [{ sentAt: 'desc' }],
skip,
@@ -244,7 +252,15 @@ export class CasesService {
patient: { select: { id: true, firstName: true, lastName: true, mobile: true } },
},
},
tasks: { select: { id: true, status: true, prosthesisTypeCode: true, teeth: true } },
tasks: {
select: {
id: true,
status: true,
prosthesisTypeCode: true,
teeth: true,
selectionGroupId: true,
},
},
},
orderBy: [{ sentAt: 'desc' }],
skip,
@@ -405,6 +421,46 @@ export class CasesService {
return { success: true, data: await this.mapLabCaseDetail(labCase, localeInput) };
}
async setCaseExternalCode(
labCaseId: string,
dto: UpdateLabCaseExternalCodeDto,
labOrganizationId: string,
actorUserId: string,
localeInput?: string | null,
) {
await this.assertCanEditCases(actorUserId, labOrganizationId);
const existing = await this.prisma.labCase.findFirst({
where: {
id: labCaseId,
sentAt: { not: null },
sends: { some: { organizationId: labOrganizationId } },
},
select: { id: true },
});
if (!existing) {
throw new NotFoundException('Case not found');
}
const externalCode =
dto.externalCode == null || !String(dto.externalCode).trim()
? null
: String(dto.externalCode).trim().slice(0, 64);
await this.prisma.labCase.update({
where: { id: labCaseId },
data: { externalCode },
});
const labCase = await this.prisma.labCase.findFirstOrThrow({
where: { id: labCaseId },
include: labCaseListInclude,
});
return { success: true, data: await this.mapLabCaseDetail(labCase, localeInput) };
}
async listAssignableStaff(labOrganizationId: string, actorUserId: string) {
await this.assertCanEditCases(actorUserId, labOrganizationId);
@@ -648,6 +704,7 @@ export class CasesService {
dueDate: lc.dueDate?.toISOString() ?? null,
isOverdue: isLabCaseOverdue(lc.dueDate, lc.tasks),
isImportant: lc.isImportant,
externalCode: lc.externalCode ?? null,
shareUrl,
clinic: lc.treatment.organization,
patient: lc.treatment.patient,
@@ -665,6 +722,7 @@ export class CasesService {
treatmentDetailId: row.treatmentDetailId,
tooth: row.tooth,
prosthesisTypeCode: row.prosthesisTypeCode,
selectionGroupId: row.selectionGroupId?.trim() || undefined,
})),
attachments: lc.attachments.map((row) => ({
id: row.attachment.id,
@@ -764,7 +822,16 @@ export class CasesService {
groups.set(key, entry);
}
return [...groups.values()];
return [...groups.values()].map((group) => ({
treatmentDetailId: group.treatmentDetailId,
teeth: [...new Set(group.teeth)],
treatmentType: group.treatmentType,
prosthesisTypeCode: group.prosthesisTypeCode,
prosthesisTypeLabel: group.prosthesisTypeLabel,
selectionGroupId: group.selectionGroupId || undefined,
connected: Boolean(group.selectionGroupId) && [...new Set(group.teeth)].length > 1,
tasks: group.tasks,
}));
}
private mapTask(

View File

@@ -1,11 +1,18 @@
import { Transform } from 'class-transformer';
import { IsBoolean, IsDateString, IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
import { IsBoolean, IsDateString, IsInt, IsOptional, IsString, IsUUID, Max, MaxLength, Min } from 'class-validator';
export class UpdateLabCaseImportantDto {
@IsBoolean()
isImportant: boolean;
}
export class UpdateLabCaseExternalCodeDto {
@IsOptional()
@IsString()
@MaxLength(64)
externalCode?: string | null;
}
export class AssignLabCaseTaskDto {
@IsOptional()
@IsUUID()