diff --git a/docs/superpowers/specs/2026-06-24-audit-log-screen-rework-design.md b/docs/superpowers/specs/2026-06-24-audit-log-screen-rework-design.md new file mode 100644 index 0000000..efd2fb8 --- /dev/null +++ b/docs/superpowers/specs/2026-06-24-audit-log-screen-rework-design.md @@ -0,0 +1,207 @@ +# 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 : `` 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 : +```ts +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` : +```php +findByFilters( + ?int $employeeId, + ?DateTimeImmutable $from, + ?DateTimeImmutable $to, + ?array $entityTypes, // list|null + ?array $actions, // list|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).