183 lines
5.8 KiB
TypeScript
183 lines
5.8 KiB
TypeScript
import { AppError } from '../errors/app-error';
|
|
import {
|
|
CreerEmpruntRequest,
|
|
RestituerEmpruntRequest,
|
|
ChecklistElementRequest,
|
|
} from '../dtos/emprunt.dto';
|
|
import {
|
|
findEnCoursParUtilisateur,
|
|
creerEmpruntComplet,
|
|
findByIdAvecChecklists,
|
|
restituerEmpruntComplet,
|
|
EmpruntAvecMateriel,
|
|
RestituerEmpruntData,
|
|
} from '../repositories/emprunt.repository';
|
|
import { findById as findMaterielById } from '../repositories/materiel.repository';
|
|
import { findById as findPosteById } from '../repositories/poste-emprunt.repository';
|
|
import { findResponsablesParCampus } from '../repositories/utilisateur.repository';
|
|
import { StatutEmprunt } from '../models/enums';
|
|
|
|
const DUREE_EMPRUNT_JOURS = 14;
|
|
const STATUTS_RESTITUABLES: readonly string[] = ['EN_COURS', 'EN_RETARD'];
|
|
|
|
/* RG15 : un étudiant ne consulte que ses propres emprunts non restitués. */
|
|
export function listerMesEmpruntsEnCours(utilisateurId: number): Promise<EmpruntAvecMateriel[]> {
|
|
return findEnCoursParUtilisateur(utilisateurId);
|
|
}
|
|
|
|
/* RG07/RG10/RG11/RG12 : création d'un emprunt avec sa checklist de départ.
|
|
Les contrôles d'éligibilité (campus, disponibilité) sont faits ici ; l'écriture
|
|
atomique est déléguée au repository. */
|
|
export async function creerEmprunt(
|
|
utilisateurId: number,
|
|
campusId: number,
|
|
request: CreerEmpruntRequest,
|
|
): Promise<EmpruntAvecMateriel> {
|
|
const materiel = await findMaterielById(request.materielId);
|
|
if (!materiel) {
|
|
throw new AppError(404, 'Materiel introuvable');
|
|
}
|
|
if (materiel.campusId !== campusId) {
|
|
throw new AppError(403, 'Materiel rattache a un autre campus');
|
|
}
|
|
if (materiel.statut !== 'DISPONIBLE') {
|
|
throw new AppError(409, 'Materiel non disponible');
|
|
}
|
|
|
|
const poste = await findPosteById(request.posteEmpruntId);
|
|
if (!poste) {
|
|
throw new AppError(400, 'Poste introuvable');
|
|
}
|
|
if (poste.sallePret.campusId !== campusId) {
|
|
throw new AppError(403, 'Poste rattache a un autre campus');
|
|
}
|
|
|
|
const dateRetourPrevue = new Date();
|
|
dateRetourPrevue.setDate(dateRetourPrevue.getDate() + DUREE_EMPRUNT_JOURS);
|
|
|
|
return creerEmpruntComplet({
|
|
utilisateurId,
|
|
materielId: request.materielId,
|
|
campusId,
|
|
sallePretId: poste.sallePretId,
|
|
posteEmpruntId: request.posteEmpruntId,
|
|
dateRetourPrevue,
|
|
modeIdentification: request.modeIdentification,
|
|
commentaireDepart: request.commentaireDepart,
|
|
elements: request.elements,
|
|
});
|
|
}
|
|
|
|
interface ElementDepart {
|
|
accessoireId: number | null;
|
|
nomElement: string;
|
|
etat: string;
|
|
}
|
|
|
|
interface ResultatComparaison {
|
|
conforme: boolean;
|
|
auMoinsUnDeteriore: boolean;
|
|
details: string[];
|
|
}
|
|
|
|
function trouverRetour(
|
|
depart: ElementDepart,
|
|
retour: ChecklistElementRequest[],
|
|
): ChecklistElementRequest | undefined {
|
|
if (depart.accessoireId !== null) {
|
|
const parId = retour.find((element) => element.accessoireId === depart.accessoireId);
|
|
if (parId) {
|
|
return parId;
|
|
}
|
|
}
|
|
return retour.find((element) => element.nomElement === depart.nomElement);
|
|
}
|
|
|
|
/* RG17 : compare la checklist de départ à celle de retour. Un élément présent au
|
|
départ mais absent ou détérioré au retour rend la restitution non conforme (RG20). */
|
|
function comparerChecklists(
|
|
depart: ElementDepart[],
|
|
retour: ChecklistElementRequest[],
|
|
): ResultatComparaison {
|
|
const details: string[] = [];
|
|
let auMoinsUnDeteriore = false;
|
|
|
|
for (const elementDepart of depart) {
|
|
if (elementDepart.etat !== 'PRESENT') {
|
|
continue;
|
|
}
|
|
|
|
const elementRetour = trouverRetour(elementDepart, retour);
|
|
if (!elementRetour || elementRetour.etat === 'ABSENT') {
|
|
details.push(`${elementDepart.nomElement} manquant au retour`);
|
|
} else if (elementRetour.etat === 'DETERIORE') {
|
|
details.push(`${elementDepart.nomElement} deteriore au retour`);
|
|
auMoinsUnDeteriore = true;
|
|
}
|
|
}
|
|
|
|
return { conforme: details.length === 0, auMoinsUnDeteriore, details };
|
|
}
|
|
|
|
/* RG14-RG20 : restitution d'un emprunt avec comparaison des checklists, clôture
|
|
ou passage en non conforme, et création automatique d'anomalie + notifications. */
|
|
export async function restituerEmprunt(
|
|
utilisateurId: number,
|
|
empruntId: number,
|
|
request: RestituerEmpruntRequest,
|
|
): Promise<EmpruntAvecMateriel> {
|
|
const emprunt = await findByIdAvecChecklists(empruntId);
|
|
if (!emprunt) {
|
|
throw new AppError(404, 'Emprunt introuvable');
|
|
}
|
|
if (emprunt.utilisateurId !== utilisateurId) {
|
|
throw new AppError(403, 'Cet emprunt ne vous appartient pas');
|
|
}
|
|
if (!STATUTS_RESTITUABLES.includes(emprunt.statut)) {
|
|
throw new AppError(409, 'Emprunt non restituable');
|
|
}
|
|
|
|
const checklistDepart = emprunt.checklists.find((checklist) => checklist.type === 'DEPART');
|
|
if (!checklistDepart) {
|
|
throw new AppError(409, 'Checklist de depart introuvable');
|
|
}
|
|
|
|
const comparaison = comparerChecklists(
|
|
checklistDepart.elements.map((element) => ({
|
|
accessoireId: element.accessoireId,
|
|
nomElement: element.nomElement,
|
|
etat: element.etat,
|
|
})),
|
|
request.elements,
|
|
);
|
|
|
|
const statutEmprunt: StatutEmprunt = comparaison.conforme ? 'CLOTURE' : 'RETOUR_NON_CONFORME';
|
|
|
|
let statutMateriel = 'DISPONIBLE';
|
|
if (!comparaison.conforme) {
|
|
statutMateriel = comparaison.auMoinsUnDeteriore ? 'DETERIORE' : 'NON_CONFORME';
|
|
}
|
|
|
|
const donnees: RestituerEmpruntData = {
|
|
empruntId,
|
|
utilisateurId,
|
|
materielId: emprunt.materielId,
|
|
statutEmprunt,
|
|
statutMateriel,
|
|
modeIdentification: request.modeIdentification,
|
|
commentaireRetour: request.commentaireRetour,
|
|
elements: request.elements,
|
|
};
|
|
|
|
if (!comparaison.conforme) {
|
|
const responsables = await findResponsablesParCampus(emprunt.campusId);
|
|
donnees.anomalie = {
|
|
type: 'RETOUR_NON_CONFORME',
|
|
description: `Retour non conforme : ${comparaison.details.join(', ')}.`,
|
|
responsablesIds: responsables.map((responsable) => responsable.id),
|
|
};
|
|
}
|
|
|
|
return restituerEmpruntComplet(donnees);
|
|
}
|