improvement: v1 standalone treatment/case creation made possible.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsIn,
|
||||
IsOptional,
|
||||
@@ -133,3 +134,24 @@ export class ListPatientTreatmentHistoryDto {
|
||||
@IsOptional()
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export class CreateStandaloneTreatmentDto {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
patientId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
walkIn?: boolean;
|
||||
|
||||
@IsDateString()
|
||||
treatmentAt: string;
|
||||
}
|
||||
|
||||
export class ListDayTreatmentsDto {
|
||||
@IsDateString()
|
||||
from: string;
|
||||
|
||||
@IsDateString()
|
||||
to: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
@@ -24,6 +25,8 @@ import {
|
||||
SaveTreatmentDraftDto,
|
||||
SaveTreatmentLabCasesDto,
|
||||
UpdateLabCaseDueDateDto,
|
||||
CreateStandaloneTreatmentDto,
|
||||
ListDayTreatmentsDto,
|
||||
} from './dto/treatment.dto';
|
||||
import { CreateLabCaseCommentDto } from '../lab-case-comments/dto/lab-case-comment.dto';
|
||||
import { LabCaseCommentsService } from '../lab-case-comments/lab-case-comments.service';
|
||||
@@ -83,6 +86,31 @@ export class TreatmentsController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get('day')
|
||||
@ApiOperation({ summary: 'Standalone (no appointment) treatments for a local day range' })
|
||||
listDayStandalone(
|
||||
@Query() query: ListDayTreatmentsDto,
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
) {
|
||||
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
|
||||
return this.treatmentsService.listDayStandalone(
|
||||
query.from,
|
||||
query.to,
|
||||
organizationId,
|
||||
req.user.id,
|
||||
);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a treatment without an appointment (TAB_TREATMENT_EDIT)' })
|
||||
createStandalone(
|
||||
@Body() dto: CreateStandaloneTreatmentDto,
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
) {
|
||||
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
|
||||
return this.treatmentsService.createStandalone(dto, organizationId, req.user.id);
|
||||
}
|
||||
|
||||
@Get('appointments/:appointmentId/draft')
|
||||
@ApiOperation({ summary: 'Get draft treatment for an appointment (TAB_TREATMENT_READ)' })
|
||||
getDraft(
|
||||
@@ -268,4 +296,84 @@ export class TreatmentsController {
|
||||
dto,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':treatmentId/draft')
|
||||
@ApiOperation({ summary: 'Get a treatment draft by treatment id (TAB_TREATMENT_READ)' })
|
||||
getDraftByTreatment(
|
||||
@Param('treatmentId') treatmentId: string,
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
) {
|
||||
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
|
||||
return this.treatmentsService.getDraftForTreatment(
|
||||
treatmentId,
|
||||
organizationId,
|
||||
req.user.id,
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':treatmentId/draft')
|
||||
@ApiOperation({ summary: 'Save draft treatment details by treatment id (TAB_TREATMENT_EDIT)' })
|
||||
saveDraftByTreatment(
|
||||
@Param('treatmentId') treatmentId: string,
|
||||
@Body() dto: SaveTreatmentDraftDto,
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
) {
|
||||
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
|
||||
return this.treatmentsService.saveDraftForTreatment(
|
||||
treatmentId,
|
||||
dto,
|
||||
organizationId,
|
||||
req.user.id,
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':treatmentId/lab-cases')
|
||||
@ApiOperation({ summary: 'Save lab case groupings by treatment id (TAB_TREATMENT_EDIT)' })
|
||||
saveLabCasesByTreatment(
|
||||
@Param('treatmentId') treatmentId: string,
|
||||
@Body() dto: SaveTreatmentLabCasesDto,
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
) {
|
||||
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
|
||||
return this.treatmentsService.saveLabCasesForTreatment(
|
||||
treatmentId,
|
||||
dto,
|
||||
organizationId,
|
||||
req.user.id,
|
||||
);
|
||||
}
|
||||
|
||||
@Post(':treatmentId/details/:detailClientKey/attachments')
|
||||
@ApiOperation({ summary: 'Upload attachments for a standalone treatment detail' })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@UseInterceptors(
|
||||
FilesInterceptor('files', 20, {
|
||||
storage: memoryStorage(),
|
||||
}),
|
||||
)
|
||||
uploadDetailAttachmentsByTreatment(
|
||||
@Param('treatmentId') treatmentId: 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.uploadDetailAttachmentsForTreatment(
|
||||
treatmentId,
|
||||
detailClientKey,
|
||||
files,
|
||||
organizationId,
|
||||
req.user.id,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':treatmentId')
|
||||
@ApiOperation({ summary: 'Delete an empty standalone treatment (no details)' })
|
||||
deleteStandalone(
|
||||
@Param('treatmentId') treatmentId: string,
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
) {
|
||||
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
|
||||
return this.treatmentsService.deleteStandalone(treatmentId, organizationId, req.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SaveTreatmentDraftDto,
|
||||
SaveTreatmentLabCasesDto,
|
||||
UpdateLabCaseDueDateDto,
|
||||
CreateStandaloneTreatmentDto,
|
||||
} from './dto/treatment.dto';
|
||||
import {
|
||||
isLabCaseFullyCompleted,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
isActorTreatmentProvider,
|
||||
treatmentProviderScopeWhere,
|
||||
} from '../../common/treatment-provider-scope';
|
||||
import { ensureWalkInPatient } from '../../common/walk-in-patient';
|
||||
|
||||
const sentLabCaseInclude = {
|
||||
treatment: {
|
||||
@@ -67,6 +69,9 @@ const sentLabCaseInclude = {
|
||||
type SentLabCaseRow = Prisma.LabCaseGetPayload<{ include: typeof sentLabCaseInclude }>;
|
||||
|
||||
const treatmentInclude = {
|
||||
patient: {
|
||||
select: { id: true, firstName: true, lastName: true, isWalkIn: true },
|
||||
},
|
||||
details: {
|
||||
orderBy: [{ sortOrder: 'asc' as const }],
|
||||
include: {
|
||||
@@ -192,6 +197,108 @@ export class TreatmentsService {
|
||||
return { success: true, data: items.map((t) => this.mapTreatment(t)) };
|
||||
}
|
||||
|
||||
async createStandalone(
|
||||
dto: CreateStandaloneTreatmentDto,
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertCanEditTreatment(actorUserId, organizationId);
|
||||
|
||||
const walkIn = Boolean(dto.walkIn);
|
||||
if (!walkIn && !dto.patientId) {
|
||||
throw new AppException(ErrorCode.TREATMENT_PATIENT_OR_WALK_IN, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const treatmentAt = new Date(dto.treatmentAt);
|
||||
if (Number.isNaN(treatmentAt.getTime())) {
|
||||
throw new AppException(ErrorCode.VALIDATION_INVALID_REQUEST, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
let patientId: string;
|
||||
if (walkIn) {
|
||||
const sentinel = await ensureWalkInPatient(this.prisma, organizationId);
|
||||
patientId = sentinel.id;
|
||||
} else {
|
||||
await this.ensurePatientExists(dto.patientId!);
|
||||
const patient = await this.prisma.patient.findUnique({
|
||||
where: { id: dto.patientId! },
|
||||
select: { isWalkIn: true },
|
||||
});
|
||||
if (patient?.isWalkIn) {
|
||||
throw new AppException(ErrorCode.TREATMENT_PATIENT_OR_WALK_IN, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
patientId = dto.patientId!;
|
||||
}
|
||||
|
||||
const treatment = await this.prisma.treatment.create({
|
||||
data: {
|
||||
organizationId,
|
||||
patientId,
|
||||
appointmentId: null,
|
||||
providerUserId: actorUserId,
|
||||
title: generateTreatmentTitle([]),
|
||||
treatmentAt,
|
||||
},
|
||||
include: treatmentInclude,
|
||||
});
|
||||
|
||||
return { success: true, data: this.mapTreatment(treatment) };
|
||||
}
|
||||
|
||||
async deleteStandalone(
|
||||
treatmentId: string,
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertCanEditTreatment(actorUserId, organizationId);
|
||||
const treatment = await this.ensureTreatmentProvider(
|
||||
treatmentId,
|
||||
organizationId,
|
||||
actorUserId,
|
||||
);
|
||||
|
||||
if (treatment.appointmentId) {
|
||||
throw new AppException(ErrorCode.TREATMENT_NOT_STANDALONE, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
const detailCount = await this.prisma.treatmentDetail.count({
|
||||
where: { treatmentId },
|
||||
});
|
||||
if (detailCount > 0) {
|
||||
throw new AppException(ErrorCode.TREATMENT_HAS_DETAILS, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
await this.prisma.treatment.delete({ where: { id: treatmentId } });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async listDayStandalone(
|
||||
fromIso: string,
|
||||
toIso: string,
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertCanReadTreatment(actorUserId, organizationId);
|
||||
const from = new Date(fromIso);
|
||||
const to = new Date(toIso);
|
||||
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || to <= from) {
|
||||
throw new AppException(ErrorCode.VALIDATION_INVALID_REQUEST, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const items = await this.prisma.treatment.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
appointmentId: null,
|
||||
treatmentAt: { gte: from, lt: to },
|
||||
...treatmentProviderScopeWhere(actorUserId),
|
||||
},
|
||||
include: treatmentInclude,
|
||||
orderBy: [{ treatmentAt: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
|
||||
return { success: true, data: items.map((t) => this.mapTreatment(t)) };
|
||||
}
|
||||
|
||||
async listPatientLabCases(
|
||||
patientId: string,
|
||||
organizationId: string,
|
||||
@@ -322,16 +429,20 @@ export class TreatmentsService {
|
||||
}))
|
||||
.sort((a, b) => a.prosthesisTypeCode.localeCompare(b.prosthesisTypeCode));
|
||||
|
||||
const patient = lc.treatment.patient;
|
||||
const treatment = lc.treatment;
|
||||
if (!treatment) {
|
||||
throw new AppException(ErrorCode.LAB_CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
const patient = treatment.patient;
|
||||
|
||||
return {
|
||||
labCaseId: lc.id,
|
||||
patientId: patient.id,
|
||||
patientFirstName: patient.firstName,
|
||||
patientLastName: patient.lastName,
|
||||
treatmentId: lc.treatment.id,
|
||||
appointmentId: lc.treatment.appointmentId,
|
||||
treatmentAt: lc.treatment.treatmentAt.toISOString(),
|
||||
treatmentId: treatment.id,
|
||||
appointmentId: treatment.appointmentId,
|
||||
treatmentAt: treatment.treatmentAt.toISOString(),
|
||||
detailClientId: detail?.clientKey ?? detailLink?.treatmentDetailId ?? '',
|
||||
teeth: detailTeeth,
|
||||
prosthesisGroups,
|
||||
@@ -385,6 +496,15 @@ export class TreatmentsService {
|
||||
return { success: true, data: treatment ? this.mapTreatment(treatment) : null };
|
||||
}
|
||||
|
||||
async getDraftForTreatment(
|
||||
treatmentId: string,
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
const treatment = await this.ensureTreatmentProvider(treatmentId, organizationId, actorUserId);
|
||||
return { success: true, data: this.mapTreatment(treatment) };
|
||||
}
|
||||
|
||||
async saveDraftForAppointment(
|
||||
appointmentId: string,
|
||||
dto: SaveTreatmentDraftDto,
|
||||
@@ -398,11 +518,78 @@ export class TreatmentsService {
|
||||
actorUserId,
|
||||
);
|
||||
|
||||
for (const d of dto.details) {
|
||||
const existing = await this.prisma.treatment.findFirst({
|
||||
where: { appointmentId: appointment.id, organizationId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const treatmentId = existing
|
||||
? existing.id
|
||||
: (
|
||||
await this.prisma.treatment.create({
|
||||
data: {
|
||||
organizationId,
|
||||
patientId: appointment.patientId,
|
||||
appointmentId: appointment.id,
|
||||
providerUserId: actorUserId,
|
||||
title: generateTreatmentTitle([]),
|
||||
treatmentAt: appointment.startAt,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
).id;
|
||||
|
||||
const saved = await this.persistDraftDetails({
|
||||
treatmentId,
|
||||
organizationId,
|
||||
actorUserId,
|
||||
dto,
|
||||
pendingAppointmentId: appointment.id,
|
||||
treatmentAt: appointment.startAt,
|
||||
patientId: appointment.patientId,
|
||||
});
|
||||
|
||||
return { success: true, data: this.mapTreatment(saved) };
|
||||
}
|
||||
|
||||
async saveDraftForTreatment(
|
||||
treatmentId: string,
|
||||
dto: SaveTreatmentDraftDto,
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertCanEditTreatment(actorUserId, organizationId);
|
||||
const existing = await this.ensureTreatmentProvider(treatmentId, organizationId, actorUserId);
|
||||
|
||||
const saved = await this.persistDraftDetails({
|
||||
treatmentId,
|
||||
organizationId,
|
||||
actorUserId,
|
||||
dto,
|
||||
pendingAppointmentId: existing.appointmentId,
|
||||
pendingTreatmentId: existing.appointmentId ? null : existing.id,
|
||||
treatmentAt: existing.appointmentId ? existing.treatmentAt : existing.treatmentAt,
|
||||
patientId: existing.patientId,
|
||||
});
|
||||
|
||||
return { success: true, data: this.mapTreatment(saved) };
|
||||
}
|
||||
|
||||
private async persistDraftDetails(args: {
|
||||
treatmentId: string;
|
||||
organizationId: string;
|
||||
actorUserId: string;
|
||||
dto: SaveTreatmentDraftDto;
|
||||
pendingAppointmentId: string | null;
|
||||
pendingTreatmentId?: string | null;
|
||||
treatmentAt: Date;
|
||||
patientId: string;
|
||||
}) {
|
||||
for (const d of args.dto.details) {
|
||||
this.treatmentCatalog.assertKnownTreatmentType(d.treatmentType);
|
||||
}
|
||||
|
||||
const normalizedDetails = dto.details.map((d, index) => {
|
||||
const normalizedDetails = args.dto.details.map((d, index) => {
|
||||
const teeth = normalizeTeeth(d.teeth);
|
||||
const toothSelectionGroups = normalizeToothSelectionGroups(
|
||||
d.toothSelectionGroups,
|
||||
@@ -422,41 +609,23 @@ export class TreatmentsService {
|
||||
normalizedDetails.map((d) => ({ treatmentType: d.treatmentType, teeth: d.teeth })),
|
||||
);
|
||||
|
||||
const treatment = await this.prisma.$transaction(async (tx) => {
|
||||
const existing = await tx.treatment.findFirst({
|
||||
where: { appointmentId: appointment.id, organizationId },
|
||||
select: { id: true },
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const saved = await tx.treatment.update({
|
||||
where: { id: args.treatmentId },
|
||||
data: {
|
||||
title,
|
||||
treatmentAt: args.treatmentAt,
|
||||
patientId: args.patientId,
|
||||
providerUserId: args.actorUserId,
|
||||
},
|
||||
});
|
||||
|
||||
const saved = existing
|
||||
? await tx.treatment.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
title,
|
||||
treatmentAt: appointment.startAt,
|
||||
patientId: appointment.patientId,
|
||||
providerUserId: actorUserId,
|
||||
},
|
||||
})
|
||||
: await tx.treatment.create({
|
||||
data: {
|
||||
organizationId,
|
||||
patientId: appointment.patientId,
|
||||
appointmentId: appointment.id,
|
||||
providerUserId: actorUserId,
|
||||
title,
|
||||
treatmentAt: appointment.startAt,
|
||||
},
|
||||
});
|
||||
|
||||
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, labCaseLink: { select: { labCase: { select: { sentAt: true } } } } },
|
||||
})
|
||||
: [];
|
||||
const existingDetails = await tx.treatmentDetail.findMany({
|
||||
where: { treatmentId: saved.id },
|
||||
select: { id: true, labCaseLink: { select: { labCase: { select: { sentAt: true } } } } },
|
||||
});
|
||||
|
||||
const lockedDetailIds = new Set(
|
||||
existingDetails
|
||||
@@ -504,11 +673,14 @@ export class TreatmentsService {
|
||||
});
|
||||
|
||||
const allowedAttachmentIds = new Set(d.attachmentIds);
|
||||
const pendingWhere: Prisma.TreatmentDetailAttachmentWhereInput = {
|
||||
detailClientKey: d.clientId,
|
||||
...(args.pendingAppointmentId
|
||||
? { appointmentId: args.pendingAppointmentId }
|
||||
: { treatmentId: args.pendingTreatmentId ?? saved.id }),
|
||||
};
|
||||
const pendingAttachments = await tx.treatmentDetailAttachment.findMany({
|
||||
where: {
|
||||
appointmentId: appointment.id,
|
||||
detailClientKey: d.clientId,
|
||||
},
|
||||
where: pendingWhere,
|
||||
});
|
||||
|
||||
for (const attachment of pendingAttachments) {
|
||||
@@ -517,7 +689,12 @@ export class TreatmentsService {
|
||||
} else {
|
||||
await tx.treatmentDetailAttachment.update({
|
||||
where: { id: attachment.id },
|
||||
data: { detailId: row.id, appointmentId: null, detailClientKey: null },
|
||||
data: {
|
||||
detailId: row.id,
|
||||
appointmentId: null,
|
||||
treatmentId: null,
|
||||
detailClientKey: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -535,8 +712,6 @@ export class TreatmentsService {
|
||||
include: treatmentInclude,
|
||||
});
|
||||
});
|
||||
|
||||
return { success: true, data: this.mapTreatment(treatment) };
|
||||
}
|
||||
|
||||
async saveLabCasesForAppointment(
|
||||
@@ -561,6 +736,20 @@ export class TreatmentsService {
|
||||
throw new AppException(ErrorCode.TREATMENT_SAVE_DETAILS_BEFORE_LAB, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
return this.saveLabCasesForTreatment(treatment.id, dto, organizationId, actorUserId);
|
||||
}
|
||||
|
||||
async saveLabCasesForTreatment(
|
||||
treatmentId: string,
|
||||
dto: SaveTreatmentLabCasesDto,
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertCanEditTreatment(actorUserId, organizationId);
|
||||
await this.ensureTreatmentProvider(treatmentId, organizationId, actorUserId);
|
||||
|
||||
const treatment = { id: treatmentId };
|
||||
|
||||
const detailIds = dto.labCases.map((lc) => lc.treatmentDetailId);
|
||||
const uniqueDetailIds = new Set(detailIds);
|
||||
if (uniqueDetailIds.size !== detailIds.length) {
|
||||
@@ -658,6 +847,7 @@ export class TreatmentsService {
|
||||
: await tx.labCase.create({
|
||||
data: {
|
||||
treatmentId: treatment.id,
|
||||
origin: 'CLINIC_DISPATCH',
|
||||
clientKey: lc.clientId,
|
||||
sortOrder: index,
|
||||
destinationOrganizationId: lc.destinationOrganizationId ?? null,
|
||||
@@ -679,6 +869,8 @@ export class TreatmentsService {
|
||||
data: lc.toothProsthesis.map((tp) => ({
|
||||
labCaseId: row.id,
|
||||
treatmentDetailId: tp.treatmentDetailId,
|
||||
sourceKey: tp.treatmentDetailId,
|
||||
treatmentType: detailById.get(tp.treatmentDetailId)?.treatmentType ?? 'prosthesis',
|
||||
tooth: tp.tooth,
|
||||
prosthesisTypeCode: tp.prosthesisTypeCode,
|
||||
selectionGroupId: tp.selectionGroupId?.trim() || '',
|
||||
@@ -763,7 +955,17 @@ export class TreatmentsService {
|
||||
throw new AppException(ErrorCode.TREATMENT_CASE_ONE_DETAIL, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
assertCompleteToothProsthesisMap(labCase);
|
||||
if (!labCase.treatment) {
|
||||
throw new AppException(ErrorCode.LAB_CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
assertCompleteToothProsthesisMap({
|
||||
details: labCase.details,
|
||||
toothProsthesis: labCase.toothProsthesis.filter(
|
||||
(row): row is typeof row & { treatmentDetailId: string } =>
|
||||
Boolean(row.treatmentDetailId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!isActorTreatmentProvider(labCase.treatment, actorUserId)) {
|
||||
throw new AppException(ErrorCode.TREATMENT_ONLY_PROVIDER_SEND, HttpStatus.FORBIDDEN);
|
||||
@@ -1011,6 +1213,72 @@ export class TreatmentsService {
|
||||
return { success: true, data: created };
|
||||
}
|
||||
|
||||
async uploadDetailAttachmentsForTreatment(
|
||||
treatmentId: string,
|
||||
detailClientKey: string,
|
||||
files: Express.Multer.File[],
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertCanEditTreatment(actorUserId, organizationId);
|
||||
await this.ensureTreatmentProvider(treatmentId, organizationId, actorUserId);
|
||||
|
||||
if (!detailClientKey?.trim()) {
|
||||
throw new AppException(ErrorCode.TREATMENT_DETAIL_KEY_REQUIRED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!files?.length) {
|
||||
throw new AppException(ErrorCode.TREATMENT_FILE_REQUIRED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const existingDetail = await this.prisma.treatmentDetail.findFirst({
|
||||
where: {
|
||||
clientKey: detailClientKey,
|
||||
treatmentId,
|
||||
},
|
||||
select: {
|
||||
labCaseLink: {
|
||||
select: { labCase: { select: { sentAt: true } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (existingDetail?.labCaseLink?.labCase.sentAt) {
|
||||
throw new AppException(ErrorCode.TREATMENT_DETAIL_SENT, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
const orgDir = join(this.uploadRoot, organizationId);
|
||||
mkdirSync(orgDir, { recursive: true });
|
||||
|
||||
const created: {
|
||||
id: string;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
}[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const storageName = `${randomUUID()}-${file.originalname.replace(/[^\w.\-()+]/g, '_')}`;
|
||||
const storagePath = join(orgDir, storageName);
|
||||
const { writeFileSync } = await import('fs');
|
||||
writeFileSync(storagePath, file.buffer);
|
||||
|
||||
const attachment = await this.prisma.treatmentDetailAttachment.create({
|
||||
data: {
|
||||
treatmentId,
|
||||
detailClientKey,
|
||||
fileName: file.originalname,
|
||||
mimeType: file.mimetype || 'application/octet-stream',
|
||||
sizeBytes: file.size,
|
||||
storagePath,
|
||||
},
|
||||
});
|
||||
|
||||
created.push(this.mapAttachment(attachment));
|
||||
}
|
||||
|
||||
return { success: true, data: created };
|
||||
}
|
||||
|
||||
async streamAttachmentFile(
|
||||
attachmentId: string,
|
||||
organizationId: string,
|
||||
@@ -1031,6 +1299,7 @@ export class TreatmentsService {
|
||||
},
|
||||
},
|
||||
{ appointmentId: { not: null } },
|
||||
{ treatmentId: { not: null } },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
@@ -1060,6 +1329,20 @@ export class TreatmentsService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!attachment.detail && attachment.treatmentId) {
|
||||
const treatment = await this.prisma.treatment.findFirst({
|
||||
where: {
|
||||
id: attachment.treatmentId,
|
||||
organizationId,
|
||||
providerUserId: actorUserId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (!treatment) {
|
||||
throw new AppException(ErrorCode.TREATMENT_ATTACHMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
if (!existsSync(attachment.storagePath)) {
|
||||
throw new AppException(ErrorCode.TREATMENT_FILE_UNAVAILABLE, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
@@ -1075,8 +1358,15 @@ export class TreatmentsService {
|
||||
id: string;
|
||||
patientId: string;
|
||||
appointmentId: string | null;
|
||||
providerUserId?: string;
|
||||
title: string;
|
||||
treatmentAt: Date;
|
||||
patient?: {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isWalkIn: boolean;
|
||||
};
|
||||
details: Array<{
|
||||
id: string;
|
||||
clientKey: string | null;
|
||||
@@ -1127,8 +1417,17 @@ export class TreatmentsService {
|
||||
id: treatment.id,
|
||||
patientId: treatment.patientId,
|
||||
appointmentId: treatment.appointmentId,
|
||||
providerUserId: treatment.providerUserId ?? null,
|
||||
title: treatment.title,
|
||||
treatmentAt: treatment.treatmentAt.toISOString(),
|
||||
patient: treatment.patient
|
||||
? {
|
||||
id: treatment.patient.id,
|
||||
firstName: treatment.patient.firstName,
|
||||
lastName: treatment.patient.lastName,
|
||||
isWalkIn: treatment.patient.isWalkIn,
|
||||
}
|
||||
: null,
|
||||
details: treatment.details.map((d) => this.mapDetail(d)),
|
||||
labCases: treatment.labCases.map((lc) => this.mapLabCase(lc)),
|
||||
documents,
|
||||
@@ -1209,7 +1508,7 @@ export class TreatmentsService {
|
||||
organization?: { id: string; name: string };
|
||||
}>;
|
||||
toothProsthesis?: Array<{
|
||||
treatmentDetailId: string;
|
||||
treatmentDetailId: string | null;
|
||||
tooth: string;
|
||||
prosthesisTypeCode: string;
|
||||
selectionGroupId?: string;
|
||||
@@ -1319,6 +1618,24 @@ export class TreatmentsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureTreatmentProvider(
|
||||
treatmentId: string,
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
const treatment = await this.prisma.treatment.findFirst({
|
||||
where: { id: treatmentId, organizationId },
|
||||
include: treatmentInclude,
|
||||
});
|
||||
if (!treatment) {
|
||||
throw new AppException(ErrorCode.TREATMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (!isActorTreatmentProvider(treatment, actorUserId)) {
|
||||
throw new AppException(ErrorCode.TREATMENT_ONLY_PROVIDER_SEND, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return treatment;
|
||||
}
|
||||
|
||||
private async ensureAppointmentProvider(
|
||||
appointmentId: string,
|
||||
organizationId: string,
|
||||
|
||||
Reference in New Issue
Block a user