Files
SIRH/docs/superpowers/specs/2026-06-24-audit-log-screen-rework-design.md
T
2026-06-24 11:03:36 +02:00

10 KiB

Refonte de l'écran Journal d'activité (MalioDataTable + drawer de filtre)

Date : 2026-06-24 Branche : feature/SIRH-41-ajouter-plus-d-info-dans-le-journal-d-activite

Problème / objectif

L'écran frontend/pages/audit-logs.vue (journal d'activité, ROLE_SUPER_ADMIN) est aujourd'hui fait main : <select>/<input> natifs, tableau en grille CSS, lignes dépliables affichant le diff JSON brut, pagination « précédent/suivant » figée à 50/page. Il faut le moderniser :

  1. Passer le tableau en MalioDataTable (1er usage dans SIRH).
  2. Mettre les filtres dans un drawer, sur le même principe que STARSEED (les écrans de liste modules/.../pages/.../index.vue : MalioDrawer + MalioAccordion, état brouillon/appliqué, footer Réinitialiser/Appliquer, badge de compteur de filtres actifs).
  3. Passer tous les composants de l'écran en composants Malio quand l'équivalent existe.
  4. Exploiter les nouvelles données forensiques (IP, appareil, User-Agent, device id) déjà captées par le backend.

Référence de pattern

  • STARSEED, écran canonique : /home/m-tristan/workspace/Starseed/frontend/modules/commercial/pages/clients/index.vue (drawer de filtre, MalioAccordion, brouillon→appliqué, MalioDataTable, badge compteur).
  • Adaptations SIRH : libellés en français en dur (convention des drawers SIRH existants — employees/index.vue, sites.vue — pas d'i18n comme STARSEED) ; filtres non persistés en URL (comme STARSEED et l'écran actuel).
  • Malio @malio/layer-ui 1.7.15 (doc node_modules/@malio/layer-ui/COMPONENTS.md).

Périmètre

Inclus : refonte complète de audit-logs.vue (tableau, filtres, détail) + évolutions backend nécessaires (perPage + nouveaux filtres) + DTO TS + docs.

Exclus : toute autre page ; l'audit reste ROLE_SUPER_ADMIN ; pas de doc in-app (outil caché, aucun article existant — décision déjà prise au lot précédent).


A. Tableau — MalioDataTable

API (1.7.15) : :columns ({key,label}[]), :items, :total-items, v-model:page, v-model:per-page, :per-page-options, row-clickable, événements row-click / update:page / update:per-page, slots #cell-{key} et #empty.

Colonnes :

key label rendu
createdAt Date action JJ/MM/AAAA HH:MM (déjà formaté par le provider)
username Utilisateur texte brut
action Action badge couleur via #cell-action (create=vert, update=bleu, delete=rouge, validate=violet, site_validate=indigo, défaut=neutre)
entityType Type libellé FR via #cell-entityType (work_hour→Heures, absence→Absence, employee→Employé, contract_suspension→Suspension, rtt_payment→RTT, fractioned_days→Fract., paid_leave_days→Congés payés, week_comment→Commentaire)
employeeName Employé nom ou
deviceLabel Appareil deviceLabel ou
description Description tronqué (truncate + title) via #cell-description
  • :per-page-options="[25, 50, 100]", perPage par défaut 50.
  • @row-click → ouvre le drawer de détail avec la ligne cliquée.
  • :items = directement les AuditLog de la page courante (le DTO porte déjà toutes les clés ; les key de colonnes correspondent aux champs).

B. Drawer de détail (clic ligne)

MalioDrawer (droite, drawer-class="max-w-xl"), titre #header = « Détail de l'action ». Contenu (lecture seule, sections) :

  • Méta : Utilisateur, Employé, Date action, Date affectée, Action (badge), Type (libellé).
  • Contexte technique : IP (ipAddress), Appareil (deviceLabel), User-Agent brut (userAgent, en break-all/petite police), Device id (deviceId). Champs nuls → .
  • Changements : si changes non nul, rendu lisible — pour chaque clé présente dans old/new, une ligne clé : ancienne → nouvelle (au lieu du double bloc JSON brut actuel). Helper front formatChanges(changes) qui fusionne les clés de old et new. Si changes nul → « Aucun détail de modification ».

État : selectedLog: AuditLog | null + detailOpen: boolean. Fermeture standard MalioDrawer.

C. Drawer de filtre (principe STARSEED)

Bouton « Filtrer » (MalioButton variant="tertiary" icon-name="mdi:tune") dans la barre de titre ; son label porte le compteur de filtres actifs (Filtrer (N) si N>0).

MalioDrawer (drawer-class="max-w-[450px]", body-class="p-0", footer-class="justify-between border-t border-black p-6"), titre #header = « Filtres ». Corps en MalioAccordion (un MalioAccordionItem par section) :

Section Composant Champ filtre
Période MalioDateRange (v-model = {start,end} ISO) from/to sur affectedDate (sémantique actuelle conservée)
Employé MalioSelect (options = employés chargés au mount) employeeId (valeur unique)
Type d'entité liste de MalioCheckbox (multi) entityType[]
Action liste de MalioCheckbox (multi) action[]
Utilisateur / compte MalioInputText (icon mdi:magnify) username (ILIKE partiel)
IP MalioInputText ip (ILIKE partiel)
Appareil MalioInputText device (ILIKE partiel sur device_label OU device_id)

Footer : MalioButton variant="tertiary" Réinitialiser (gauche) + MalioButton variant="primary" Appliquer (droite).

État brouillon → appliqué (pattern STARSEED) :

  • draft* refs (éditées dans le drawer) et applied* refs (pilotent le fetch).
  • openFilters() : copie applied*draft* puis ouvre.
  • applyFilters() : copie draft*applied*, remet page=1, refetch, ferme le drawer.
  • resetFilters() : vide draft* et applied*, remet page=1, refetch, laisse le drawer ouvert.
  • activeFilterCount (computed sur applied*) → badge bouton.
  • Helpers toggle(arrayRef, value, selected) pour les multi-select.
  • Options Type d'entité / Action = listes statiques (mêmes codes que le provider) ; options Employé chargées une fois au onMounted (réutiliser le chargement employés déjà fait par l'écran actuel).

D. Composable useAuditLogsList

Composable spécifique à l'écran (frontend/composables/useAuditLogsList.ts) — pas de usePaginatedList générique (un seul consommateur → YAGNI). Expose :

  • état : items, total, page, perPage, loading, les draft*/applied*, activeFilterCount, employeeOptions.
  • actions : load() (fetch avec filtres appliqués + page/perPage), goToPage(n), setPerPage(n), openFilters(), applyFilters(), resetFilters(), loadEmployeeOptions().
  • load() doit ignorer les réponses périmées (garde anti-race : compteur de requête, on jette les réponses dont l'index n'est pas le dernier émis).

La page audit-logs.vue se réduit à : barre de titre (titre + bouton Filtrer), MalioDataTable, drawer filtre, drawer détail — toute la logique vit dans le composable.

E. Backend

frontend/services/dto/audit-log.ts (AuditLogFilters)

Étendre :

export type AuditLogFilters = {
  employeeId?: number
  from?: string
  to?: string
  entityType?: string[]
  action?: string[]
  username?: string
  ip?: string
  device?: string
  page?: number
  perPage?: number
}

fetchAuditLogs sérialise les tableaux en entityType[]/action[] (syntaxe PHP) et n'inclut que les filtres non vides.

src/ApiResource/AuditLogResource.php

Ajouter les QueryParameter : perPage, username, ip, device, action (entityType existe déjà). (Les QueryParameter sont surtout documentaires : le provider lit $request->query.)

src/State/AuditLogProvider.php

  • Lire perPage (défaut 50, clampé à un ensemble autorisé [25,50,100], fallback 50 ; borne dure).
  • Lire username, ip, device (chaînes, null si vide).
  • Lire entityType et action en tableaux ($request->query->all('entityType') / ->all('action')), null/[] si absent. Conserver la rétro-compat : si entityType arrive en scalaire, le normaliser en tableau à un élément.
  • Passer le tout au repository ; perPage remplace la constante PER_PAGE. La réponse renvoie perPage réel.

src/Repository/Contract/AuditLogReadRepositoryInterface.php + AuditLogRepository.php

Faire évoluer findByFilters / countByFilters :

findByFilters(
    ?int $employeeId,
    ?DateTimeImmutable $from,
    ?DateTimeImmutable $to,
    ?array $entityTypes,   // list<string>|null
    ?array $actions,       // list<string>|null
    ?string $username,
    ?string $ip,
    ?string $device,
    int $limit,
    int $offset,
): array
countByFilters(... mêmes filtres ...): int

Clauses : employeeId =, dates BETWEEN sur affectedDate (inchangé), entityTypes/actions IN (:...) si non vides, username/ip ILIKE %v% (paramètre échappé), device(device_label ILIKE :d OR device_id ILIKE :d). Tri inchangé (createdAt DESC). Mutualiser la construction des critères entre les deux méthodes (méthode privée applyFilters(QueryBuilder, ...)) pour rester DRY.

Tests

  • Backend : AuditLogProviderTest étendu — vérifier que perPage, username, ip, device, entityType[], action[] sont lus et transmis au repository (repo stubbé, on asserte les arguments via un spy), et que perPage hors liste retombe sur 50.
  • Backend : test repository des nouvelles clauses si un test repository existe ; sinon couvrir via le provider (le repo réel n'est pas unit-testé aujourd'hui — ne pas introduire d'intégration DB).
  • Front : pas de test auto (convention SIRH, pas de build) — revue de diff. Le composable useAuditLogsList reste pur/réactif et testable manuellement.

Documentation

  • doc/audit-logging.md : section « Filtres disponibles » mise à jour (employé, période, type[], action[], utilisateur, IP, appareil ; pagination perPage) + mention du drawer et du drawer de détail.
  • CLAUDE.md : compléter la puce « Contexte forensique » / journal pour noter l'écran refondu (MalioDataTable, drawer de filtre façon STARSEED, drawer de détail, filtres back username/ip/device/action[]/entityType[]/perPage).

Risques / notes

  • 1er MalioDataTable de SIRH : valider le rendu (le composant gère sa propre pagination/markup ; ne pas réappliquer le gabarit grille maison du CLAUDE.md à ce tableau).
  • MalioDateRange filtre affectedDate (cohérent avec l'existant) ; ne pas confondre avec createdAt (date d'action affichée en colonne).
  • Évolution de signature de AuditLogReadRepositoryInterface : mettre à jour l'implémentation et le provider dans le même lot (ils sont les seuls consommateurs).