Files
EME_APP/eme-backend/src/services/responsable.service.ts
T
2026-07-24 15:47:48 +02:00

587 lines
15 KiB
TypeScript

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<StatutAnomalie, StatutAnomalie[]> = {
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<DashboardResponsableData> {
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<EmpruntResponsable[]> {
verifierResponsable(roleCode);
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,
filtres: ListerMaterielsResponsableFiltres,
): Promise<MaterielResponsable[]> {
verifierResponsable(roleCode);
return findMaterielsResponsable(campusId, filtres);
}
export function listerAnomaliesResponsable(
roleCode: string,
campusId: number,
filtres: ListerAnomaliesResponsableFiltres,
): Promise<AnomalieResponsable[]> {
verifierResponsable(roleCode);
return findAnomaliesResponsable(campusId, filtres);
}
export function listerNotificationsResponsable(
utilisateurId: number,
roleCode: string,
filtres: ListerNotificationsResponsableFiltres,
): Promise<NotificationResponsable[]> {
verifierResponsable(roleCode);
return findNotificationsResponsable(utilisateurId, filtres);
}
export async function marquerNotificationResponsableLue(
utilisateurId: number,
roleCode: string,
notificationId: number,
): Promise<NotificationResponsable> {
verifierResponsable(roleCode);
try {
return await marquerNotificationLue(utilisateurId, notificationId);
} catch {
throw new AppError(404, 'Notification introuvable');
}
}
export function marquerToutesNotificationsResponsableLues(
utilisateurId: number,
roleCode: string,
): Promise<number> {
verifierResponsable(roleCode);
return marquerNotificationsResponsableLues(utilisateurId);
}
export function listerHistoriqueResponsable(
roleCode: string,
campusId: number,
filtres: ListerHistoriqueResponsableFiltres,
): Promise<HistoriqueResponsable[]> {
verifierResponsable(roleCode);
return findHistoriqueResponsable(campusId, filtres);
}
export async function changerStatutAnomalieResponsableService(
responsableId: number,
roleCode: string,
campusId: number,
anomalieId: number,
data: ChangerStatutAnomalieResponsableRequest,
): Promise<AnomalieResponsable> {
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,
});
}