import { BadRequestException, ConflictException, Injectable, NotFoundException, } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import type { RequestApprove, RequestCreate, RequestReject, RequestsResponse, RequestSummary, WorkOrderDetail, } from '@siop/shared'; import type { AuthenticatedUser } from '../auth/current-user.decorator'; import { PermissionsService } from '../permissions/permissions.service'; import { PrismaService } from '../prisma/prisma.service'; import { WorkOrdersService } from '../work-orders/work-orders.service'; const requestInclude = { asset: { include: { location: { include: { parent: true } } } }, requestedBy: true, workOrder: true, } satisfies Prisma.RequestInclude; type RequestRow = Prisma.RequestGetPayload<{ include: typeof requestInclude }>; @Injectable() export class RequestsService { constructor( private readonly prisma: PrismaService, private readonly permissions: PermissionsService, private readonly workOrders: WorkOrdersService, ) {} private async scope(user: AuthenticatedUser): Promise { const viewOther = await this.permissions.can(user.roleId, 'REQUESTS', 'viewOther'); return viewOther ? {} : { requestedById: user.userId }; } async list(user: AuthenticatedUser): Promise { const rows = await this.prisma.request.findMany({ where: await this.scope(user), include: requestInclude, orderBy: { createdAt: 'desc' }, }); rows.sort((a, b) => Number(b.isPersonTrapped) - Number(a.isPersonTrapped)); return { requests: rows.map((r) => this.toDto(r)) }; } async create(dto: RequestCreate, user: AuthenticatedUser): Promise { const asset = await this.prisma.asset.findUnique({ where: { id: dto.assetId } }); if (!asset) throw new BadRequestException('Équipement inconnu'); for (let essai = 0; ; essai++) { try { const created = await this.prisma.request.create({ data: { reference: await this.nextReference(), description: dto.description, isPersonTrapped: dto.isPersonTrapped ?? false, assetId: dto.assetId, requestedById: user.userId, }, include: requestInclude, }); return this.toDto(created); } catch (e) { if ( e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002' && essai < 3 ) { continue; } throw e; } } } /** Approuver = créer l'OT lié (1-1). Une demande ne se traite qu'une fois. */ async approve( id: string, dto: RequestApprove, user: AuthenticatedUser, ): Promise { const request = await this.prisma.request.findUnique({ where: { id }, include: requestInclude, }); if (!request) throw new NotFoundException('Demande inconnue'); if (request.status !== 'RECEIVED') { throw new ConflictException('Cette demande a déjà été traitée'); } const created = await this.workOrders.createRaw({ title: dto.title ?? request.description.slice(0, 120), description: `${request.description}\n\n(Demande ${request.reference} — ${this.requesterLabel(request)})`, type: 'CORRECTIVE', priority: dto.priority ?? (request.isPersonTrapped ? 'PERSON_TRAPPED' : 'MEDIUM'), assetId: request.assetId, dueDate: dto.dueDate, assigneeIds: dto.assigneeIds, createdById: user.userId, eventKind: 'FROM_REQUEST', eventMessage: `OT créé depuis la demande ${request.reference}`, }); await this.prisma.request.update({ where: { id }, data: { status: 'APPROVED', workOrderId: created.id }, }); return this.workOrders.get(created.id, user); } async reject( id: string, dto: RequestReject, user: AuthenticatedUser, ): Promise { void user; const request = await this.prisma.request.findUnique({ where: { id } }); if (!request) throw new NotFoundException('Demande inconnue'); if (request.status !== 'RECEIVED') { throw new ConflictException('Cette demande a déjà été traitée'); } const updated = await this.prisma.request.update({ where: { id }, data: { status: 'REJECTED', rejectionReason: dto.reason }, include: requestInclude, }); return this.toDto(updated); } private async nextReference(): Promise { const [{ nextval }] = await this.prisma.$queryRaw<[{ nextval: bigint }]>` SELECT nextval('request_ref_seq')`; return `DEM-${new Date().getFullYear()}-${String(nextval).padStart(4, '0')}`; } private requesterLabel(row: RequestRow): string { return row.requestedBy?.displayName ?? row.requesterName ?? 'Portail'; } private toDto(row: RequestRow): RequestSummary { return { id: row.id, reference: row.reference, description: row.description, isPersonTrapped: row.isPersonTrapped, status: row.status, rejectionReason: row.rejectionReason, assetId: row.assetId, assetReference: row.asset.reference, siteName: row.asset.location.parent?.name ?? row.asset.location.name, requesterLabel: this.requesterLabel(row), workOrder: row.workOrder ? { id: row.workOrder.id, reference: row.workOrder.reference, status: row.workOrder.status, } : null, createdAt: row.createdAt.toISOString(), }; } }