feat(backend): add responsable anomalies endpoint

This commit is contained in:
SaidSoighiri94
2026-07-15 15:30:24 +02:00
parent 26f036046d
commit 1ce023de71
6 changed files with 194 additions and 6 deletions
@@ -2,13 +2,16 @@ import { Request, Response } from 'express';
import { AppError } from '../errors/app-error';
import {
getDashboardResponsable,
listerAnomaliesResponsable,
listerMaterielsResponsable,
listerEmpruntsResponsable,
parseCategorieId,
parseStatutAnomalie,
parseStatutMateriel,
parseStatutEmprunt,
} from '../services/responsable.service';
import {
toAnomalieResponsableResponse,
toDashboardResponsableResponse,
toEmpruntResponsableResponse,
toMaterielResponsableResponse,
@@ -52,3 +55,19 @@ export async function getMateriels(req: Request, res: Response): Promise<void> {
});
res.json({ data: materiels.map(toMaterielResponsableResponse) });
}
export async function getAnomalies(req: Request, res: Response): Promise<void> {
const user = req.user;
if (!user) {
throw new AppError(401, 'Authentification requise');
}
const statut = parseStatutAnomalie(req.query.statut);
const type = typeof req.query.type === 'string' ? req.query.type : undefined;
const anomalies = await listerAnomaliesResponsable(user.roleCode, user.campusId, {
statut,
type,
});
res.json({ data: anomalies.map(toAnomalieResponsableResponse) });
}
+87
View File
@@ -1,5 +1,6 @@
import {
ActiviteResponsable,
AnomalieResponsable,
EmpruntResponsable,
MaterielResponsable,
StatutCount,
@@ -84,6 +85,46 @@ export interface MaterielResponsableResponse {
}>;
}
export interface AnomalieResponsableResponse {
id: number;
type: string;
description: string;
statut: string;
detecteeAutomatiquement: boolean;
observation: string | null;
dateDetection: Date;
dateResolution: Date | null;
emprunt: {
id: number;
statut: string;
dateEmprunt: Date;
dateRetourPrevue: Date;
dateRetourReelle: Date | null;
};
etudiant: {
id: number;
nom: string;
prenom: string;
email: string;
classe: string | null;
};
materiel: {
id: number;
nom: string;
reference: string;
statut: string;
categorie: {
id: number;
nom: string;
};
};
traitePar: {
id: number;
nom: string;
prenom: string;
} | null;
}
function toRepartition(rows: StatutCount[]): RepartitionStatutResponse {
const parStatut: Record<string, number> = {};
let total = 0;
@@ -184,3 +225,49 @@ export function toMaterielResponsableResponse(
})),
};
}
export function toAnomalieResponsableResponse(
anomalie: AnomalieResponsable,
): AnomalieResponsableResponse {
return {
id: anomalie.id,
type: anomalie.type,
description: anomalie.description,
statut: anomalie.statut,
detecteeAutomatiquement: anomalie.detecteeAutomatiquement,
observation: anomalie.observation,
dateDetection: anomalie.dateDetection,
dateResolution: anomalie.dateResolution,
emprunt: {
id: anomalie.emprunt.id,
statut: anomalie.emprunt.statut,
dateEmprunt: anomalie.emprunt.dateEmprunt,
dateRetourPrevue: anomalie.emprunt.dateRetourPrevue,
dateRetourReelle: anomalie.emprunt.dateRetourReelle,
},
etudiant: {
id: anomalie.etudiant.id,
nom: anomalie.etudiant.nom,
prenom: anomalie.etudiant.prenom,
email: anomalie.etudiant.email,
classe: anomalie.etudiant.classe,
},
materiel: {
id: anomalie.materiel.id,
nom: anomalie.materiel.nom,
reference: anomalie.materiel.reference,
statut: anomalie.materiel.statut,
categorie: {
id: anomalie.materiel.categorie.id,
nom: anomalie.materiel.categorie.nom,
},
},
traitePar: anomalie.traitePar
? {
id: anomalie.traitePar.id,
nom: anomalie.traitePar.nom,
prenom: anomalie.traitePar.prenom,
}
: null,
};
}
@@ -28,6 +28,15 @@ export type MaterielResponsable = Prisma.MaterielGetPayload<{
};
}>;
export type AnomalieResponsable = Prisma.AnomalieGetPayload<{
include: {
emprunt: true;
etudiant: true;
materiel: { include: { categorie: true } };
traitePar: true;
};
}>;
export interface EmpruntResponsableFiltres {
statut?: string;
}
@@ -38,6 +47,11 @@ export interface MaterielResponsableFiltres {
recherche?: string;
}
export interface AnomalieResponsableFiltres {
statut?: string;
type?: string;
}
function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] {
return rows.map((row) => ({ statut: row.statut, count: row._count._all }));
}
@@ -139,3 +153,23 @@ export function findMaterielsResponsable(
orderBy: { nom: 'asc' },
});
}
export function findAnomaliesResponsable(
campusId: number,
filtres: AnomalieResponsableFiltres,
): Promise<AnomalieResponsable[]> {
return prisma.anomalie.findMany({
where: {
emprunt: { campusId },
...(filtres.statut ? { statut: filtres.statut } : {}),
...(filtres.type ? { type: filtres.type } : {}),
},
include: {
emprunt: true,
etudiant: true,
materiel: { include: { categorie: true } },
traitePar: true,
},
orderBy: { dateDetection: 'desc' },
});
}
+7 -1
View File
@@ -1,8 +1,14 @@
import { Router } from 'express';
import { getDashboard, getEmprunts, getMateriels } from '../controllers/responsable.controller';
import {
getAnomalies,
getDashboard,
getEmprunts,
getMateriels,
} from '../controllers/responsable.controller';
export const responsableRoutes: Router = Router();
responsableRoutes.get('/dashboard', getDashboard);
responsableRoutes.get('/emprunts', getEmprunts);
responsableRoutes.get('/materiels', getMateriels);
responsableRoutes.get('/anomalies', getAnomalies);
@@ -5,14 +5,23 @@ import {
countMaterielsParStatut,
countNotificationsNonLues,
findActiviteRecente,
findAnomaliesResponsable,
findEmpruntsResponsable,
findMaterielsResponsable,
StatutCount,
ActiviteResponsable,
AnomalieResponsable,
EmpruntResponsable,
MaterielResponsable,
} from '../repositories/responsable.repository';
import { STATUT_EMPRUNT, STATUT_MATERIEL, StatutEmprunt, StatutMateriel } from '../models/enums';
import {
STATUT_ANOMALIE,
STATUT_EMPRUNT,
STATUT_MATERIEL,
StatutAnomalie,
StatutEmprunt,
StatutMateriel,
} from '../models/enums';
export interface DashboardResponsableData {
materiels: StatutCount[];
@@ -32,6 +41,11 @@ export interface ListerMaterielsResponsableFiltres {
recherche?: string;
}
export interface ListerAnomaliesResponsableFiltres {
statut?: StatutAnomalie;
type?: string;
}
function verifierResponsable(roleCode: string): void {
if (roleCode !== 'RESPONSABLE') {
throw new AppError(403, 'Acces reserve au responsable materiel');
@@ -45,14 +59,15 @@ export async function getDashboardResponsable(
): Promise<DashboardResponsableData> {
verifierResponsable(roleCode);
const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] =
await Promise.all([
const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] = await Promise.all(
[
countMaterielsParStatut(campusId),
countEmpruntsParStatut(campusId),
countAnomaliesParStatut(campusId),
countNotificationsNonLues(utilisateurId),
findActiviteRecente(campusId, 10),
]);
],
);
return {
materiels,
@@ -83,6 +98,16 @@ export function parseStatutMateriel(value: unknown): StatutMateriel | undefined
return value as StatutMateriel;
}
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 parseCategorieId(value: unknown): number | undefined {
if (value === undefined) {
return undefined;
@@ -113,3 +138,12 @@ export function listerMaterielsResponsable(
verifierResponsable(roleCode);
return findMaterielsResponsable(campusId, filtres);
}
export function listerAnomaliesResponsable(
roleCode: string,
campusId: number,
filtres: ListerAnomaliesResponsableFiltres,
): Promise<AnomalieResponsable[]> {
verifierResponsable(roleCode);
return findAnomaliesResponsable(campusId, filtres);
}
+9 -1
View File
@@ -349,6 +349,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
- Réponse enrichie avec catégorie et accessoires attendus.
- Vérifié au runtime : Karim obtient tout le stock du campus, `statut=DISPONIBLE` et `q=MacBook` filtrent correctement, statut invalide renvoie 400, Lucas obtient 403.
### Étape 37 — API responsable : consultation des anomalies
- Branche dédiée `feat/responsable-anomalies-api` créée après merge de la consultation du stock responsable dans `develop`.
- Endpoint `GET /api/responsable/anomalies` ajouté, protégé par rôle `RESPONSABLE`.
- Filtrage par campus du responsable appliqué systématiquement via l'emprunt lié à l'anomalie (RG27).
- Filtres optionnels ajoutés : `statut` (`DETECTEE`, `EN_COURS_TRAITEMENT`, `RESOLUE`, `CLOTUREE`) et `type`.
- Réponse enrichie avec anomalie, emprunt, étudiant concerné, matériel, catégorie et responsable de traitement si renseigné.
- Limite volontaire : consultation uniquement ; le cycle de traitement/résolution reste à développer.
---
*Dernière mise à jour : 2026-07-15 — Dashboard, consultation des emprunts et consultation du stock responsable ajoutés et testés.*
*Dernière mise à jour : 2026-07-15 — Dashboard, consultation des emprunts, du stock et des anomalies responsable ajoutés côté API.*