feat(responsable): add anomaly and history tables
This commit is contained in:
@@ -24,7 +24,9 @@ import {
|
|||||||
parseStatutAnomalie,
|
parseStatutAnomalie,
|
||||||
parseStatutMateriel,
|
parseStatutMateriel,
|
||||||
parseStatutEmprunt,
|
parseStatutEmprunt,
|
||||||
|
parseTriAnomalie,
|
||||||
parseTriEmprunt,
|
parseTriEmprunt,
|
||||||
|
parseTriHistorique,
|
||||||
parseTriMateriel,
|
parseTriMateriel,
|
||||||
} from '../services/responsable.service';
|
} from '../services/responsable.service';
|
||||||
import {
|
import {
|
||||||
@@ -53,6 +55,9 @@ function getHistoriqueFiltres(req: Request) {
|
|||||||
empruntId: parsePositiveIntQuery(req.query.empruntId, 'empruntId'),
|
empruntId: parsePositiveIntQuery(req.query.empruntId, 'empruntId'),
|
||||||
dateDebut: parseDateQuery(req.query.dateDebut, 'dateDebut'),
|
dateDebut: parseDateQuery(req.query.dateDebut, 'dateDebut'),
|
||||||
dateFin: parseDateQuery(req.query.dateFin, 'dateFin'),
|
dateFin: parseDateQuery(req.query.dateFin, 'dateFin'),
|
||||||
|
recherche: typeof req.query.q === 'string' ? req.query.q : undefined,
|
||||||
|
tri: parseTriHistorique(req.query.tri),
|
||||||
|
ordre: parseOrdreTri(req.query.ordre),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,10 +168,16 @@ export async function getAnomalies(req: Request, res: Response): Promise<void> {
|
|||||||
|
|
||||||
const statut = parseStatutAnomalie(req.query.statut);
|
const statut = parseStatutAnomalie(req.query.statut);
|
||||||
const type = typeof req.query.type === 'string' ? req.query.type : undefined;
|
const type = typeof req.query.type === 'string' ? req.query.type : undefined;
|
||||||
|
const recherche = typeof req.query.q === 'string' ? req.query.q : undefined;
|
||||||
|
const tri = parseTriAnomalie(req.query.tri);
|
||||||
|
const ordre = parseOrdreTri(req.query.ordre);
|
||||||
|
|
||||||
const anomalies = await listerAnomaliesResponsable(user.roleCode, user.campusId, {
|
const anomalies = await listerAnomaliesResponsable(user.roleCode, user.campusId, {
|
||||||
statut,
|
statut,
|
||||||
type,
|
type,
|
||||||
|
recherche,
|
||||||
|
tri,
|
||||||
|
ordre,
|
||||||
});
|
});
|
||||||
res.json({ data: anomalies.map(toAnomalieResponsableResponse) });
|
res.json({ data: anomalies.map(toAnomalieResponsableResponse) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ export interface MaterielResponsableFiltres {
|
|||||||
export interface AnomalieResponsableFiltres {
|
export interface AnomalieResponsableFiltres {
|
||||||
statut?: string;
|
statut?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
recherche?: string;
|
||||||
|
tri?: 'dateDetection' | 'type' | 'materiel' | 'etudiant' | 'statut';
|
||||||
|
ordre?: 'asc' | 'desc';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationResponsableFiltres {
|
export interface NotificationResponsableFiltres {
|
||||||
@@ -109,6 +112,9 @@ export interface HistoriqueResponsableFiltres {
|
|||||||
empruntId?: number;
|
empruntId?: number;
|
||||||
dateDebut?: Date;
|
dateDebut?: Date;
|
||||||
dateFin?: Date;
|
dateFin?: Date;
|
||||||
|
recherche?: string;
|
||||||
|
tri?: 'dateAction' | 'action' | 'utilisateur' | 'materiel' | 'emprunt';
|
||||||
|
ordre?: 'asc' | 'desc';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangerStatutAnomalieData {
|
export interface ChangerStatutAnomalieData {
|
||||||
@@ -421,11 +427,40 @@ export function findAnomaliesResponsable(
|
|||||||
campusId: number,
|
campusId: number,
|
||||||
filtres: AnomalieResponsableFiltres,
|
filtres: AnomalieResponsableFiltres,
|
||||||
): Promise<AnomalieResponsable[]> {
|
): Promise<AnomalieResponsable[]> {
|
||||||
|
const ordre = filtres.ordre ?? 'desc';
|
||||||
|
const orderBy: Prisma.AnomalieOrderByWithRelationInput[] = (() => {
|
||||||
|
switch (filtres.tri) {
|
||||||
|
case 'type':
|
||||||
|
return [{ type: ordre }, { dateDetection: 'desc' }, { id: 'desc' }];
|
||||||
|
case 'materiel':
|
||||||
|
return [{ materiel: { nom: ordre } }, { dateDetection: 'desc' }, { id: 'desc' }];
|
||||||
|
case 'etudiant':
|
||||||
|
return [{ etudiant: { nom: ordre } }, { dateDetection: 'desc' }, { id: 'desc' }];
|
||||||
|
case 'statut':
|
||||||
|
return [{ statut: ordre }, { dateDetection: 'desc' }, { id: 'desc' }];
|
||||||
|
default:
|
||||||
|
return [{ dateDetection: ordre }, { id: 'desc' }];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return prisma.anomalie.findMany({
|
return prisma.anomalie.findMany({
|
||||||
where: {
|
where: {
|
||||||
emprunt: { campusId },
|
emprunt: { campusId },
|
||||||
...(filtres.statut ? { statut: filtres.statut } : {}),
|
...(filtres.statut ? { statut: filtres.statut } : {}),
|
||||||
...(filtres.type ? { type: filtres.type } : {}),
|
...(filtres.type ? { type: filtres.type } : {}),
|
||||||
|
...(filtres.recherche
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{ description: { contains: filtres.recherche } },
|
||||||
|
{ type: { contains: filtres.recherche } },
|
||||||
|
{ materiel: { nom: { contains: filtres.recherche } } },
|
||||||
|
{ materiel: { reference: { contains: filtres.recherche } } },
|
||||||
|
{ etudiant: { nom: { contains: filtres.recherche } } },
|
||||||
|
{ etudiant: { prenom: { contains: filtres.recherche } } },
|
||||||
|
{ etudiant: { email: { contains: filtres.recherche } } },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
emprunt: true,
|
emprunt: true,
|
||||||
@@ -433,7 +468,7 @@ export function findAnomaliesResponsable(
|
|||||||
materiel: { include: { categorie: true } },
|
materiel: { include: { categorie: true } },
|
||||||
traitePar: true,
|
traitePar: true,
|
||||||
},
|
},
|
||||||
orderBy: { dateDetection: 'desc' },
|
orderBy,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,6 +583,22 @@ export function findHistoriqueResponsable(
|
|||||||
campusId: number,
|
campusId: number,
|
||||||
filtres: HistoriqueResponsableFiltres,
|
filtres: HistoriqueResponsableFiltres,
|
||||||
): Promise<HistoriqueResponsable[]> {
|
): Promise<HistoriqueResponsable[]> {
|
||||||
|
const ordre = filtres.ordre ?? 'desc';
|
||||||
|
const orderBy: Prisma.HistoriqueOrderByWithRelationInput[] = (() => {
|
||||||
|
switch (filtres.tri) {
|
||||||
|
case 'action':
|
||||||
|
return [{ action: ordre }, { dateAction: 'desc' }, { id: 'desc' }];
|
||||||
|
case 'utilisateur':
|
||||||
|
return [{ utilisateur: { nom: ordre } }, { dateAction: 'desc' }, { id: 'desc' }];
|
||||||
|
case 'materiel':
|
||||||
|
return [{ materiel: { nom: ordre } }, { dateAction: 'desc' }, { id: 'desc' }];
|
||||||
|
case 'emprunt':
|
||||||
|
return [{ empruntId: ordre }, { dateAction: 'desc' }, { id: 'desc' }];
|
||||||
|
default:
|
||||||
|
return [{ dateAction: ordre }, { id: 'desc' }];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return prisma.historique.findMany({
|
return prisma.historique.findMany({
|
||||||
where: {
|
where: {
|
||||||
campusId,
|
campusId,
|
||||||
@@ -563,6 +614,19 @@ export function findHistoriqueResponsable(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
...(filtres.recherche
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{ action: { contains: filtres.recherche } },
|
||||||
|
{ description: { contains: filtres.recherche } },
|
||||||
|
{ utilisateur: { nom: { contains: filtres.recherche } } },
|
||||||
|
{ utilisateur: { prenom: { contains: filtres.recherche } } },
|
||||||
|
{ utilisateur: { email: { contains: filtres.recherche } } },
|
||||||
|
{ materiel: { nom: { contains: filtres.recherche } } },
|
||||||
|
{ materiel: { reference: { contains: filtres.recherche } } },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
utilisateur: true,
|
utilisateur: true,
|
||||||
@@ -571,6 +635,6 @@ export function findHistoriqueResponsable(
|
|||||||
sallePret: true,
|
sallePret: true,
|
||||||
posteEmprunt: true,
|
posteEmprunt: true,
|
||||||
},
|
},
|
||||||
orderBy: { dateAction: 'desc' },
|
orderBy,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,8 +80,18 @@ export type TriMaterielResponsable =
|
|||||||
export interface ListerAnomaliesResponsableFiltres {
|
export interface ListerAnomaliesResponsableFiltres {
|
||||||
statut?: StatutAnomalie;
|
statut?: StatutAnomalie;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
recherche?: string;
|
||||||
|
tri?: TriAnomalieResponsable;
|
||||||
|
ordre?: OrdreTri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TriAnomalieResponsable =
|
||||||
|
| 'dateDetection'
|
||||||
|
| 'type'
|
||||||
|
| 'materiel'
|
||||||
|
| 'etudiant'
|
||||||
|
| 'statut';
|
||||||
|
|
||||||
export interface ListerNotificationsResponsableFiltres {
|
export interface ListerNotificationsResponsableFiltres {
|
||||||
lu?: boolean;
|
lu?: boolean;
|
||||||
}
|
}
|
||||||
@@ -93,8 +103,18 @@ export interface ListerHistoriqueResponsableFiltres {
|
|||||||
empruntId?: number;
|
empruntId?: number;
|
||||||
dateDebut?: Date;
|
dateDebut?: Date;
|
||||||
dateFin?: Date;
|
dateFin?: Date;
|
||||||
|
recherche?: string;
|
||||||
|
tri?: TriHistoriqueResponsable;
|
||||||
|
ordre?: OrdreTri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TriHistoriqueResponsable =
|
||||||
|
| 'dateAction'
|
||||||
|
| 'action'
|
||||||
|
| 'utilisateur'
|
||||||
|
| 'materiel'
|
||||||
|
| 'emprunt';
|
||||||
|
|
||||||
export interface ChangerStatutAnomalieResponsableRequest {
|
export interface ChangerStatutAnomalieResponsableRequest {
|
||||||
statut: StatutAnomalie;
|
statut: StatutAnomalie;
|
||||||
observation?: string;
|
observation?: string;
|
||||||
@@ -244,6 +264,23 @@ export function parseStatutAnomalie(value: unknown): StatutAnomalie | undefined
|
|||||||
return value as StatutAnomalie;
|
return value as StatutAnomalie;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseTriAnomalie(value: unknown): TriAnomalieResponsable | undefined {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const valeurs: readonly TriAnomalieResponsable[] = [
|
||||||
|
'dateDetection',
|
||||||
|
'type',
|
||||||
|
'materiel',
|
||||||
|
'etudiant',
|
||||||
|
'statut',
|
||||||
|
];
|
||||||
|
if (typeof value !== 'string' || !valeurs.includes(value as TriAnomalieResponsable)) {
|
||||||
|
throw new AppError(400, 'tri invalide');
|
||||||
|
}
|
||||||
|
return value as TriAnomalieResponsable;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseCategorieId(value: unknown): number | undefined {
|
export function parseCategorieId(value: unknown): number | undefined {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -328,6 +365,23 @@ export function parseDateQuery(value: unknown, champ: string): Date | undefined
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseTriHistorique(value: unknown): TriHistoriqueResponsable | undefined {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const valeurs: readonly TriHistoriqueResponsable[] = [
|
||||||
|
'dateAction',
|
||||||
|
'action',
|
||||||
|
'utilisateur',
|
||||||
|
'materiel',
|
||||||
|
'emprunt',
|
||||||
|
];
|
||||||
|
if (typeof value !== 'string' || !valeurs.includes(value as TriHistoriqueResponsable)) {
|
||||||
|
throw new AppError(400, 'tri invalide');
|
||||||
|
}
|
||||||
|
return value as TriHistoriqueResponsable;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseLu(value: unknown): boolean | undefined {
|
export function parseLu(value: unknown): boolean | undefined {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -50,19 +50,86 @@ class ResponsableService {
|
|||||||
return _liste("/responsable/materiels?$query");
|
return _liste("/responsable/materiels?$query");
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<Map<String, dynamic>>> anomalies() =>
|
static Future<List<Map<String, dynamic>>> anomalies({
|
||||||
_liste("/responsable/anomalies");
|
String? statut,
|
||||||
|
String? type,
|
||||||
|
String? recherche,
|
||||||
|
String tri = "dateDetection",
|
||||||
|
String ordre = "desc",
|
||||||
|
}) {
|
||||||
|
final parametres = <String, String>{
|
||||||
|
"tri": tri,
|
||||||
|
"ordre": ordre,
|
||||||
|
"statut": ?statut,
|
||||||
|
"type": ?type,
|
||||||
|
if (recherche != null && recherche.trim().isNotEmpty)
|
||||||
|
"q": recherche.trim(),
|
||||||
|
};
|
||||||
|
final query = Uri(queryParameters: parametres).query;
|
||||||
|
return _liste("/responsable/anomalies?$query");
|
||||||
|
}
|
||||||
|
|
||||||
static Future<List<Map<String, dynamic>>> notifications() =>
|
static Future<List<Map<String, dynamic>>> notifications() =>
|
||||||
_liste("/responsable/notifications");
|
_liste("/responsable/notifications");
|
||||||
|
|
||||||
static Future<List<Map<String, dynamic>>> historique() =>
|
static Future<List<Map<String, dynamic>>> historique({
|
||||||
_liste("/responsable/historique");
|
String? action,
|
||||||
|
DateTime? dateDebut,
|
||||||
|
DateTime? dateFin,
|
||||||
|
String? recherche,
|
||||||
|
String tri = "dateAction",
|
||||||
|
String ordre = "desc",
|
||||||
|
}) {
|
||||||
|
return _liste(
|
||||||
|
"/responsable/historique?${_historiqueQuery(action: action, dateDebut: dateDebut, dateFin: dateFin, recherche: recherche, tri: tri, ordre: ordre)}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<ApiFile> exporterHistorique() => ApiClient.getFile(
|
static Future<ApiFile> exporterHistorique({
|
||||||
"/responsable/historique/export.csv",
|
String? action,
|
||||||
|
DateTime? dateDebut,
|
||||||
|
DateTime? dateFin,
|
||||||
|
String? recherche,
|
||||||
|
String tri = "dateAction",
|
||||||
|
String ordre = "desc",
|
||||||
|
}) {
|
||||||
|
final query = _historiqueQuery(
|
||||||
|
action: action,
|
||||||
|
dateDebut: dateDebut,
|
||||||
|
dateFin: dateFin,
|
||||||
|
recherche: recherche,
|
||||||
|
tri: tri,
|
||||||
|
ordre: ordre,
|
||||||
|
);
|
||||||
|
return ApiClient.getFile(
|
||||||
|
"/responsable/historique/export.csv?$query",
|
||||||
fallbackFileName: "historique-responsable.csv",
|
fallbackFileName: "historique-responsable.csv",
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _historiqueQuery({
|
||||||
|
String? action,
|
||||||
|
DateTime? dateDebut,
|
||||||
|
DateTime? dateFin,
|
||||||
|
String? recherche,
|
||||||
|
required String tri,
|
||||||
|
required String ordre,
|
||||||
|
}) {
|
||||||
|
final finIncluse = dateFin == null
|
||||||
|
? null
|
||||||
|
: DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59, 999);
|
||||||
|
return Uri(
|
||||||
|
queryParameters: {
|
||||||
|
"tri": tri,
|
||||||
|
"ordre": ordre,
|
||||||
|
"action": ?action,
|
||||||
|
if (dateDebut != null) "dateDebut": dateDebut.toUtc().toIso8601String(),
|
||||||
|
if (finIncluse != null) "dateFin": finIncluse.toUtc().toIso8601String(),
|
||||||
|
if (recherche != null && recherche.trim().isNotEmpty)
|
||||||
|
"q": recherche.trim(),
|
||||||
|
},
|
||||||
|
).query;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<int> marquerNotificationsLues() async {
|
static Future<int> marquerNotificationsLues() async {
|
||||||
final reponse = await ApiClient.patch(
|
final reponse = await ApiClient.patch(
|
||||||
|
|||||||
@@ -501,6 +501,22 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
- Les menus de filtres restent sur une ligne et tronquent les libellés trop longs pour prévenir tout débordement.
|
- Les menus de filtres restent sur une ligne et tronquent les libellés trop longs pour prévenir tout débordement.
|
||||||
- Vérifications réussies : build et lint backend, analyse Dart, tris et filtres combinés testés sur SQL Server.
|
- Vérifications réussies : build et lint backend, analyse Dart, tris et filtres combinés testés sur SQL Server.
|
||||||
|
|
||||||
|
### Étape 52 — Tableau des anomalies responsable
|
||||||
|
- La liste des anomalies devient un tableau opérationnel sur ordinateur et une liste compacte sur petit écran.
|
||||||
|
- Les colonnes présentent la date de détection, le type et la description, le matériel, l'étudiant, le statut et l'action suivante.
|
||||||
|
- La recherche serveur couvre la description, le type, le matériel, la référence et l'identité de l'étudiant.
|
||||||
|
- Les anomalies peuvent être filtrées par statut et type, puis triées par date, type, matériel, étudiant ou statut.
|
||||||
|
- Le changement de statut existant reste disponible dans chaque ligne et conserve les transitions métier autorisées.
|
||||||
|
- Vérifications réussies : build et lint backend, analyse Dart, tris et filtres combinés testés sur SQL Server.
|
||||||
|
|
||||||
|
### Étape 53 — Tableau et export filtré de l'historique responsable
|
||||||
|
- L'historique devient un tableau d'audit sur ordinateur et une liste compacte sur petit écran.
|
||||||
|
- Les colonnes présentent la date et l'heure, l'action et sa description, l'utilisateur, le matériel, l'emprunt et le point d'opération.
|
||||||
|
- La recherche serveur couvre l'action, la description, l'utilisateur et le matériel.
|
||||||
|
- Les événements peuvent être filtrés par action et période, puis triés par date, action, utilisateur, matériel ou emprunt.
|
||||||
|
- L'export CSV reprend désormais les filtres, la période et l'ordre actifs dans l'interface.
|
||||||
|
- Vérifications réussies : build et lint backend, analyse Dart, filtre combiné sur 22 événements et export CSV filtré testés sur SQL Server.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Dernière mise à jour : 2026-07-24 — Tableau de supervision du stock matériel enrichi.*
|
*Dernière mise à jour : 2026-07-24 — Supervision des anomalies et historique enrichie.*
|
||||||
|
|||||||
Reference in New Issue
Block a user