feat(loans): confirm declared material state changes

This commit is contained in:
SaidSoighiri94
2026-07-24 13:41:40 +02:00
parent 3739e9b0a0
commit a562b3248f
26 changed files with 1832 additions and 282 deletions
@@ -5,7 +5,10 @@ import {
countEmpruntsParStatut,
countMaterielsParStatut,
countNotificationsNonLues,
deciderEcartResponsable,
findActiviteRecente,
findEcartResponsableById,
findEcartsResponsable,
findAnomalieResponsableById,
findAnomaliesResponsable,
findEmpruntsResponsable,
@@ -21,6 +24,7 @@ import {
HistoriqueResponsable,
MaterielResponsable,
NotificationResponsable,
EcartResponsable,
} from '../repositories/responsable.repository';
import {
STATUT_ANOMALIE,
@@ -72,6 +76,20 @@ export interface ChangerStatutAnomalieResponsableRequest {
observation?: string;
}
export interface DeciderEcartResponsableRequest {
decision: 'CONFIRMER' | 'REFUSER';
observation?: string;
statutMateriel?: StatutMateriel;
}
const STATUTS_MATERIEL_RETOUR_CONFIRMES: readonly StatutMateriel[] = [
'DISPONIBLE',
'NON_CONFORME',
'DETERIORE',
'MAINTENANCE',
'INDISPONIBLE',
];
const TRANSITIONS_ANOMALIE: Record<StatutAnomalie, StatutAnomalie[]> = {
DETECTEE: ['EN_COURS_TRAITEMENT'],
EN_COURS_TRAITEMENT: ['RESOLUE'],
@@ -180,6 +198,19 @@ export function parseAnomalieId(value: unknown): number {
return parsed;
}
export function parseEmpruntId(value: unknown): number {
if (typeof value !== 'string') {
throw new AppError(400, 'empruntId invalide');
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new AppError(400, 'empruntId invalide');
}
return parsed;
}
export function parsePositiveIntQuery(value: unknown, champ: string): number | undefined {
if (value === undefined) {
return undefined;
@@ -234,6 +265,101 @@ export function listerEmpruntsResponsable(
return findEmpruntsResponsable(campusId, filtres);
}
export function listerEcartsResponsable(
roleCode: string,
campusId: number,
): Promise<EcartResponsable[]> {
verifierResponsable(roleCode);
return findEcartsResponsable(campusId);
}
function decrireChecklist(ecart: EcartResponsable, type: 'DEPART' | 'RETOUR'): string {
const checklist = ecart.checklists.find((item) => item.type === type);
if (!checklist) {
throw new AppError(409, `Checklist de ${type.toLocaleLowerCase()} introuvable`);
}
return checklist.elements
.map((element) => `${element.nomElement}=${element.etat}/${element.quantiteConstatee}`)
.join(', ');
}
export async function deciderEcartResponsableService(
responsableId: number,
roleCode: string,
campusId: number,
empruntId: number,
request: DeciderEcartResponsableRequest,
): Promise<EcartResponsable> {
verifierResponsable(roleCode);
const ecart = await findEcartResponsableById(campusId, empruntId);
if (!ecart) {
throw new AppError(404, 'Ecart en attente introuvable');
}
const typeEcart = ecart.statut === 'EN_ATTENTE_VALIDATION_DEPART' ? 'DEPART' : 'RETOUR';
if (
typeEcart === 'RETOUR' &&
request.decision === 'CONFIRMER' &&
(!request.statutMateriel || !STATUTS_MATERIEL_RETOUR_CONFIRMES.includes(request.statutMateriel))
) {
throw new AppError(400, 'statutMateriel final requis pour confirmer le retour');
}
const description =
`Ecart ${typeEcart.toLocaleLowerCase()} ${request.decision.toLocaleLowerCase()} ` +
`pour l'emprunt #${ecart.id} : ${decrireChecklist(ecart, typeEcart)}.`;
let elementsDepartCorriges: Array<{ id: number; quantiteConstatee: number }> | undefined;
if (typeEcart === 'DEPART' && request.decision === 'REFUSER') {
const checklistDepart = ecart.checklists.find((checklist) => checklist.type === 'DEPART');
if (!checklistDepart) {
throw new AppError(409, 'Checklist de depart introuvable');
}
elementsDepartCorriges = checklistDepart.elements.map((element) => {
const accessoire = ecart.materiel.accessoires.find(
(liaison) => liaison.accessoireId === element.accessoireId,
);
return {
id: element.id,
quantiteConstatee: accessoire?.quantiteAttendue ?? 1,
};
});
}
try {
return await deciderEcartResponsable({
empruntId,
campusId,
responsableId,
statutAttendu: ecart.statut,
statutEmpruntFinal:
typeEcart === 'DEPART'
? 'EN_COURS'
: request.decision === 'CONFIRMER'
? 'RETOUR_NON_CONFORME'
: 'CLOTURE',
statutMaterielFinal:
typeEcart === 'DEPART'
? 'EMPRUNTE'
: request.decision === 'CONFIRMER'
? (request.statutMateriel as StatutMateriel)
: 'DISPONIBLE',
typeEcart,
decision: request.decision,
description,
observation: request.observation,
elementsDepartCorriges,
});
} catch (error) {
if (error instanceof Error && error.message === 'ECART_DEJA_TRAITE') {
throw new AppError(409, 'Cet ecart a deja ete traite');
}
throw error;
}
}
export function listerMaterielsResponsable(
roleCode: string,
campusId: number,