feat(backend): add responsable anomalies endpoint
This commit is contained in:
@@ -2,13 +2,16 @@ import { Request, Response } from 'express';
|
|||||||
import { AppError } from '../errors/app-error';
|
import { AppError } from '../errors/app-error';
|
||||||
import {
|
import {
|
||||||
getDashboardResponsable,
|
getDashboardResponsable,
|
||||||
|
listerAnomaliesResponsable,
|
||||||
listerMaterielsResponsable,
|
listerMaterielsResponsable,
|
||||||
listerEmpruntsResponsable,
|
listerEmpruntsResponsable,
|
||||||
parseCategorieId,
|
parseCategorieId,
|
||||||
|
parseStatutAnomalie,
|
||||||
parseStatutMateriel,
|
parseStatutMateriel,
|
||||||
parseStatutEmprunt,
|
parseStatutEmprunt,
|
||||||
} from '../services/responsable.service';
|
} from '../services/responsable.service';
|
||||||
import {
|
import {
|
||||||
|
toAnomalieResponsableResponse,
|
||||||
toDashboardResponsableResponse,
|
toDashboardResponsableResponse,
|
||||||
toEmpruntResponsableResponse,
|
toEmpruntResponsableResponse,
|
||||||
toMaterielResponsableResponse,
|
toMaterielResponsableResponse,
|
||||||
@@ -52,3 +55,19 @@ export async function getMateriels(req: Request, res: Response): Promise<void> {
|
|||||||
});
|
});
|
||||||
res.json({ data: materiels.map(toMaterielResponsableResponse) });
|
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) });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ActiviteResponsable,
|
ActiviteResponsable,
|
||||||
|
AnomalieResponsable,
|
||||||
EmpruntResponsable,
|
EmpruntResponsable,
|
||||||
MaterielResponsable,
|
MaterielResponsable,
|
||||||
StatutCount,
|
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 {
|
function toRepartition(rows: StatutCount[]): RepartitionStatutResponse {
|
||||||
const parStatut: Record<string, number> = {};
|
const parStatut: Record<string, number> = {};
|
||||||
let total = 0;
|
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 {
|
export interface EmpruntResponsableFiltres {
|
||||||
statut?: string;
|
statut?: string;
|
||||||
}
|
}
|
||||||
@@ -38,6 +47,11 @@ export interface MaterielResponsableFiltres {
|
|||||||
recherche?: string;
|
recherche?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AnomalieResponsableFiltres {
|
||||||
|
statut?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] {
|
function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] {
|
||||||
return rows.map((row) => ({ statut: row.statut, count: row._count._all }));
|
return rows.map((row) => ({ statut: row.statut, count: row._count._all }));
|
||||||
}
|
}
|
||||||
@@ -139,3 +153,23 @@ export function findMaterielsResponsable(
|
|||||||
orderBy: { nom: 'asc' },
|
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' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Router } from 'express';
|
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();
|
export const responsableRoutes: Router = Router();
|
||||||
|
|
||||||
responsableRoutes.get('/dashboard', getDashboard);
|
responsableRoutes.get('/dashboard', getDashboard);
|
||||||
responsableRoutes.get('/emprunts', getEmprunts);
|
responsableRoutes.get('/emprunts', getEmprunts);
|
||||||
responsableRoutes.get('/materiels', getMateriels);
|
responsableRoutes.get('/materiels', getMateriels);
|
||||||
|
responsableRoutes.get('/anomalies', getAnomalies);
|
||||||
|
|||||||
@@ -5,14 +5,23 @@ import {
|
|||||||
countMaterielsParStatut,
|
countMaterielsParStatut,
|
||||||
countNotificationsNonLues,
|
countNotificationsNonLues,
|
||||||
findActiviteRecente,
|
findActiviteRecente,
|
||||||
|
findAnomaliesResponsable,
|
||||||
findEmpruntsResponsable,
|
findEmpruntsResponsable,
|
||||||
findMaterielsResponsable,
|
findMaterielsResponsable,
|
||||||
StatutCount,
|
StatutCount,
|
||||||
ActiviteResponsable,
|
ActiviteResponsable,
|
||||||
|
AnomalieResponsable,
|
||||||
EmpruntResponsable,
|
EmpruntResponsable,
|
||||||
MaterielResponsable,
|
MaterielResponsable,
|
||||||
} from '../repositories/responsable.repository';
|
} 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 {
|
export interface DashboardResponsableData {
|
||||||
materiels: StatutCount[];
|
materiels: StatutCount[];
|
||||||
@@ -32,6 +41,11 @@ export interface ListerMaterielsResponsableFiltres {
|
|||||||
recherche?: string;
|
recherche?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ListerAnomaliesResponsableFiltres {
|
||||||
|
statut?: StatutAnomalie;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
function verifierResponsable(roleCode: string): void {
|
function verifierResponsable(roleCode: string): void {
|
||||||
if (roleCode !== 'RESPONSABLE') {
|
if (roleCode !== 'RESPONSABLE') {
|
||||||
throw new AppError(403, 'Acces reserve au responsable materiel');
|
throw new AppError(403, 'Acces reserve au responsable materiel');
|
||||||
@@ -45,14 +59,15 @@ export async function getDashboardResponsable(
|
|||||||
): Promise<DashboardResponsableData> {
|
): Promise<DashboardResponsableData> {
|
||||||
verifierResponsable(roleCode);
|
verifierResponsable(roleCode);
|
||||||
|
|
||||||
const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] =
|
const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] = await Promise.all(
|
||||||
await Promise.all([
|
[
|
||||||
countMaterielsParStatut(campusId),
|
countMaterielsParStatut(campusId),
|
||||||
countEmpruntsParStatut(campusId),
|
countEmpruntsParStatut(campusId),
|
||||||
countAnomaliesParStatut(campusId),
|
countAnomaliesParStatut(campusId),
|
||||||
countNotificationsNonLues(utilisateurId),
|
countNotificationsNonLues(utilisateurId),
|
||||||
findActiviteRecente(campusId, 10),
|
findActiviteRecente(campusId, 10),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
materiels,
|
materiels,
|
||||||
@@ -83,6 +98,16 @@ export function parseStatutMateriel(value: unknown): StatutMateriel | undefined
|
|||||||
return value as StatutMateriel;
|
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 {
|
export function parseCategorieId(value: unknown): number | undefined {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -113,3 +138,12 @@ export function listerMaterielsResponsable(
|
|||||||
verifierResponsable(roleCode);
|
verifierResponsable(roleCode);
|
||||||
return findMaterielsResponsable(campusId, filtres);
|
return findMaterielsResponsable(campusId, filtres);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listerAnomaliesResponsable(
|
||||||
|
roleCode: string,
|
||||||
|
campusId: number,
|
||||||
|
filtres: ListerAnomaliesResponsableFiltres,
|
||||||
|
): Promise<AnomalieResponsable[]> {
|
||||||
|
verifierResponsable(roleCode);
|
||||||
|
return findAnomaliesResponsable(campusId, filtres);
|
||||||
|
}
|
||||||
|
|||||||
@@ -349,6 +349,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
- Réponse enrichie avec catégorie et accessoires attendus.
|
- 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.
|
- 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.*
|
||||||
|
|||||||
Reference in New Issue
Block a user