import { AppError } from '../errors/app-error'; import { changerStatutAnomalieResponsable, countAnomaliesParStatut, countEmpruntsParStatut, countMaterielsParStatut, countNotificationsNonLues, deciderEcartResponsable, findActiviteRecente, findEcartResponsableById, findEcartsResponsable, findAnomalieResponsableById, findAnomaliesResponsable, findEmpruntsResponsable, findHistoriqueResponsable, findMaterielsResponsable, findNotificationsResponsable, marquerNotificationLue, marquerNotificationsResponsableLues, StatutCount, ActiviteResponsable, AnomalieResponsable, EmpruntResponsable, HistoriqueResponsable, MaterielResponsable, NotificationResponsable, EcartResponsable, } from '../repositories/responsable.repository'; import { STATUT_ANOMALIE, STATUT_EMPRUNT, STATUT_MATERIEL, StatutAnomalie, StatutEmprunt, StatutMateriel, } from '../models/enums'; export interface DashboardResponsableData { materiels: StatutCount[]; emprunts: StatutCount[]; anomalies: StatutCount[]; notificationsNonLues: number; activiteRecente: ActiviteResponsable[]; } export interface ListerEmpruntsResponsableFiltres { statut?: StatutEmprunt; annee?: number; recherche?: string; tri?: TriEmpruntResponsable; ordre?: OrdreTri; } export type TriEmpruntResponsable = | 'dateEmprunt' | 'dateRetourPrevue' | 'dateRetourReelle' | 'materiel' | 'etudiant' | 'statut'; export type OrdreTri = 'asc' | 'desc'; export interface ListerMaterielsResponsableFiltres { statut?: StatutMateriel; categorieId?: number; recherche?: string; tri?: TriMaterielResponsable; ordre?: OrdreTri; } export type TriMaterielResponsable = | 'nom' | 'reference' | 'categorie' | 'marque' | 'etatGeneral' | 'statut'; export interface ListerAnomaliesResponsableFiltres { statut?: StatutAnomalie; type?: string; recherche?: string; tri?: TriAnomalieResponsable; ordre?: OrdreTri; } export type TriAnomalieResponsable = | 'dateDetection' | 'type' | 'materiel' | 'etudiant' | 'statut'; export interface ListerNotificationsResponsableFiltres { lu?: boolean; } export interface ListerHistoriqueResponsableFiltres { action?: string; utilisateurId?: number; materielId?: number; empruntId?: number; dateDebut?: Date; dateFin?: Date; recherche?: string; tri?: TriHistoriqueResponsable; ordre?: OrdreTri; } export type TriHistoriqueResponsable = | 'dateAction' | 'action' | 'utilisateur' | 'materiel' | 'emprunt'; export interface ChangerStatutAnomalieResponsableRequest { statut: StatutAnomalie; 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 = { DETECTEE: ['EN_COURS_TRAITEMENT'], EN_COURS_TRAITEMENT: ['RESOLUE'], RESOLUE: ['CLOTUREE'], CLOTUREE: [], }; function verifierResponsable(roleCode: string): void { if (roleCode !== 'RESPONSABLE') { throw new AppError(403, 'Acces reserve au responsable materiel'); } } export async function getDashboardResponsable( utilisateurId: number, roleCode: string, campusId: number, ): Promise { verifierResponsable(roleCode); const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] = await Promise.all( [ countMaterielsParStatut(campusId), countEmpruntsParStatut(campusId), countAnomaliesParStatut(campusId), countNotificationsNonLues(utilisateurId), findActiviteRecente(campusId, 10), ], ); return { materiels, emprunts, anomalies, notificationsNonLues, activiteRecente, }; } export function parseStatutEmprunt(value: unknown): StatutEmprunt | undefined { if (value === undefined) { return undefined; } if (typeof value !== 'string' || !(STATUT_EMPRUNT as readonly string[]).includes(value)) { throw new AppError(400, 'statut invalide'); } return value as StatutEmprunt; } export function parseAnneeEmprunt(value: unknown): number | undefined { if (value === undefined) { return undefined; } if (typeof value !== 'string') { throw new AppError(400, 'annee invalide'); } const annee = Number(value); if (!Number.isInteger(annee) || annee < 2000 || annee > 2100) { throw new AppError(400, 'annee invalide'); } return annee; } export function parseTriEmprunt(value: unknown): TriEmpruntResponsable | undefined { if (value === undefined) { return undefined; } const valeurs: readonly TriEmpruntResponsable[] = [ 'dateEmprunt', 'dateRetourPrevue', 'dateRetourReelle', 'materiel', 'etudiant', 'statut', ]; if (typeof value !== 'string' || !valeurs.includes(value as TriEmpruntResponsable)) { throw new AppError(400, 'tri invalide'); } return value as TriEmpruntResponsable; } export function parseOrdreTri(value: unknown): OrdreTri | undefined { if (value === undefined) { return undefined; } if (value !== 'asc' && value !== 'desc') { throw new AppError(400, 'ordre invalide'); } return value; } export function parseStatutMateriel(value: unknown): StatutMateriel | undefined { if (value === undefined) { return undefined; } if (typeof value !== 'string' || !(STATUT_MATERIEL as readonly string[]).includes(value)) { throw new AppError(400, 'statut invalide'); } return value as StatutMateriel; } export function parseTriMateriel(value: unknown): TriMaterielResponsable | undefined { if (value === undefined) { return undefined; } const valeurs: readonly TriMaterielResponsable[] = [ 'nom', 'reference', 'categorie', 'marque', 'etatGeneral', 'statut', ]; if (typeof value !== 'string' || !valeurs.includes(value as TriMaterielResponsable)) { throw new AppError(400, 'tri invalide'); } return value as TriMaterielResponsable; } export function parseStatutAnomalie(value: unknown): StatutAnomalie | undefined { if (value === undefined) { return undefined; } if (typeof value !== 'string' || !(STATUT_ANOMALIE as readonly string[]).includes(value)) { throw new AppError(400, 'statut invalide'); } return value as StatutAnomalie; } export function parseTriAnomalie(value: unknown): TriAnomalieResponsable | undefined { if (value === undefined) { return undefined; } const valeurs: readonly TriAnomalieResponsable[] = [ 'dateDetection', 'type', 'materiel', 'etudiant', 'statut', ]; if (typeof value !== 'string' || !valeurs.includes(value as TriAnomalieResponsable)) { throw new AppError(400, 'tri invalide'); } return value as TriAnomalieResponsable; } export function parseCategorieId(value: unknown): number | undefined { if (value === undefined) { return undefined; } const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) { throw new AppError(400, 'categorieId invalide'); } return parsed; } export function parseNotificationId(value: unknown): number { if (typeof value !== 'string') { throw new AppError(400, 'notificationId invalide'); } const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) { throw new AppError(400, 'notificationId invalide'); } return parsed; } export function parseAnomalieId(value: unknown): number { if (typeof value !== 'string') { throw new AppError(400, 'anomalieId invalide'); } const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) { throw new AppError(400, 'anomalieId invalide'); } 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; } if (typeof value !== 'string') { throw new AppError(400, `${champ} invalide`); } const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) { throw new AppError(400, `${champ} invalide`); } return parsed; } export function parseDateQuery(value: unknown, champ: string): Date | undefined { if (value === undefined) { return undefined; } if (typeof value !== 'string') { throw new AppError(400, `${champ} invalide`); } const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { throw new AppError(400, `${champ} invalide`); } return parsed; } export function parseTriHistorique(value: unknown): TriHistoriqueResponsable | undefined { if (value === undefined) { return undefined; } const valeurs: readonly TriHistoriqueResponsable[] = [ 'dateAction', 'action', 'utilisateur', 'materiel', 'emprunt', ]; if (typeof value !== 'string' || !valeurs.includes(value as TriHistoriqueResponsable)) { throw new AppError(400, 'tri invalide'); } return value as TriHistoriqueResponsable; } export function parseLu(value: unknown): boolean | undefined { if (value === undefined) { return undefined; } if (value === 'true') { return true; } if (value === 'false') { return false; } throw new AppError(400, 'lu invalide'); } export function listerEmpruntsResponsable( roleCode: string, campusId: number, filtres: ListerEmpruntsResponsableFiltres, ): Promise { verifierResponsable(roleCode); return findEmpruntsResponsable(campusId, filtres); } export function listerEcartsResponsable( roleCode: string, campusId: number, ): Promise { 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 { 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, filtres: ListerMaterielsResponsableFiltres, ): Promise { verifierResponsable(roleCode); return findMaterielsResponsable(campusId, filtres); } export function listerAnomaliesResponsable( roleCode: string, campusId: number, filtres: ListerAnomaliesResponsableFiltres, ): Promise { verifierResponsable(roleCode); return findAnomaliesResponsable(campusId, filtres); } export function listerNotificationsResponsable( utilisateurId: number, roleCode: string, filtres: ListerNotificationsResponsableFiltres, ): Promise { verifierResponsable(roleCode); return findNotificationsResponsable(utilisateurId, filtres); } export async function marquerNotificationResponsableLue( utilisateurId: number, roleCode: string, notificationId: number, ): Promise { verifierResponsable(roleCode); try { return await marquerNotificationLue(utilisateurId, notificationId); } catch { throw new AppError(404, 'Notification introuvable'); } } export function marquerToutesNotificationsResponsableLues( utilisateurId: number, roleCode: string, ): Promise { verifierResponsable(roleCode); return marquerNotificationsResponsableLues(utilisateurId); } export function listerHistoriqueResponsable( roleCode: string, campusId: number, filtres: ListerHistoriqueResponsableFiltres, ): Promise { verifierResponsable(roleCode); return findHistoriqueResponsable(campusId, filtres); } export async function changerStatutAnomalieResponsableService( responsableId: number, roleCode: string, campusId: number, anomalieId: number, data: ChangerStatutAnomalieResponsableRequest, ): Promise { verifierResponsable(roleCode); const anomalie = await findAnomalieResponsableById(campusId, anomalieId); if (!anomalie) { throw new AppError(404, 'Anomalie introuvable'); } const statutActuel = anomalie.statut as StatutAnomalie; if (!TRANSITIONS_ANOMALIE[statutActuel]?.includes(data.statut)) { throw new AppError(400, `Transition invalide depuis ${anomalie.statut} vers ${data.statut}`); } return changerStatutAnomalieResponsable({ anomalieId, campusId, responsableId, statut: data.statut, observation: data.observation, }); }