merge: responsable emprunts consultation api

This commit is contained in:
SaidSoighiri94
2026-07-15 12:25:47 +02:00
6 changed files with 151 additions and 8 deletions
@@ -1,7 +1,14 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { AppError } from '../errors/app-error'; import { AppError } from '../errors/app-error';
import { getDashboardResponsable } from '../services/responsable.service'; import {
import { toDashboardResponsableResponse } from '../dtos/responsable.dto'; getDashboardResponsable,
listerEmpruntsResponsable,
parseStatutEmprunt,
} from '../services/responsable.service';
import {
toDashboardResponsableResponse,
toEmpruntResponsableResponse,
} from '../dtos/responsable.dto';
export async function getDashboard(req: Request, res: Response): Promise<void> { export async function getDashboard(req: Request, res: Response): Promise<void> {
const user = req.user; const user = req.user;
@@ -12,3 +19,14 @@ export async function getDashboard(req: Request, res: Response): Promise<void> {
const dashboard = await getDashboardResponsable(user.id, user.roleCode, user.campusId); const dashboard = await getDashboardResponsable(user.id, user.roleCode, user.campusId);
res.json({ data: toDashboardResponsableResponse(dashboard) }); res.json({ data: toDashboardResponsableResponse(dashboard) });
} }
export async function getEmprunts(req: Request, res: Response): Promise<void> {
const user = req.user;
if (!user) {
throw new AppError(401, 'Authentification requise');
}
const statut = parseStatutEmprunt(req.query.statut);
const emprunts = await listerEmpruntsResponsable(user.roleCode, user.campusId, { statut });
res.json({ data: emprunts.map(toEmpruntResponsableResponse) });
}
+59 -1
View File
@@ -1,4 +1,8 @@
import { ActiviteResponsable, StatutCount } from '../repositories/responsable.repository'; import {
ActiviteResponsable,
EmpruntResponsable,
StatutCount,
} from '../repositories/responsable.repository';
import { DashboardResponsableData } from '../services/responsable.service'; import { DashboardResponsableData } from '../services/responsable.service';
export interface RepartitionStatutResponse { export interface RepartitionStatutResponse {
@@ -34,6 +38,31 @@ export interface DashboardResponsableResponse {
activiteRecente: ActiviteResponsableResponse[]; activiteRecente: ActiviteResponsableResponse[];
} }
export interface EmpruntResponsableResponse {
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;
};
};
}
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;
@@ -81,3 +110,32 @@ export function toDashboardResponsableResponse(
activiteRecente: data.activiteRecente.map(toActiviteResponse), activiteRecente: data.activiteRecente.map(toActiviteResponse),
}; };
} }
export function toEmpruntResponsableResponse(
emprunt: EmpruntResponsable,
): EmpruntResponsableResponse {
return {
id: emprunt.id,
statut: emprunt.statut,
dateEmprunt: emprunt.dateEmprunt,
dateRetourPrevue: emprunt.dateRetourPrevue,
dateRetourReelle: emprunt.dateRetourReelle,
etudiant: {
id: emprunt.utilisateur.id,
nom: emprunt.utilisateur.nom,
prenom: emprunt.utilisateur.prenom,
email: emprunt.utilisateur.email,
classe: emprunt.utilisateur.classe,
},
materiel: {
id: emprunt.materiel.id,
nom: emprunt.materiel.nom,
reference: emprunt.materiel.reference,
statut: emprunt.materiel.statut,
categorie: {
id: emprunt.materiel.categorie.id,
nom: emprunt.materiel.categorie.nom,
},
},
};
}
@@ -14,6 +14,17 @@ export type ActiviteResponsable = Prisma.HistoriqueGetPayload<{
}; };
}>; }>;
export type EmpruntResponsable = Prisma.EmpruntGetPayload<{
include: {
utilisateur: true;
materiel: { include: { categorie: true } };
};
}>;
export interface EmpruntResponsableFiltres {
statut?: 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 }));
} }
@@ -69,3 +80,20 @@ export function findActiviteRecente(
take: limite, take: limite,
}); });
} }
export function findEmpruntsResponsable(
campusId: number,
filtres: EmpruntResponsableFiltres,
): Promise<EmpruntResponsable[]> {
return prisma.emprunt.findMany({
where: {
campusId,
...(filtres.statut ? { statut: filtres.statut } : {}),
},
include: {
utilisateur: true,
materiel: { include: { categorie: true } },
},
orderBy: { dateEmprunt: 'desc' },
});
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { Router } from 'express'; import { Router } from 'express';
import { getDashboard } from '../controllers/responsable.controller'; import { getDashboard, getEmprunts } 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);
@@ -5,9 +5,12 @@ import {
countMaterielsParStatut, countMaterielsParStatut,
countNotificationsNonLues, countNotificationsNonLues,
findActiviteRecente, findActiviteRecente,
findEmpruntsResponsable,
StatutCount, StatutCount,
ActiviteResponsable, ActiviteResponsable,
EmpruntResponsable,
} from '../repositories/responsable.repository'; } from '../repositories/responsable.repository';
import { STATUT_EMPRUNT, StatutEmprunt } from '../models/enums';
export interface DashboardResponsableData { export interface DashboardResponsableData {
materiels: StatutCount[]; materiels: StatutCount[];
@@ -17,14 +20,22 @@ export interface DashboardResponsableData {
activiteRecente: ActiviteResponsable[]; activiteRecente: ActiviteResponsable[];
} }
export interface ListerEmpruntsResponsableFiltres {
statut?: StatutEmprunt;
}
function verifierResponsable(roleCode: string): void {
if (roleCode !== 'RESPONSABLE') {
throw new AppError(403, 'Acces reserve au responsable materiel');
}
}
export async function getDashboardResponsable( export async function getDashboardResponsable(
utilisateurId: number, utilisateurId: number,
roleCode: string, roleCode: string,
campusId: number, campusId: number,
): Promise<DashboardResponsableData> { ): Promise<DashboardResponsableData> {
if (roleCode !== 'RESPONSABLE') { verifierResponsable(roleCode);
throw new AppError(403, 'Acces reserve au responsable materiel');
}
const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] = const [materiels, emprunts, anomalies, notificationsNonLues, activiteRecente] =
await Promise.all([ await Promise.all([
@@ -43,3 +54,22 @@ export async function getDashboardResponsable(
activiteRecente, 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 listerEmpruntsResponsable(
roleCode: string,
campusId: number,
filtres: ListerEmpruntsResponsableFiltres,
): Promise<EmpruntResponsable[]> {
verifierResponsable(roleCode);
return findEmpruntsResponsable(campusId, filtres);
}
+9 -1
View File
@@ -332,6 +332,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
- Activité récente exposée depuis l'historique, avec utilisateur, matériel et emprunt associé. - Activité récente exposée depuis l'historique, avec utilisateur, matériel et emprunt associé.
- Vérifié au runtime : Karim (`RESPONSABLE`) obtient 200 avec KPIs ; Lucas (`ETUDIANT`) obtient 403. - Vérifié au runtime : Karim (`RESPONSABLE`) obtient 200 avec KPIs ; Lucas (`ETUDIANT`) obtient 403.
### Étape 35 — API responsable : consultation des emprunts
- Branche dédiée `feat/responsable-emprunts-api` créée après merge du dashboard responsable dans `develop`.
- Endpoint `GET /api/responsable/emprunts` ajouté, protégé par rôle `RESPONSABLE`.
- Filtrage par campus du responsable appliqué systématiquement (RG27).
- Filtre optionnel `statut` ajouté pour RG28 : `EN_COURS`, `EN_RETARD`, `CLOTURE`, `RETOUR_NON_CONFORME`, `ANNULE`.
- Réponse enrichie avec l'étudiant, le matériel, la catégorie, les dates et le statut.
- Vérifié au runtime : Karim obtient les emprunts du campus, `statut=EN_COURS` filtre correctement, statut invalide renvoie 400, Lucas obtient 403.
--- ---
*Dernière mise à jour : 2026-07-15 — Premier endpoint responsable ajouté et testé.* *Dernière mise à jour : 2026-07-15 — Dashboard et consultation des emprunts responsable ajoutés et testés.*