Story CP-016: Audit Trail UI + Access Control¶
Module: control-plane Slice: Slice 4 from architecture (split from CP-004) Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 4 Depends on: CP-004 Estimated complexity: M
Objective¶
Build the audit trail viewer UI and the query endpoint with filters. The DG can browse, filter, and inspect audit entries. Non-DG users are denied access. This story builds on the AuditService and AuditEntry infrastructure from CP-004.
Backend Scope¶
Entities & Records¶
No new entities. Uses AuditEntry entity from CP-004.
DTOs (from CP-004, used by the query endpoint):
public record AuditEntryResponse(
UUID id, UUID userId, String userFullName, Instant timestamp,
String entityType, UUID entityId, String moduleCode,
AuditAction action, JsonNode beforeValue, JsonNode afterValue,
JsonNode metadata
) {}
public record AuditPageResponse(
List<AuditEntryResponse> entries, int totalCount, int page, int pageSize
) {}
public record AuditQueryParams(
@Nullable String entityType,
@Nullable UUID entityId,
@Nullable UUID userId,
@Nullable AuditAction action,
@Nullable String moduleCode,
@Nullable Instant dateFrom,
@Nullable Instant dateTo,
int page, // default 0
int pageSize // default 50
) {}
Repository Layer¶
Uses AuditRepository from CP-004:
- findByFilters(UUID tenantId, AuditQueryParams) → Page<AuditEntry> — Paginated query with optional filters on entity_type, entity_id, user_id, action, module_code, date range
Service Layer¶
AuditService.java (addition to CP-004's AuditService):
- query(AuditQueryParams params) → AuditPageResponse — Tenant-scoped query for the UI. Automatically scopes by TenantContext.current().tenantId().
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/audit | Query params (filters) | AuditPageResponse | ROLE_DG |
Validation Rules¶
entity_typemax 100 charsmodule_codemax 20 chars- Query date range:
date_frommust be beforedate_to - Page size: 1–100 (default 50)
- Only ROLE_DG can access this endpoint (403 for all other roles)
Frontend Scope¶
Pages & Routes¶
/admin/audit— Audit Trail viewer page
Components¶
AuditTrailPage.tsx— Filterable, paginated audit logAuditEntryCard.tsx— Single audit entry (mobile card view)AuditEntryRow.tsx— Table row (desktop view)AuditDetailSheet.tsx— Bottom sheet (mobile) / side panel (desktop) showing full before/after diffAuditFilterBar.tsx— Filter chips: Module, Action, Date range
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton: 5 entry card placeholders | — |
| Empty | Illustration + message | "Aucune activité enregistrée pour cette période." |
| Error | Red toast | "Une erreur est survenue. Veuillez réessayer." |
| Offline | Full page offline message | "Le journal d'audit n'est pas disponible hors ligne." |
| Success | Entries displayed with pagination | — |
| Unauthorized | Redirect or message | "Accès non autorisé" |
Responsive Behavior¶
- 360px (mobile): Timeline list. Each entry card shows: date/time, user name, human-readable action label, entity name in quotes, module badge, "Détails -->" link.
- 768px (tablet): Compact table view.
- 1024px+ (desktop): Full-width table with columns: Date/Heure, Utilisateur, Action, Entite, Module, Resume. Expandable rows showing before/after diff inline. Advanced filters: date range picker, module dropdown, action type checkboxes, user search.
Interactions¶
- Filters: Module dropdown, Action type (Creation/Modification/Suppression), Date range picker. Filters stack as chips.
- Pagination: "Charger plus..." button at bottom (mobile). Page numbers (desktop).
- Detail view: Tap "Détails -->" on any entry --> bottom sheet (mobile) / side panel (desktop) showing:
- Action, Entite, Par (user), Date
- "Modifications" section showing field-by-field before/after
- Before/After display: Human-readable format:
French micro-copy for audit UI:
| Key | French Text |
|---|---|
audit.title |
"Journal d'audit" |
audit.filter.module |
"Module" |
audit.filter.action |
"Action" |
audit.filter.date |
"Période" |
audit.action.create |
"Création" |
audit.action.update |
"Modification" |
audit.action.delete |
"Suppression" |
audit.detail.title |
"Détail de l'audit" |
audit.detail.action |
"Action" |
audit.detail.entity |
"Entité" |
audit.detail.by |
"Par" |
audit.detail.date |
"Date" |
audit.detail.changes |
"Modifications" |
audit.detail.before |
"Avant" |
audit.detail.after |
"Après" |
audit.load_more |
"Charger plus..." |
empty.audit |
"Aucune activité enregistrée pour cette période." |
Acceptance Criteria¶
AC-029: Audit query with filters
Given 50 audit entries exist for my tenant
When I call GET /api/v1/audit?entityType=Employee&action=CREATE&dateFrom=2026-02-01
Then I receive only audit entries matching all three filters
AC-031: Audit UI displays timeline
Given I am DG and navigate to /admin/audit
When the page loads
Then I see a chronological list of audit entries with filters at the top
AC-032: Audit detail view shows before/after
Given an UPDATE audit entry for a department rename
When I tap "Détails -->" on the entry
Then I see the before/after diff in human-readable format: "Nom -- Avant: Production -- Apres: Prod."
AC-033: Audit accessible only to DG
Given I am authenticated with role EMPLOYE
When I navigate to /admin/audit
Then I am redirected or shown "Acces non autorise"
OHADA & Regulatory Rules¶
- AUDCIF Art. 24: The audit viewer provides the DG with traceability of all data mutations, satisfying the 10-year retention visibility requirement.
- AUDCIF Art. 22: Audit entries displayed are immutable — the UI is read-only. No edit or delete actions exist.
- Loi 2013-450: Audit trail viewer restricted to DG role enforces accountability for personal data access.
Standards & Conventions¶
docs/standards/react-typescript-guidelines.md— §List/table components, §Filter pattern, §Bottom sheet detail, §Paginationdocs/standards/api-guidelines.md— §Pagination, §Query parameter conventions
Testing Requirements¶
Unit Tests¶
- AuditService.query(): filter combinations (entity_type, action, date range, module)
- AuditService.query(): pagination (page, pageSize)
- Before/after diff generation for human-readable display
Integration Tests¶
- GET /api/v1/audit — with multiple filter combinations
- GET /api/v1/audit — pagination (50 per page, next page)
- GET /api/v1/audit — tenant isolation (tenant A queries, no tenant B data)
- GET /api/v1/audit — EMPLOYE role → 403
- GET /api/v1/audit — DG role → 200
What QA Will Validate¶
- Audit page loads with entries in reverse chronological order
- Filters work: Module, Action type, Date range
- Detail view shows before/after diff correctly
- Pagination ("Charger plus...") loads next page
- Page shows offline message when disconnected
- DG can access audit page; EMPLOYE cannot
- All breakpoints render correctly (360px, 768px, 1024px)
Out of Scope¶
- CSV export of audit trail — V2
- Cross-tenant audit query (for platform admin) — CP-012
- Audit trail for admin console actions — CP-012
Definition of Done¶
- [ ] GET /api/v1/audit endpoint with filters returns correct paginated data
- [ ] All frontend pages render correctly at 360px, 768px, 1024px
- [ ] All 6 UI states implemented (loading, empty, error, offline, success, unauthorized)
- [ ] French micro-copy matches design spec exactly
- [ ] All acceptance criteria pass manually
- [ ] Unit tests written and passing
- [ ] Integration tests written and passing
- [ ] DG-only access enforced (403 for other roles)
- [ ] No TypeScript
any— all types explicit - [ ] No Java raw types — all generics explicit
- [ ] API responses follow standard envelope format