merge: responsable notifications api

This commit is contained in:
SaidSoighiri94
2026-07-15 19:47:55 +02:00
6 changed files with 235 additions and 1 deletions
@@ -3,9 +3,14 @@ import { AppError } from '../errors/app-error';
import {
getDashboardResponsable,
listerAnomaliesResponsable,
listerNotificationsResponsable,
listerMaterielsResponsable,
listerEmpruntsResponsable,
marquerNotificationResponsableLue,
marquerToutesNotificationsResponsableLues,
parseCategorieId,
parseLu,
parseNotificationId,
parseStatutAnomalie,
parseStatutMateriel,
parseStatutEmprunt,
@@ -15,6 +20,7 @@ import {
toDashboardResponsableResponse,
toEmpruntResponsableResponse,
toMaterielResponsableResponse,
toNotificationResponsableResponse,
} from '../dtos/responsable.dto';
export async function getDashboard(req: Request, res: Response): Promise<void> {
@@ -71,3 +77,39 @@ export async function getAnomalies(req: Request, res: Response): Promise<void> {
});
res.json({ data: anomalies.map(toAnomalieResponsableResponse) });
}
export async function getNotifications(req: Request, res: Response): Promise<void> {
const user = req.user;
if (!user) {
throw new AppError(401, 'Authentification requise');
}
const lu = parseLu(req.query.lu);
const notifications = await listerNotificationsResponsable(user.id, user.roleCode, { lu });
res.json({ data: notifications.map(toNotificationResponsableResponse) });
}
export async function patchNotificationLue(req: Request, res: Response): Promise<void> {
const user = req.user;
if (!user) {
throw new AppError(401, 'Authentification requise');
}
const notificationId = parseNotificationId(req.params.id);
const notification = await marquerNotificationResponsableLue(
user.id,
user.roleCode,
notificationId,
);
res.json({ data: toNotificationResponsableResponse(notification) });
}
export async function patchNotificationsLues(req: Request, res: Response): Promise<void> {
const user = req.user;
if (!user) {
throw new AppError(401, 'Authentification requise');
}
const count = await marquerToutesNotificationsResponsableLues(user.id, user.roleCode);
res.json({ data: { count } });
}
+47
View File
@@ -3,6 +3,7 @@ import {
AnomalieResponsable,
EmpruntResponsable,
MaterielResponsable,
NotificationResponsable,
StatutCount,
} from '../repositories/responsable.repository';
import { DashboardResponsableData } from '../services/responsable.service';
@@ -125,6 +126,26 @@ export interface AnomalieResponsableResponse {
} | null;
}
export interface NotificationResponsableResponse {
id: number;
titre: string;
message: string;
type: string;
lu: boolean;
dateCreation: Date;
anomalie: {
id: number;
statut: string;
type: string;
empruntId: number;
materiel: {
id: number;
nom: string;
reference: string;
};
} | null;
}
function toRepartition(rows: StatutCount[]): RepartitionStatutResponse {
const parStatut: Record<string, number> = {};
let total = 0;
@@ -271,3 +292,29 @@ export function toAnomalieResponsableResponse(
: null,
};
}
export function toNotificationResponsableResponse(
notification: NotificationResponsable,
): NotificationResponsableResponse {
return {
id: notification.id,
titre: notification.titre,
message: notification.message,
type: notification.type,
lu: notification.lu,
dateCreation: notification.dateCreation,
anomalie: notification.anomalie
? {
id: notification.anomalie.id,
statut: notification.anomalie.statut,
type: notification.anomalie.type,
empruntId: notification.anomalie.emprunt.id,
materiel: {
id: notification.anomalie.materiel.id,
nom: notification.anomalie.materiel.nom,
reference: notification.anomalie.materiel.reference,
},
}
: null,
};
}
@@ -37,6 +37,17 @@ export type AnomalieResponsable = Prisma.AnomalieGetPayload<{
};
}>;
export type NotificationResponsable = Prisma.NotificationGetPayload<{
include: {
anomalie: {
include: {
emprunt: true;
materiel: true;
};
};
};
}>;
export interface EmpruntResponsableFiltres {
statut?: string;
}
@@ -52,6 +63,10 @@ export interface AnomalieResponsableFiltres {
type?: string;
}
export interface NotificationResponsableFiltres {
lu?: boolean;
}
function mapStatutCounts(rows: Array<{ statut: string; _count: { _all: number } }>): StatutCount[] {
return rows.map((row) => ({ statut: row.statut, count: row._count._all }));
}
@@ -173,3 +188,54 @@ export function findAnomaliesResponsable(
orderBy: { dateDetection: 'desc' },
});
}
export function findNotificationsResponsable(
utilisateurId: number,
filtres: NotificationResponsableFiltres,
): Promise<NotificationResponsable[]> {
return prisma.notification.findMany({
where: {
utilisateurId,
...(filtres.lu !== undefined ? { lu: filtres.lu } : {}),
},
include: {
anomalie: {
include: {
emprunt: true,
materiel: true,
},
},
},
orderBy: { dateCreation: 'desc' },
});
}
export function marquerNotificationLue(
utilisateurId: number,
notificationId: number,
): Promise<NotificationResponsable> {
return prisma.notification.update({
where: {
id: notificationId,
utilisateurId,
},
data: { lu: true },
include: {
anomalie: {
include: {
emprunt: true,
materiel: true,
},
},
},
});
}
export async function marquerNotificationsResponsableLues(utilisateurId: number): Promise<number> {
const result = await prisma.notification.updateMany({
where: { utilisateurId, lu: false },
data: { lu: true },
});
return result.count;
}
@@ -4,6 +4,9 @@ import {
getDashboard,
getEmprunts,
getMateriels,
getNotifications,
patchNotificationLue,
patchNotificationsLues,
} from '../controllers/responsable.controller';
export const responsableRoutes: Router = Router();
@@ -12,3 +15,6 @@ responsableRoutes.get('/dashboard', getDashboard);
responsableRoutes.get('/emprunts', getEmprunts);
responsableRoutes.get('/materiels', getMateriels);
responsableRoutes.get('/anomalies', getAnomalies);
responsableRoutes.get('/notifications', getNotifications);
responsableRoutes.patch('/notifications/lu-toutes', patchNotificationsLues);
responsableRoutes.patch('/notifications/:id/lu', patchNotificationLue);
@@ -8,11 +8,15 @@ import {
findAnomaliesResponsable,
findEmpruntsResponsable,
findMaterielsResponsable,
findNotificationsResponsable,
marquerNotificationLue,
marquerNotificationsResponsableLues,
StatutCount,
ActiviteResponsable,
AnomalieResponsable,
EmpruntResponsable,
MaterielResponsable,
NotificationResponsable,
} from '../repositories/responsable.repository';
import {
STATUT_ANOMALIE,
@@ -46,6 +50,10 @@ export interface ListerAnomaliesResponsableFiltres {
type?: string;
}
export interface ListerNotificationsResponsableFiltres {
lu?: boolean;
}
function verifierResponsable(roleCode: string): void {
if (roleCode !== 'RESPONSABLE') {
throw new AppError(403, 'Acces reserve au responsable materiel');
@@ -121,6 +129,32 @@ export function parseCategorieId(value: unknown): number | undefined {
return parsed;
}
export function parseNotificationId(value: unknown): number {
if (typeof value !== 'string') {
throw new AppError(400, 'notificationId invalide');
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new AppError(400, 'notificationId invalide');
}
return parsed;
}
export function parseLu(value: unknown): boolean | undefined {
if (value === undefined) {
return undefined;
}
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
throw new AppError(400, 'lu invalide');
}
export function listerEmpruntsResponsable(
roleCode: string,
campusId: number,
@@ -147,3 +181,34 @@ export function listerAnomaliesResponsable(
verifierResponsable(roleCode);
return findAnomaliesResponsable(campusId, filtres);
}
export function listerNotificationsResponsable(
utilisateurId: number,
roleCode: string,
filtres: ListerNotificationsResponsableFiltres,
): Promise<NotificationResponsable[]> {
verifierResponsable(roleCode);
return findNotificationsResponsable(utilisateurId, filtres);
}
export async function marquerNotificationResponsableLue(
utilisateurId: number,
roleCode: string,
notificationId: number,
): Promise<NotificationResponsable> {
verifierResponsable(roleCode);
try {
return await marquerNotificationLue(utilisateurId, notificationId);
} catch {
throw new AppError(404, 'Notification introuvable');
}
}
export function marquerToutesNotificationsResponsableLues(
utilisateurId: number,
roleCode: string,
): Promise<number> {
verifierResponsable(roleCode);
return marquerNotificationsResponsableLues(utilisateurId);
}
+9 -1
View File
@@ -357,6 +357,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
- 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.
### Étape 38 — API responsable : notifications
- Branche dédiée `feat/responsable-notifications-api` créée après merge de la consultation des anomalies responsable dans `develop`.
- Endpoint `GET /api/responsable/notifications` ajouté, protégé par rôle `RESPONSABLE`.
- Filtre optionnel `lu=true|false` ajouté pour séparer les notifications lues et non lues.
- Endpoint `PATCH /api/responsable/notifications/:id/lu` ajouté pour marquer une notification du responsable connecté comme lue.
- Endpoint `PATCH /api/responsable/notifications/lu-toutes` ajouté pour marquer toutes les notifications non lues du responsable connecté comme lues.
- Réponse enrichie avec l'anomalie liée et le matériel concerné lorsque la notification pointe vers une anomalie.
---
*Dernière mise à jour : 2026-07-15 — Dashboard, consultation des emprunts, du stock et des anomalies responsable ajoutés côté API.*
*Dernière mise à jour : 2026-07-15 — Dashboard, consultations responsable et notifications ajoutés côté API.*