feat(backend): add responsable stock endpoint
This commit is contained in:
@@ -2,12 +2,16 @@ import { Request, Response } from 'express';
|
|||||||
import { AppError } from '../errors/app-error';
|
import { AppError } from '../errors/app-error';
|
||||||
import {
|
import {
|
||||||
getDashboardResponsable,
|
getDashboardResponsable,
|
||||||
|
listerMaterielsResponsable,
|
||||||
listerEmpruntsResponsable,
|
listerEmpruntsResponsable,
|
||||||
|
parseCategorieId,
|
||||||
|
parseStatutMateriel,
|
||||||
parseStatutEmprunt,
|
parseStatutEmprunt,
|
||||||
} from '../services/responsable.service';
|
} from '../services/responsable.service';
|
||||||
import {
|
import {
|
||||||
toDashboardResponsableResponse,
|
toDashboardResponsableResponse,
|
||||||
toEmpruntResponsableResponse,
|
toEmpruntResponsableResponse,
|
||||||
|
toMaterielResponsableResponse,
|
||||||
} from '../dtos/responsable.dto';
|
} from '../dtos/responsable.dto';
|
||||||
|
|
||||||
export async function getDashboard(req: Request, res: Response): Promise<void> {
|
export async function getDashboard(req: Request, res: Response): Promise<void> {
|
||||||
@@ -30,3 +34,21 @@ export async function getEmprunts(req: Request, res: Response): Promise<void> {
|
|||||||
const emprunts = await listerEmpruntsResponsable(user.roleCode, user.campusId, { statut });
|
const emprunts = await listerEmpruntsResponsable(user.roleCode, user.campusId, { statut });
|
||||||
res.json({ data: emprunts.map(toEmpruntResponsableResponse) });
|
res.json({ data: emprunts.map(toEmpruntResponsableResponse) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMateriels(req: Request, res: Response): Promise<void> {
|
||||||
|
const user = req.user;
|
||||||
|
if (!user) {
|
||||||
|
throw new AppError(401, 'Authentification requise');
|
||||||
|
}
|
||||||
|
|
||||||
|
const statut = parseStatutMateriel(req.query.statut);
|
||||||
|
const categorieId = parseCategorieId(req.query.categorieId);
|
||||||
|
const recherche = typeof req.query.q === 'string' ? req.query.q : undefined;
|
||||||
|
|
||||||
|
const materiels = await listerMaterielsResponsable(user.roleCode, user.campusId, {
|
||||||
|
statut,
|
||||||
|
categorieId,
|
||||||
|
recherche,
|
||||||
|
});
|
||||||
|
res.json({ data: materiels.map(toMaterielResponsableResponse) });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ActiviteResponsable,
|
ActiviteResponsable,
|
||||||
EmpruntResponsable,
|
EmpruntResponsable,
|
||||||
|
MaterielResponsable,
|
||||||
StatutCount,
|
StatutCount,
|
||||||
} from '../repositories/responsable.repository';
|
} from '../repositories/responsable.repository';
|
||||||
import { DashboardResponsableData } from '../services/responsable.service';
|
import { DashboardResponsableData } from '../services/responsable.service';
|
||||||
@@ -63,6 +64,26 @@ export interface EmpruntResponsableResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MaterielResponsableResponse {
|
||||||
|
id: number;
|
||||||
|
nom: string;
|
||||||
|
marque: string;
|
||||||
|
modele: string;
|
||||||
|
reference: string;
|
||||||
|
statut: string;
|
||||||
|
etatGeneral: string;
|
||||||
|
categorie: {
|
||||||
|
id: number;
|
||||||
|
nom: string;
|
||||||
|
};
|
||||||
|
accessoires: Array<{
|
||||||
|
id: number;
|
||||||
|
nom: string;
|
||||||
|
quantiteAttendue: number;
|
||||||
|
obligatoire: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -139,3 +160,27 @@ export function toEmpruntResponsableResponse(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toMaterielResponsableResponse(
|
||||||
|
materiel: MaterielResponsable,
|
||||||
|
): MaterielResponsableResponse {
|
||||||
|
return {
|
||||||
|
id: materiel.id,
|
||||||
|
nom: materiel.nom,
|
||||||
|
marque: materiel.marque,
|
||||||
|
modele: materiel.modele,
|
||||||
|
reference: materiel.reference,
|
||||||
|
statut: materiel.statut,
|
||||||
|
etatGeneral: materiel.etatGeneral,
|
||||||
|
categorie: {
|
||||||
|
id: materiel.categorie.id,
|
||||||
|
nom: materiel.categorie.nom,
|
||||||
|
},
|
||||||
|
accessoires: materiel.accessoires.map((liaison) => ({
|
||||||
|
id: liaison.accessoire.id,
|
||||||
|
nom: liaison.accessoire.nom,
|
||||||
|
quantiteAttendue: liaison.quantiteAttendue,
|
||||||
|
obligatoire: liaison.obligatoire,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,10 +21,23 @@ export type EmpruntResponsable = Prisma.EmpruntGetPayload<{
|
|||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type MaterielResponsable = Prisma.MaterielGetPayload<{
|
||||||
|
include: {
|
||||||
|
categorie: true;
|
||||||
|
accessoires: { include: { accessoire: true } };
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
export interface EmpruntResponsableFiltres {
|
export interface EmpruntResponsableFiltres {
|
||||||
statut?: string;
|
statut?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MaterielResponsableFiltres {
|
||||||
|
statut?: string;
|
||||||
|
categorieId?: number;
|
||||||
|
recherche?: 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 }));
|
||||||
}
|
}
|
||||||
@@ -97,3 +110,32 @@ export function findEmpruntsResponsable(
|
|||||||
orderBy: { dateEmprunt: 'desc' },
|
orderBy: { dateEmprunt: 'desc' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findMaterielsResponsable(
|
||||||
|
campusId: number,
|
||||||
|
filtres: MaterielResponsableFiltres,
|
||||||
|
): Promise<MaterielResponsable[]> {
|
||||||
|
return prisma.materiel.findMany({
|
||||||
|
where: {
|
||||||
|
campusId,
|
||||||
|
actif: true,
|
||||||
|
...(filtres.statut ? { statut: filtres.statut } : {}),
|
||||||
|
...(filtres.categorieId !== undefined ? { categorieId: filtres.categorieId } : {}),
|
||||||
|
...(filtres.recherche
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{ nom: { contains: filtres.recherche } },
|
||||||
|
{ marque: { contains: filtres.recherche } },
|
||||||
|
{ modele: { contains: filtres.recherche } },
|
||||||
|
{ reference: { contains: filtres.recherche } },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
categorie: true,
|
||||||
|
accessoires: { include: { accessoire: true } },
|
||||||
|
},
|
||||||
|
orderBy: { nom: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { getDashboard, getEmprunts } from '../controllers/responsable.controller';
|
import { 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);
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import {
|
|||||||
countNotificationsNonLues,
|
countNotificationsNonLues,
|
||||||
findActiviteRecente,
|
findActiviteRecente,
|
||||||
findEmpruntsResponsable,
|
findEmpruntsResponsable,
|
||||||
|
findMaterielsResponsable,
|
||||||
StatutCount,
|
StatutCount,
|
||||||
ActiviteResponsable,
|
ActiviteResponsable,
|
||||||
EmpruntResponsable,
|
EmpruntResponsable,
|
||||||
|
MaterielResponsable,
|
||||||
} from '../repositories/responsable.repository';
|
} from '../repositories/responsable.repository';
|
||||||
import { STATUT_EMPRUNT, StatutEmprunt } from '../models/enums';
|
import { STATUT_EMPRUNT, STATUT_MATERIEL, StatutEmprunt, StatutMateriel } from '../models/enums';
|
||||||
|
|
||||||
export interface DashboardResponsableData {
|
export interface DashboardResponsableData {
|
||||||
materiels: StatutCount[];
|
materiels: StatutCount[];
|
||||||
@@ -24,6 +26,12 @@ export interface ListerEmpruntsResponsableFiltres {
|
|||||||
statut?: StatutEmprunt;
|
statut?: StatutEmprunt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ListerMaterielsResponsableFiltres {
|
||||||
|
statut?: StatutMateriel;
|
||||||
|
categorieId?: number;
|
||||||
|
recherche?: 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');
|
||||||
@@ -65,6 +73,29 @@ export function parseStatutEmprunt(value: unknown): StatutEmprunt | undefined {
|
|||||||
return value as StatutEmprunt;
|
return value as StatutEmprunt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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 listerEmpruntsResponsable(
|
export function listerEmpruntsResponsable(
|
||||||
roleCode: string,
|
roleCode: string,
|
||||||
campusId: number,
|
campusId: number,
|
||||||
@@ -73,3 +104,12 @@ export function listerEmpruntsResponsable(
|
|||||||
verifierResponsable(roleCode);
|
verifierResponsable(roleCode);
|
||||||
return findEmpruntsResponsable(campusId, filtres);
|
return findEmpruntsResponsable(campusId, filtres);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listerMaterielsResponsable(
|
||||||
|
roleCode: string,
|
||||||
|
campusId: number,
|
||||||
|
filtres: ListerMaterielsResponsableFiltres,
|
||||||
|
): Promise<MaterielResponsable[]> {
|
||||||
|
verifierResponsable(roleCode);
|
||||||
|
return findMaterielsResponsable(campusId, filtres);
|
||||||
|
}
|
||||||
|
|||||||
@@ -340,6 +340,15 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
- Réponse enrichie avec l'étudiant, le matériel, la catégorie, les dates et le statut.
|
- 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.
|
- Vérifié au runtime : Karim obtient les emprunts du campus, `statut=EN_COURS` filtre correctement, statut invalide renvoie 400, Lucas obtient 403.
|
||||||
|
|
||||||
|
### Étape 36 — API responsable : consultation du stock
|
||||||
|
- Branche dédiée `feat/responsable-stock-api` créée après merge de la consultation des emprunts responsable dans `develop`.
|
||||||
|
- Endpoint `GET /api/responsable/materiels` ajouté, protégé par rôle `RESPONSABLE`.
|
||||||
|
- Filtrage par campus du responsable appliqué systématiquement (RG27/RG29).
|
||||||
|
- Filtres optionnels ajoutés : `statut`, `categorieId`, `q` (nom, marque, modèle, référence).
|
||||||
|
- Contrairement au catalogue étudiant, cet endpoint retourne tout le stock actif du campus, quel que soit le statut.
|
||||||
|
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Dernière mise à jour : 2026-07-15 — Dashboard et consultation des emprunts responsable ajoutés et testés.*
|
*Dernière mise à jour : 2026-07-15 — Dashboard, consultation des emprunts et consultation du stock responsable ajoutés et testés.*
|
||||||
|
|||||||
Reference in New Issue
Block a user