Story AB-011 : Liste et détail des demandes de congés (Backend + Frontend)¶
Module : absence-management Slice : VS-AB-011 de l'architecture Contexte produit : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priorité : 11 Dépend de : AB-010 Peut être développé en parallèle avec : AB-012, AB-013 Ordre de merge : Après AB-010 Complexité estimée : S PRD User Stories : US-ABS-001, US-ABS-009
Objectif¶
Permettre aux employés de consulter l'historique de leurs demandes de congés (regroupées par statut : En cours, En attente, Passées) avec pagination par curseur et filtrage selon le rôle, ainsi qu'une vue détaillée des demandes individuelles affichant les dates, le statut et la chronologie d'approbation.
Périmètre Backend¶
Entités & Enregistrements¶
Enregistrements de réponse :
public record LeaveRequestListResponse(
List<LeaveRequestSummary> items,
String nextCursor, // null if no more pages
int totalCount
) {}
public record LeaveRequestSummary(
UUID id,
LeaveType leaveType,
LeaveRequestStatus status,
LocalDate startDate,
LocalDate endDate,
BigDecimal numberOfDays,
Instant createdAt
) {}
public record LeaveRequestDetailResponse(
UUID id,
LeaveType leaveType,
LeaveRequestStatus status,
LocalDate startDate,
LocalDate endDate,
boolean halfDayStart,
boolean halfDayEnd,
BigDecimal numberOfDays,
String reason,
boolean isRetroactive,
String rejectionReason, // null unless REJECTED
Instant createdAt,
Instant approvedAt, // null unless APPROVED
String approverName, // null unless APPROVED/REJECTED
UUID supportingDocumentId, // null unless document attached
List<StatusTimelineEntry> timeline
) {}
public record StatusTimelineEntry(
LeaveRequestStatus status,
Instant timestamp,
String actorName,
String note // rejection reason, etc.
) {}
Couche Repository¶
LeaveRequestRepository (extension d'AB-002) :
- findByTenantIdAndEmployeeIdWithCursor(UUID tenantId, UUID employeeId, LeaveRequestStatus status, LocalDate from, LocalDate to, String cursor, int limit) — pagination par curseur en utilisant le keyset (pas d'offset)
- findByTenantIdWithCursor(UUID tenantId, ...) — pour l'accès HR/DG (tous les employés)
- findByTenantIdAndManagerIdWithCursor(UUID tenantId, UUID managerId, ...) — pour MANAGER (collaborateurs directs + soi-même)
- countByTenantIdAndEmployeeIdAndStatus(UUID tenantId, UUID employeeId, LeaveRequestStatus status) — pour les compteurs de badges
Couche Service¶
LeaveRequestQueryService.java :
- getMyRequests(filters, cursor) : EMPLOYEE → uniquement ses propres demandes
- getRequests(filters, cursor) : MANAGER → collaborateurs directs + soi-même ; HR_MANAGER/DG → tous
- getRequestDetail(UUID requestId) : retourne le détail avec la chronologie, respecte le RBAC
- Pagination par curseur : pagination keyset sur la clé composite (created_at, id) — pas de dérive d'offset
Endpoints API¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/absence/leave-requests |
?status=...&from=...&to=...&sort=createdAt,desc&cursor=...&limit=20 |
LeaveRequestListResponse |
EMPLOYEE (own), MANAGER (direct reports+own), HR_MANAGER/DG (all) |
| GET | /api/v1/absence/leave-requests/{id} |
— | LeaveRequestDetailResponse |
EMPLOYEE (own), MANAGER (if subordinate), HR_MANAGER, DG |
Règles de validation¶
- EMPLOYEE peut uniquement voir ses propres demandes
- MANAGER peut voir ses collaborateurs directs + ses propres demandes
- HR_MANAGER et DG peuvent voir toutes les demandes du tenant
- Le curseur doit être un jeton opaque valide (base64 encodé
created_at:id) limitplafonné à 50 (défaut 20)- La plage de dates
from/tosont des filtres optionnels
Considérations multi-tenant¶
- Toutes les requêtes incluent
tenantIddepuisScopedValue<TenantContext> - Les jetons de curseur sont filtrés par tenant (ne peuvent pas être utilisés entre tenants)
- Fonctionne sur les 4 profils DB
Périmètre Frontend¶
RAPPEL OFFLINE SYNC (Papillon) : Cette story est principalement en lecture seule, mais les données de la liste des demandes DOIVENT être mises en cache dans IndexedDB pour une consultation hors connexion. Mettre en cache la page la plus récente des demandes. Les demandes DRAFT (créées hors connexion dans AB-010) doivent apparaître dans la liste avec une icône de synchronisation en attente. Afficher la bannière "Hors connexion" avec l'heure de la dernière synchronisation en cas de déconnexion.
Pages & Routes¶
| Route | Page | Description |
|---|---|---|
/absences |
AbsenceHome (section historique) | Liste des demandes regroupées par statut |
/absences/demande/{id} |
LeaveRequestDetail | Détail et chronologie d'une demande individuelle |
Composants¶
S1 — Section historique (sur AbsenceHome) :
- RequestCard — Carte affichant l'icône du type de congé, les dates, le nombre de jours, le Badge de statut
- StatusSection — Section groupée avec en-tête ("En cours", "En attente", "Passées")
- Badge — Badge de statut avec correspondance de couleurs (voir ci-dessous)
- InfiniteScroll — Déclenche la pagination par curseur au défilement vers le bas
S3 — Vue détaillée :
- RequestDetailCard — Carte d'informations complète (type, statut, dates, durée, demi-journée, motif, dates)
- StatusTimeline — Chronologie verticale affichant l'historique des statuts
- DocumentIndicator — Affiche le statut du document joint
- RejectionCard — Carte teintée de rouge affichant le motif du refus (si REJECTED)
États UI (TOUS OBLIGATOIRES)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | 3 card-shaped shimmer rectangles | — |
| Empty (no requests) | EmptyState with calendar+sunshine illustration | "Vous n'avez pas encore de demande de conge" / CTA: "Faire ma premiere demande" |
| Error | Toast + retry | "Impossible de charger vos conges. Reessayez." |
| Offline | Cached data from IndexedDB + amber banner | "Hors connexion -- derniere synchro : {time}" |
| Success (after form) | New card at top with scale-bounce entrance animation | — |
Comportement responsive¶
Mobile (360px — priorité Papillon) : - Cartes regroupées par sections de statut : "En cours" (APPROVED, IN_PROGRESS), "En attente" (PENDING_APPROVAL, PENDING_DOCUMENT, DRAFT), "Passées" (COMPLETED, REJECTED, CANCELLED) - Chaque carte : icône de type de congé + dates + nombre de jours + Badge de statut - Appuyer sur une carte → naviguer vers la vue détaillée - Défilement infini pour la pagination - Cibles de toucher >= 48px
Desktop (1024px+ — priorité Enterprise) : - DataTable remplace les cartes avec les colonnes : Type, Dates, Jours, Statut - Onglets : "En cours (N)" / "En attente (N)" / "Passées" - Colonnes triables, lignes cliquables → vue détaillée dans un panneau latéral ou une modal - Menu déroulant de filtre par statut + sélecteur de plage de dates dans la barre d'outils
Interactions¶
- Appuyer/cliquer sur une carte ou une ligne → naviguer vers
/absences/demande/{id} - Défiler vers le bas → charger la page suivante via pagination par curseur
- Glisser vers le bas pour actualiser (mobile) → recharger depuis l'API
- Filtrer par statut (onglets desktop, en-têtes de sections mobile)
- Bouton retour depuis le détail → retour à la liste avec la position de défilement préservée
Correspondance des badges de statut¶
| Status | Color | French Label |
|---|---|---|
| APPROVED | success (green) | "Approuve" |
| PENDING_APPROVAL | warning (amber) | "En attente d'approbation" |
| PENDING_DOCUMENT | warning (amber) | "En attente de justificatif" |
| REJECTED | error (red) | "Rejete" |
| CANCELLED | error (red) | "Annule" |
| IN_PROGRESS | info (blue) | "En cours" |
| COMPLETED | neutral (gray) | "Termine" |
| DRAFT | info (blue) | "Brouillon" |
Critères d'acceptation¶
AC-036: Role-based request visibility
Given I am an employee
When I view my leave requests
Then I see only my own requests; managers see direct reports + own; HR/DG see all
AC-037: Cursor-based pagination
Given there are more than 20 leave requests
When I scroll to the bottom of the list
Then additional requests are loaded via cursor pagination (no offset drift)
AC-038: Filter by status and date range
Given I am viewing my leave request list
When I filter by status=APPROVED and date range 2026-01-01 to 2026-06-30
Then only matching requests are shown
Règles OHADA & Réglementaires¶
- Aucune règle OHADA spécifique pour l'affichage de la liste/du détail. Les données des demandes ont été créées et validées conformément aux règles OHADA dans AB-010.
- Les transitions de statut suivent la machine à états définie dans AB-003.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §REST controller patterns, §Cursor-based paginationdocs/standards/database-guidelines.md— §Keyset pagination pattern, §Multi-tenant repository patterndocs/standards/api-guidelines.md— §GET endpoint patterns, §ApiResponse envelope, §Pagination response formatdocs/standards/react-typescript-guidelines.md— §Component patterns, §CSS Modules, §i18n, §Infinite scrolldocs/standards/design-system.md— §Badge, §Card, §EmptyState, §DataTable, §Shimmer loadingdocs/standards/offline-sync-guidelines.md— §IndexedDB caching for read-only data, §Offline banner pattern
Exigences de test¶
Tests unitaires¶
LeaveRequestQueryService.getMyRequests(): rôle EMPLOYEE → retourne uniquement ses propres demandesLeaveRequestQueryService.getRequests(): rôle MANAGER → retourne les collaborateurs directs + soi-mêmeLeaveRequestQueryService.getRequests(): rôle HR_MANAGER → retourne toutes les demandes du tenantLeaveRequestQueryService.getRequestDetail(): EMPLOYEE accédant à sa propre demande → 200LeaveRequestQueryService.getRequestDetail(): EMPLOYEE accédant à la demande d'un autre → 403- Analyse du curseur : curseur base64 valide → offset correct appliqué
- Analyse du curseur : curseur invalide → 400
Tests d'intégration¶
- GET
/api/v1/absence/leave-requestsavec le rôle EMPLOYEE → retourne uniquement les siennes, 200 - GET
/api/v1/absence/leave-requests?status=APPROVED&from=2026-01-01&to=2026-06-30→ résultats filtrés - GET
/api/v1/absence/leave-requests?cursor={valid}→ page suivante des résultats - GET
/api/v1/absence/leave-requests/{id}avec le rôle correct → 200 avec la chronologie - Isolation multi-tenant : les demandes du tenant A ne sont pas visibles dans le tenant B
Ce que la QA validera¶
- Les cartes de demandes s'affichent à 360px regroupées par sections de statut
- Appuyer sur une carte navigue vers la vue détaillée avec les données correctes
- Le défilement infini charge la page suivante sans interruption
- La vue détaillée affiche la chronologie avec l'historique des statuts
- Le détail REJECTED affiche le motif du refus dans une carte rouge
- Desktop : DataTable avec colonnes triables et onglets
- Hors connexion : les demandes en cache s'affichent avec la bannière "Hors connexion"
- Les demandes DRAFT (de la création hors connexion) affichent une icône de synchronisation en attente
- Nouvelle demande après soumission du formulaire apparaît avec animation d'entrée
Hors périmètre¶
- Création de demandes (AB-010)
- Annulation de demandes (AB-013)
- Actions d'approbation (AB-012)
- Téléversement de document (AB-014)
- Vue calendrier d'équipe du manager (détail AB-012)
Définition de Done¶
- [ ] Tous les endpoints backend implémentés et retournant les réponses correctes
- [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px, 1024px
- [ ] Les 5 états UI implémentés (loading, empty, error, offline, success)
- [ ] Le micro-copy français correspond exactement au spec design
- [ ] Tous les critères d'acceptation passent manuellement
- [ ] Tests unitaires écrits et passants
- [ ] Tests d'intégration écrits et passants
- [ ] Isolation multi-tenant vérifiée — demandes filtrées par le contexte tenant, RBAC appliqué
- [ ] Offline write+sync fonctionne (Papillon) / N/A (Enterprise) — Liste des demandes en cache dans IndexedDB, bannière hors connexion, cartes DRAFT avec icône de synchronisation
- [ ] Pas de
anyTypeScript - [ ] Pas de types raw Java — tous les generics explicites
- [ ] Les réponses API suivent le format envelope standard
- [ ] Entrées de piste d'audit pour chaque mutation de données — N/A (story en lecture seule)