diff --git a/CLAUDE.md b/CLAUDE.md index b7177b4..4792cde 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -209,7 +209,7 @@ - `AuditLogger::log()` persists without flushing — the processor's `flush()` handles both the data change and the audit entry atomically - Audit logs are accessible only via `ROLE_SUPER_ADMIN` (hidden role, added manually in DB) - **Contexte forensique automatique** : chaque entrée capte aussi `ipAddress`, `userAgent` (brut), `deviceLabel` (libellé lisible via `App\Service\UserAgentParser`) et `deviceId` (header `X-Device-Id`, device id persistant `localStorage['sirh-device-id']` envoyé par le front depuis `useApi`/`useDeviceId`). Capture centralisée dans `AuditLogger::log()` via `RequestStack` (null en contexte CLI). But : distinguer les appareils derrière un compte partagé (ex. « Usine »). IP fiable derrière proxy → activer `framework.trusted_proxies`. **CORS** : `X-Device-Id` doit rester dans `nelmio_cors.allow_headers` (front/API cross-origin → préflight, sinon le navigateur bloque toutes les requêtes). Affichage écran (`audit-logs.vue`) non couvert (refonte séparée). Doc : `doc/audit-logging.md`. -- **Écran Journal refondu** (`frontend/pages/audit-logs.vue` + `useAuditLogsList`) : tableau en `MalioDataTable` (1er usage SIRH), **drawer de filtre** façon STARSEED (`MalioDrawer` + `MalioAccordion`, état brouillon/appliqué, badge compteur, Réinitialiser/Appliquer), **drawer de détail** au clic ligne. Filtres backend : `username`/`ip`/`device` (LIKE insensible casse), `entityType[]`/`action[]` (IN), `perPage` (25/50/100). Logique dans `useAuditLogsList` ; libellés FR en dur ; filtres hors URL. Provider/`AuditLogReadRepositoryInterface`/repository portent les nouveaux critères. +- **Écran Journal refondu** (`frontend/pages/audit-logs.vue` + `useAuditLogsList`) : tableau en `MalioDataTable` (1er usage SIRH), **drawer de filtre** façon STARSEED (`MalioDrawer` + `MalioAccordion`, état brouillon/appliqué, badge compteur, Réinitialiser/Appliquer), **drawer de détail** au clic ligne. Filtres backend : `username`/`ip`/`device` (LIKE insensible casse), `entityType[]`/`action[]` (IN), `perPage` (10/25/50/100, défaut 10). Filtres **employé** et **utilisateur** = `MalioSelect` (listes employés / comptes via `listUsers`). Logique dans `useAuditLogsList` ; libellés FR en dur ; filtres hors URL. Provider/`AuditLogReadRepositoryInterface`/repository portent les nouveaux critères. - Documentation: `doc/audit-logging.md` ## Backend Conventions diff --git a/doc/audit-logging.md b/doc/audit-logging.md index 96c9f72..5861a8c 100644 --- a/doc/audit-logging.md +++ b/doc/audit-logging.md @@ -51,15 +51,15 @@ Capture : automatique et centralisée dans `AuditLogger::log()` (via `RequestSta ## Filtres disponibles -- Par employé (affecté) +- Par employé (affecté) — **select** (liste des employés) - Par période (date affectée) — sélecteur de plage - Par type(s) d'entité (multi-sélection) - Par action(s) (multi-sélection) -- Par utilisateur / compte (recherche partielle, insensible à la casse) +- Par utilisateur / compte — **select** (liste des comptes ; correspondance partielle insensible à la casse côté back) - Par IP (recherche partielle) - Par appareil (recherche partielle sur le libellé ou le device id) -Pagination : `perPage` (25 / 50 / 100, défaut 50) + `page`. +Pagination : `perPage` (10 / 25 / 50 / 100, défaut 10) + `page`. L'écran utilise un `MalioDataTable`, un **drawer de filtre** (bouton « Filtrer » avec compteur de filtres actifs, état brouillon/appliqué, Réinitialiser/Appliquer) et un **drawer de détail** ouvert au clic sur une ligne (méta + contexte technique IP/appareil/User-Agent/device id + diff lisible des changements). diff --git a/frontend/composables/useAuditLogsList.ts b/frontend/composables/useAuditLogsList.ts index 58a4f2d..4a4dd99 100644 --- a/frontend/composables/useAuditLogsList.ts +++ b/frontend/composables/useAuditLogsList.ts @@ -2,18 +2,21 @@ import { ref, computed } from 'vue' import type { AuditLog } from '~/services/dto/audit-log' import { fetchAuditLogs, type AuditLogFilters } from '~/services/audit-logs' import { listEmployees } from '~/services/employees' +import { listUsers } from '~/services/users' type Range = { start: string, end: string } | null type SelectOption = { value: number, text: string } +type UsernameOption = { value: string, text: string } export const useAuditLogsList = () => { const items = ref([]) const total = ref(0) const page = ref(1) - const perPage = ref(50) + const perPage = ref(10) const loading = ref(false) const filterOpen = ref(false) const employeeOptions = ref([]) + const usernameOptions = ref([]) // Applied filters (drive the fetch) const appliedEmployeeId = ref(undefined) @@ -85,6 +88,14 @@ export const useAuditLogsList = () => { } catch { employeeOptions.value = [] } + try { + const users = await listUsers() + usernameOptions.value = users + .map(u => ({ value: u.username, text: u.username })) + .sort((a, b) => a.text.localeCompare(b.text)) + } catch { + usernameOptions.value = [] + } await load() } @@ -149,7 +160,7 @@ export const useAuditLogsList = () => { const toggleAction = (value: string, selected: boolean) => toggle(draftActions, value, selected) return { - items, total, page, perPage, loading, filterOpen, employeeOptions, activeFilterCount, + items, total, page, perPage, loading, filterOpen, employeeOptions, usernameOptions, activeFilterCount, draftEmployeeId, draftRange, draftEntityTypes, draftActions, draftUsername, draftIp, draftDevice, init, goToPage, setPerPage, openFilters, applyFilters, resetFilters, toggleEntityType, toggleAction, } diff --git a/frontend/pages/audit-logs.vue b/frontend/pages/audit-logs.vue index f5fba90..cd85e8a 100644 --- a/frontend/pages/audit-logs.vue +++ b/frontend/pages/audit-logs.vue @@ -17,7 +17,7 @@ :total-items="list.total.value" :page="list.page.value" :per-page="list.perPage.value" - :per-page-options="[25, 50, 100]" + :per-page-options="[10, 25, 50, 100]" empty-message="Aucune entrée trouvée." @row-click="openDetail" @update:page="list.goToPage($event)" @@ -97,7 +97,11 @@ - + diff --git a/src/State/AuditLogProvider.php b/src/State/AuditLogProvider.php index 2cd9b80..390f138 100644 --- a/src/State/AuditLogProvider.php +++ b/src/State/AuditLogProvider.php @@ -14,8 +14,8 @@ use Symfony\Component\HttpFoundation\RequestStack; class AuditLogProvider implements ProviderInterface { - private const DEFAULT_PER_PAGE = 50; - private const ALLOWED_PER_PAGE = [25, 50, 100]; + private const DEFAULT_PER_PAGE = 10; + private const ALLOWED_PER_PAGE = [10, 25, 50, 100]; public function __construct( private readonly RequestStack $requestStack, diff --git a/tests/State/AuditLogProviderTest.php b/tests/State/AuditLogProviderTest.php index 54c7ea8..6c4c34d 100644 --- a/tests/State/AuditLogProviderTest.php +++ b/tests/State/AuditLogProviderTest.php @@ -77,13 +77,22 @@ final class AuditLogProviderTest extends TestCase self::assertNull($repo->findArgs['actions']); } - public function testPerPageOutOfRangeFallsBackTo50(): void + public function testPerPageOutOfRangeFallsBackToDefault(): void { $repo = $this->spyRepository(); $response = $this->provideWith($repo, ['perPage' => '999']); - self::assertSame(50, $repo->findArgs['limit']); - self::assertSame(50, json_decode((string) $response->getContent(), true)['perPage']); + self::assertSame(10, $repo->findArgs['limit']); + self::assertSame(10, json_decode((string) $response->getContent(), true)['perPage']); + } + + public function testDefaultPerPageIs10(): void + { + $repo = $this->spyRepository(); + $response = $this->provideWith($repo, []); + + self::assertSame(10, $repo->findArgs['limit']); + self::assertSame(10, json_decode((string) $response->getContent(), true)['perPage']); } private function spyRepository(array $items = [], int $count = 0): AuditLogReadRepositoryInterface