import { Prisma } from '@prisma/client'; import { prisma } from '../db/prisma'; import { StatutEmprunt } from '../models/enums'; import { AppError } from '../errors/app-error'; export type EmpruntAvecMateriel = Prisma.EmpruntGetPayload<{ include: { materiel: { include: { categorie: true } }; checklists: { include: { elements: true } }; }; }>; export type EmpruntAvecChecklists = Prisma.EmpruntGetPayload<{ include: { checklists: { include: { elements: true } } }; }>; const STATUTS_EN_COURS: StatutEmprunt[] = ['EN_COURS', 'EN_RETARD']; const STATUTS_BLOQUANTS: StatutEmprunt[] = [ 'EN_ATTENTE_VALIDATION_DEPART', 'EN_COURS', 'EN_RETARD', 'EN_ATTENTE_VALIDATION_RETOUR', ]; export function findEnCoursParUtilisateur(utilisateurId: number): Promise { return prisma.emprunt.findMany({ where: { utilisateurId, statut: { in: STATUTS_EN_COURS }, }, include: { materiel: { include: { categorie: true } }, checklists: { include: { elements: true } }, }, orderBy: { dateRetourPrevue: 'asc' }, }); } export function findByIdAvecChecklists(id: number): Promise { return prisma.emprunt.findUnique({ where: { id }, include: { checklists: { include: { elements: true } } }, }); } export interface SignalerTentativeRetourData { empruntId: number; utilisateurId: number; materielId: number; campusId: number; sallePretId: number; posteEmpruntId: number; responsablesIds: number[]; description: string; } export function signalerTentativeRetour(data: SignalerTentativeRetourData): Promise { return prisma.$transaction(async (tx) => { const emprunt = await tx.emprunt.findFirst({ where: { id: data.empruntId, utilisateurId: data.utilisateurId, statut: { in: STATUTS_EN_COURS }, }, select: { id: true }, }); if (!emprunt) { throw new AppError(409, 'Emprunt non restituable'); } const dejaSignalee = await tx.historique.findFirst({ where: { empruntId: data.empruntId, action: 'TENTATIVE_MODIFICATION_RETOUR', }, select: { id: true }, }); if (dejaSignalee) { return false; } if (data.responsablesIds.length > 0) { await tx.notification.createMany({ data: data.responsablesIds.map((responsableId) => ({ utilisateurId: responsableId, titre: 'Tentative de modification au retour', message: data.description, type: 'TENTATIVE_MODIFICATION_RETOUR', })), }); } await tx.historique.create({ data: { utilisateurId: data.utilisateurId, empruntId: data.empruntId, materielId: data.materielId, campusId: data.campusId, sallePretId: data.sallePretId, posteEmpruntId: data.posteEmpruntId, action: 'TENTATIVE_MODIFICATION_RETOUR', description: data.description, dateAction: new Date(), }, }); return true; }); } interface ChecklistElementData { accessoireId?: number; nomElement: string; etat: string; quantiteConstatee: number; commentaire?: string; } export interface CreerEmpruntData { utilisateurId: number; materielId: number; categorieId: number; campusId: number; sallePretId: number; posteEmpruntId: number; dateRetourPrevue: Date; modeIdentification: string; commentaireDepart?: string; elements: ChecklistElementData[]; statutEmprunt: StatutEmprunt; statutMateriel: string; responsablesIds: number[]; descriptionEcart?: string; } export function creerEmpruntComplet(data: CreerEmpruntData): Promise { return prisma.$transaction( async (tx) => { const empruntBloquant = await tx.emprunt.findFirst({ where: { utilisateurId: data.utilisateurId, statut: { in: STATUTS_BLOQUANTS }, materiel: { categorieId: data.categorieId }, }, select: { id: true }, }); if (empruntBloquant) { throw new AppError( 409, 'Un materiel de cette categorie est deja emprunte ou en attente de confirmation', ); } const reservation = await tx.materiel.updateMany({ where: { id: data.materielId, statut: 'DISPONIBLE' }, data: { statut: data.statutMateriel }, }); if (reservation.count === 0) { throw new AppError(409, 'Materiel non disponible'); } const maintenant = new Date(); const emprunt = await tx.emprunt.create({ data: { utilisateurId: data.utilisateurId, materielId: data.materielId, campusId: data.campusId, sallePretId: data.sallePretId, posteEmpruntId: data.posteEmpruntId, dateEmprunt: maintenant, dateRetourPrevue: data.dateRetourPrevue, statut: data.statutEmprunt, modeIdentificationEmprunt: data.modeIdentification, commentaireDepart: data.commentaireDepart ?? null, }, }); await tx.checklist.create({ data: { empruntId: emprunt.id, utilisateurId: data.utilisateurId, type: 'DEPART', dateVerification: maintenant, elements: { create: data.elements.map((element) => ({ accessoireId: element.accessoireId ?? null, nomElement: element.nomElement, etat: element.etat, quantiteConstatee: element.quantiteConstatee, commentaire: element.commentaire ?? null, })), }, }, }); if (data.descriptionEcart) { if (data.responsablesIds.length > 0) { await tx.notification.createMany({ data: data.responsablesIds.map((responsableId) => ({ utilisateurId: responsableId, titre: 'Ecart signale au depart', message: data.descriptionEcart as string, type: 'ECART_DEPART', })), }); } await tx.historique.create({ data: { utilisateurId: data.utilisateurId, empruntId: emprunt.id, materielId: data.materielId, campusId: data.campusId, sallePretId: data.sallePretId, posteEmpruntId: data.posteEmpruntId, action: 'ECART_DEPART_SIGNALE', description: data.descriptionEcart, dateAction: maintenant, }, }); } else { await tx.historique.create({ data: { utilisateurId: data.utilisateurId, empruntId: emprunt.id, materielId: data.materielId, campusId: data.campusId, sallePretId: data.sallePretId, posteEmpruntId: data.posteEmpruntId, action: 'EMPRUNT_AUTOMATIQUE', description: `Emprunt #${emprunt.id} active avec une checklist de depart inchangee`, dateAction: maintenant, }, }); } return tx.emprunt.findUniqueOrThrow({ where: { id: emprunt.id }, include: { materiel: { include: { categorie: true } }, checklists: { include: { elements: true } }, }, }); }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, ); } export interface RestituerEmpruntData { empruntId: number; utilisateurId: number; materielId: number; statutEmprunt: string; statutMateriel: string; modeIdentification: string; commentaireRetour?: string; elements: ChecklistElementData[]; campusId: number; sallePretId: number; posteEmpruntId: number; responsablesIds: number[]; descriptionEcart?: string; } export function restituerEmpruntComplet(data: RestituerEmpruntData): Promise { return prisma.$transaction(async (tx) => { const maintenant = new Date(); const transition = await tx.emprunt.updateMany({ where: { id: data.empruntId, utilisateurId: data.utilisateurId, statut: { in: STATUTS_EN_COURS }, }, data: { statut: data.statutEmprunt, ...(data.statutEmprunt === 'CLOTURE' ? { dateRetourReelle: maintenant } : {}), modeIdentificationRetour: data.modeIdentification, commentaireRetour: data.commentaireRetour ?? null, }, }); if (transition.count === 0) { throw new AppError(409, 'Emprunt non restituable'); } await tx.checklist.create({ data: { empruntId: data.empruntId, utilisateurId: data.utilisateurId, type: 'RETOUR', dateVerification: maintenant, elements: { create: data.elements.map((element) => ({ accessoireId: element.accessoireId ?? null, nomElement: element.nomElement, etat: element.etat, quantiteConstatee: element.quantiteConstatee, commentaire: element.commentaire ?? null, })), }, }, }); await tx.materiel.update({ where: { id: data.materielId }, data: { statut: data.statutMateriel }, }); if (data.descriptionEcart) { const tentativeDejaSignalee = await tx.historique.findFirst({ where: { empruntId: data.empruntId, action: 'TENTATIVE_MODIFICATION_RETOUR', }, select: { id: true }, }); if (!tentativeDejaSignalee && data.responsablesIds.length > 0) { await tx.notification.createMany({ data: data.responsablesIds.map((responsableId) => ({ utilisateurId: responsableId, titre: 'Changement signale au retour', message: data.descriptionEcart as string, type: 'ECART_RETOUR', })), }); } await tx.historique.create({ data: { utilisateurId: data.utilisateurId, empruntId: data.empruntId, materielId: data.materielId, campusId: data.campusId, sallePretId: data.sallePretId, posteEmpruntId: data.posteEmpruntId, action: 'ECART_RETOUR_SIGNALE', description: data.descriptionEcart, dateAction: maintenant, }, }); } else { await tx.historique.create({ data: { utilisateurId: data.utilisateurId, empruntId: data.empruntId, materielId: data.materielId, campusId: data.campusId, sallePretId: data.sallePretId, posteEmpruntId: data.posteEmpruntId, action: 'RESTITUTION_AUTOMATIQUE', description: `Emprunt #${data.empruntId} cloture avec une checklist de retour inchangee`, dateAction: maintenant, }, }); } return tx.emprunt.findUniqueOrThrow({ where: { id: data.empruntId }, include: { materiel: { include: { categorie: true } }, checklists: { include: { elements: true } }, }, }); }); }