merge: responsable anomalies status cycle
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AppError } from '../errors/app-error';
|
import { AppError } from '../errors/app-error';
|
||||||
import {
|
import {
|
||||||
|
changerStatutAnomalieResponsableService,
|
||||||
getDashboardResponsable,
|
getDashboardResponsable,
|
||||||
listerAnomaliesResponsable,
|
listerAnomaliesResponsable,
|
||||||
listerHistoriqueResponsable,
|
listerHistoriqueResponsable,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
listerEmpruntsResponsable,
|
listerEmpruntsResponsable,
|
||||||
marquerNotificationResponsableLue,
|
marquerNotificationResponsableLue,
|
||||||
marquerToutesNotificationsResponsableLues,
|
marquerToutesNotificationsResponsableLues,
|
||||||
|
parseAnomalieId,
|
||||||
parseCategorieId,
|
parseCategorieId,
|
||||||
parseLu,
|
parseLu,
|
||||||
parseNotificationId,
|
parseNotificationId,
|
||||||
@@ -27,6 +29,13 @@ import {
|
|||||||
toNotificationResponsableResponse,
|
toNotificationResponsableResponse,
|
||||||
} from '../dtos/responsable.dto';
|
} from '../dtos/responsable.dto';
|
||||||
|
|
||||||
|
function asBodyObject(value: unknown): Record<string, unknown> {
|
||||||
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||||
|
throw new AppError(400, 'Corps de requete invalide');
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
function getHistoriqueFiltres(req: Request) {
|
function getHistoriqueFiltres(req: Request) {
|
||||||
const action = typeof req.query.action === 'string' ? req.query.action : undefined;
|
const action = typeof req.query.action === 'string' ? req.query.action : undefined;
|
||||||
return {
|
return {
|
||||||
@@ -102,6 +111,30 @@ export async function getAnomalies(req: Request, res: Response): Promise<void> {
|
|||||||
res.json({ data: anomalies.map(toAnomalieResponsableResponse) });
|
res.json({ data: anomalies.map(toAnomalieResponsableResponse) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function patchAnomalieStatut(req: Request, res: Response): Promise<void> {
|
||||||
|
const user = req.user;
|
||||||
|
if (!user) {
|
||||||
|
throw new AppError(401, 'Authentification requise');
|
||||||
|
}
|
||||||
|
|
||||||
|
const anomalieId = parseAnomalieId(req.params.id);
|
||||||
|
const body = asBodyObject(req.body);
|
||||||
|
const statut = parseStatutAnomalie(body.statut);
|
||||||
|
if (!statut) {
|
||||||
|
throw new AppError(400, 'statut requis');
|
||||||
|
}
|
||||||
|
const observation = typeof body.observation === 'string' ? body.observation : undefined;
|
||||||
|
|
||||||
|
const anomalie = await changerStatutAnomalieResponsableService(
|
||||||
|
user.id,
|
||||||
|
user.roleCode,
|
||||||
|
user.campusId,
|
||||||
|
anomalieId,
|
||||||
|
{ statut, observation },
|
||||||
|
);
|
||||||
|
res.json({ data: toAnomalieResponsableResponse(anomalie) });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNotifications(req: Request, res: Response): Promise<void> {
|
export async function getNotifications(req: Request, res: Response): Promise<void> {
|
||||||
const user = req.user;
|
const user = req.user;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ export interface HistoriqueResponsableFiltres {
|
|||||||
dateFin?: Date;
|
dateFin?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChangerStatutAnomalieData {
|
||||||
|
anomalieId: number;
|
||||||
|
campusId: number;
|
||||||
|
responsableId: number;
|
||||||
|
statut: string;
|
||||||
|
observation?: 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 }));
|
||||||
}
|
}
|
||||||
@@ -208,6 +216,62 @@ export function findAnomaliesResponsable(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findAnomalieResponsableById(
|
||||||
|
campusId: number,
|
||||||
|
anomalieId: number,
|
||||||
|
): Promise<AnomalieResponsable | null> {
|
||||||
|
return prisma.anomalie.findFirst({
|
||||||
|
where: {
|
||||||
|
id: anomalieId,
|
||||||
|
emprunt: { campusId },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
emprunt: true,
|
||||||
|
etudiant: true,
|
||||||
|
materiel: { include: { categorie: true } },
|
||||||
|
traitePar: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changerStatutAnomalieResponsable(
|
||||||
|
data: ChangerStatutAnomalieData,
|
||||||
|
): Promise<AnomalieResponsable> {
|
||||||
|
return prisma.$transaction(async (tx) => {
|
||||||
|
const anomalie = await tx.anomalie.update({
|
||||||
|
where: { id: data.anomalieId },
|
||||||
|
data: {
|
||||||
|
statut: data.statut,
|
||||||
|
traiteeParId: data.responsableId,
|
||||||
|
...(data.observation !== undefined ? { observation: data.observation } : {}),
|
||||||
|
...(data.statut === 'RESOLUE' || data.statut === 'CLOTUREE'
|
||||||
|
? { dateResolution: new Date() }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
emprunt: true,
|
||||||
|
etudiant: true,
|
||||||
|
materiel: { include: { categorie: true } },
|
||||||
|
traitePar: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.historique.create({
|
||||||
|
data: {
|
||||||
|
utilisateurId: data.responsableId,
|
||||||
|
empruntId: anomalie.empruntId,
|
||||||
|
materielId: anomalie.materielId,
|
||||||
|
campusId: data.campusId,
|
||||||
|
action: 'TRAITEMENT_ANOMALIE',
|
||||||
|
description: `Anomalie #${anomalie.id} passee au statut ${data.statut}`,
|
||||||
|
dateAction: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return anomalie;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function findNotificationsResponsable(
|
export function findNotificationsResponsable(
|
||||||
utilisateurId: number,
|
utilisateurId: number,
|
||||||
filtres: NotificationResponsableFiltres,
|
filtres: NotificationResponsableFiltres,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getHistoriqueCsv,
|
getHistoriqueCsv,
|
||||||
getMateriels,
|
getMateriels,
|
||||||
getNotifications,
|
getNotifications,
|
||||||
|
patchAnomalieStatut,
|
||||||
patchNotificationLue,
|
patchNotificationLue,
|
||||||
patchNotificationsLues,
|
patchNotificationsLues,
|
||||||
} from '../controllers/responsable.controller';
|
} from '../controllers/responsable.controller';
|
||||||
@@ -17,6 +18,7 @@ responsableRoutes.get('/dashboard', getDashboard);
|
|||||||
responsableRoutes.get('/emprunts', getEmprunts);
|
responsableRoutes.get('/emprunts', getEmprunts);
|
||||||
responsableRoutes.get('/materiels', getMateriels);
|
responsableRoutes.get('/materiels', getMateriels);
|
||||||
responsableRoutes.get('/anomalies', getAnomalies);
|
responsableRoutes.get('/anomalies', getAnomalies);
|
||||||
|
responsableRoutes.patch('/anomalies/:id/statut', patchAnomalieStatut);
|
||||||
responsableRoutes.get('/notifications', getNotifications);
|
responsableRoutes.get('/notifications', getNotifications);
|
||||||
responsableRoutes.patch('/notifications/lu-toutes', patchNotificationsLues);
|
responsableRoutes.patch('/notifications/lu-toutes', patchNotificationsLues);
|
||||||
responsableRoutes.patch('/notifications/:id/lu', patchNotificationLue);
|
responsableRoutes.patch('/notifications/:id/lu', patchNotificationLue);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { AppError } from '../errors/app-error';
|
import { AppError } from '../errors/app-error';
|
||||||
import {
|
import {
|
||||||
|
changerStatutAnomalieResponsable,
|
||||||
countAnomaliesParStatut,
|
countAnomaliesParStatut,
|
||||||
countEmpruntsParStatut,
|
countEmpruntsParStatut,
|
||||||
countMaterielsParStatut,
|
countMaterielsParStatut,
|
||||||
countNotificationsNonLues,
|
countNotificationsNonLues,
|
||||||
findActiviteRecente,
|
findActiviteRecente,
|
||||||
|
findAnomalieResponsableById,
|
||||||
findAnomaliesResponsable,
|
findAnomaliesResponsable,
|
||||||
findEmpruntsResponsable,
|
findEmpruntsResponsable,
|
||||||
findHistoriqueResponsable,
|
findHistoriqueResponsable,
|
||||||
@@ -65,6 +67,18 @@ export interface ListerHistoriqueResponsableFiltres {
|
|||||||
dateFin?: Date;
|
dateFin?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChangerStatutAnomalieResponsableRequest {
|
||||||
|
statut: StatutAnomalie;
|
||||||
|
observation?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRANSITIONS_ANOMALIE: Record<StatutAnomalie, StatutAnomalie[]> = {
|
||||||
|
DETECTEE: ['EN_COURS_TRAITEMENT'],
|
||||||
|
EN_COURS_TRAITEMENT: ['RESOLUE'],
|
||||||
|
RESOLUE: ['CLOTUREE'],
|
||||||
|
CLOTUREE: [],
|
||||||
|
};
|
||||||
|
|
||||||
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');
|
||||||
@@ -153,6 +167,19 @@ export function parseNotificationId(value: unknown): number {
|
|||||||
return parsed;
|
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 parsePositiveIntQuery(value: unknown, champ: string): number | undefined {
|
export function parsePositiveIntQuery(value: unknown, champ: string): number | undefined {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -264,3 +291,31 @@ export function listerHistoriqueResponsable(
|
|||||||
verifierResponsable(roleCode);
|
verifierResponsable(roleCode);
|
||||||
return findHistoriqueResponsable(campusId, filtres);
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -373,6 +373,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
- Endpoint `GET /api/responsable/historique/export.csv` ajouté avec les mêmes filtres.
|
- Endpoint `GET /api/responsable/historique/export.csv` ajouté avec les mêmes filtres.
|
||||||
- Export CSV généré côté API avec les colonnes principales : date, action, description, utilisateur, email, matériel, emprunt.
|
- Export CSV généré côté API avec les colonnes principales : date, action, description, utilisateur, email, matériel, emprunt.
|
||||||
|
|
||||||
|
### Étape 40 — API responsable : cycle de statut des anomalies
|
||||||
|
- Branche dédiée `feat/responsable-anomalies-cycle-api` créée après merge de l'historique responsable dans `develop`.
|
||||||
|
- Endpoint `PATCH /api/responsable/anomalies/:id/statut` ajouté, protégé par rôle `RESPONSABLE`.
|
||||||
|
- Filtrage par campus appliqué avant toute modification pour empêcher le traitement d'une anomalie hors campus (RG27).
|
||||||
|
- Transitions autorisées : `DETECTEE -> EN_COURS_TRAITEMENT -> RESOLUE -> CLOTUREE`.
|
||||||
|
- Le responsable connecté est enregistré dans `traiteeParId`; une observation optionnelle peut être conservée.
|
||||||
|
- Une entrée d'historique `TRAITEMENT_ANOMALIE` est créée à chaque transition.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Dernière mise à jour : 2026-07-16 — Bloc API responsable complété côté consultation, notifications et historique/export.*
|
*Dernière mise à jour : 2026-07-16 — Bloc API responsable complété côté consultation, notifications, historique/export et cycle anomalies.*
|
||||||
|
|||||||
Reference in New Issue
Block a user