Story AB-012 : Approbation & Refus de congés (Backend + Frontend)¶
@assignee: @joel23p
Module : absence-management Slice : VS-AB-006 + VS-AB-008 de l'architecture Contexte produit : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priorité : 12 Dépend de : AB-010 Peut être développé en parallèle avec : AB-011, AB-013 Ordre de merge : Après AB-010 Complexité estimée : M PRD User Stories : US-ABS-002
Objectif¶
Permettre aux managers, responsables RH et DG d'approuver ou de refuser les demandes de congés en attente. L'approbation déclenche une déduction de solde avec verrou pessimiste (SELECT FOR UPDATE sur leave_employee_profile, insertion d'une entrée LEAVE_DEBIT dans le grand livre, publication de LeaveApproved). Le refus nécessite un motif obligatoire et publie une notification.
Périmètre Backend¶
Entités & Enregistrements¶
Enregistrements de demande :
Événements publiés :
public record LeaveApproved(
UUID tenantId,
UUID employeeId,
UUID leaveRequestId,
LeaveType leaveType,
LocalDate startDate,
LocalDate endDate,
BigDecimal numberOfDays, // jours calendaires
BigDecimal numberOfDaysOuvrables, // source of truth for balance deduction
boolean isPaid,
BigDecimal payRate, // 100 for full pay, 50 for half, 0 for unpaid
@Nullable SickLeavePhase sickLeavePhase, // FULL_PAY|HALF_PAY|ZERO_PAY|null (only for SICK)
boolean halfDayStart,
boolean halfDayEnd
) {}
Couche Repository¶
Utilise les repositories existants d'AB-002 :
- LeaveRequestRepository.findByIdAndTenantId(UUID id, UUID tenantId)
- LeaveEmployeeProfileRepository.findByTenantIdAndEmployeeIdForUpdate(UUID tenantId, UUID employeeId) — SELECT FOR UPDATE
- LeaveBalanceLedgerRepository.save(LeaveBalanceLedger entry)
Couche Service¶
LeaveRequestService.java — approveRequest(UUID requestId) :
1. Charger la demande, vérifier que le statut == PENDING_APPROVAL
2. Vérifier que l'appelant a le rôle MANAGER (collaborateur direct) ou HR_MANAGER/DG
3. Verrouiller la ligne LeaveEmployeeProfile via SELECT FOR UPDATE (verrou pessimiste)
4. Revalider le solde >= numberOfDays (le solde peut avoir changé depuis la création de la demande)
5. Insérer une entrée LEAVE_DEBIT dans LeaveBalanceLedger
6. Mettre à jour le solde en cache sur LeaveEmployeeProfile (décrémentation)
7. Incrémenter LeaveEmployeeProfile.version (vérification de la concurrence optimiste)
8. Mettre à jour le statut de la demande à APPROVED, définir approvedAt, approverId
9. Publier LeaveApproved (pour la paie, les notifications)
10. Retourner la réponse mise à jour
LeaveRequestService.java — rejectRequest(UUID requestId, String rejectionReason) :
1. Charger la demande, vérifier que le statut == PENDING_APPROVAL
2. Vérifier que l'appelant a le rôle MANAGER (collaborateur direct) ou HR_MANAGER/DG
3. Valider que rejectionReason n'est pas vide
4. Mettre à jour le statut de la demande à REJECTED, définir rejectionReason, rejectedAt, rejectorId
5. Publier un événement de notification (l'employé voit le motif du refus)
6. Retourner la réponse mise à jour
BalanceMutationService.java :
- deductBalance(UUID tenantId, UUID employeeId, BigDecimal days) : pattern de verrou pessimiste
- Utilise SELECT FOR UPDATE sur la ligne leave_employee_profile
- Incrémentation de version pour la détection d'approbation concurrente
- Deuxième approbation concurrente → 409 Conflict (version mismatch)
Endpoints API¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| PATCH | /api/v1/absence/leave-requests/{id}/approve |
{} |
LeaveRequestResponse |
MANAGER (direct reports), HR_MANAGER, DG |
| PATCH | /api/v1/absence/leave-requests/{id}/reject |
RejectLeaveRequestRequest |
LeaveRequestResponse |
MANAGER (direct reports), HR_MANAGER, DG |
Règles de validation¶
- Seules les demandes avec le statut
PENDING_APPROVALpeuvent être approuvées/refusées - Impossible d'approuver si un document justificatif est requis mais manquant (statut
PENDING_DOCUMENT) → 422 - Le motif de refus est obligatoire (non vide) → 422
"Veuillez indiquer le motif du refus." - Approbations concurrentes : la deuxième approbation reçoit un 409 Conflict (version mismatch après verrouillage)
- MANAGER peut uniquement approuver/refuser les demandes de ses collaborateurs directs
- Revalidation du solde à l'approbation : si le solde est maintenant insuffisant → 422
Considérations multi-tenant¶
- Toutes les requêtes incluent
tenantIddepuisScopedValue<TenantContext> - Le verrou pessimiste (
SELECT FOR UPDATE) est filtré par tenant LeaveApprovedincluttenantIdpour les consommateurs en aval- Fonctionne sur les 4 profils DB
Périmètre Frontend¶
RAPPEL OFFLINE SYNC (Papillon) : Cette story implique des mutations de données (approbation/refus). Les actions d'approbation et de refus NÉCESSITENT une connexion internet active — elles NE PEUVENT PAS être effectuées hors connexion car elles impliquent des verrous pessimistes et une déduction de solde. Hors connexion : afficher la liste depuis le cache mais désactiver les boutons approbation/refus avec l'infobulle "Cette action nécessite une connexion internet". Voir
docs/standards/offline-sync-guidelines.md.
Pages & Routes¶
| Route | Page | Description |
|---|---|---|
/absences/approbations |
PendingApprovalsPage | Liste des demandes de congés en attente d'approbation |
/absences/approbations/{id} |
ApprovalDetailPage | Détail d'une demande individuelle avec actions approbation/refus |
Composants¶
S4 — Liste des approbations en attente :
- ApprovalCard — Carte affichant le nom de l'employé, l'icône du type de congé, les dates, le nombre de jours, le solde, le temps écoulé depuis la soumission
- InlineApprovalButtons — "Approuver" (primary) + "Rejeter" (ghost) en ligne sur la carte
- SwipeableCard — Glisser à droite=approuver (fond vert), glisser à gauche=refuser (fond rouge, ouvre un BottomSheet)
- SwipeHint — Superposition d'indication affichée uniquement la première fois
S5 — Détail de l'approbation :
- ApprovalDetailCard — Nom de l'employé, type, dates, durée, solde actuel, solde après approbation, motif
- MiniTeamCalendar — Chronologie intégrée montrant qui d'autre est absent pendant la période demandée
- DocumentIndicator — Indique si un document est joint
- StickyActionBar — Barre inférieure avec "Rejeter" (ghost) + "Approuver" (primary)
- RejectionBottomSheet — BottomSheet avec textarea de motif obligatoire + bouton de confirmation
- RejectionModal — Variante desktop du flux de refus (Modal à la place du BottomSheet)
États UI (TOUS OBLIGATOIRES)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | 3 card shimmer rectangles | — |
| Empty (no pending) | EmptyState with checkmark illustration | "Toutes les demandes ont ete traitees !" / CTA: "Voir le calendrier d'equipe" |
| Error (API failure) | Toast | "Impossible de charger les approbations." |
| Offline | List from cache, approve/reject buttons disabled | "Cette action necessite une connexion internet" |
| Success (approve) | Card flashes green + slides out + Toast | "Conge approuve" |
| Success (reject) | Card flashes red + slides out + Toast | "Demande rejetee" |
Comportement responsive¶
Mobile (360px — priorité Papillon) : - Liste de cartes avec boutons approbation/refus en ligne - Gestes de glissement : droite=approuver (révèle un fond vert), gauche=refuser (révèle un fond rouge, ouvre le BottomSheet) - Indication de glissement affichée uniquement la première fois (persistée dans localStorage) - Détail de l'approbation : plein écran avec barre d'action épinglée en bas - Flux de refus : BottomSheet avec textarea de motif obligatoire + "Confirmer le refus" (bouton danger, désactivé jusqu'à saisie du texte) + "Annuler" (ghost) - Cibles de toucher >= 48px
Desktop (1024px+ — priorité Enterprise) : - DataTable avec colonnes : Nom, Type, Dates, Jours, Solde, Actions (icônes approuver/refuser/détail) - Si document manquant : bouton approuver désactivé, affiche une icône cadenas avec infobulle - Flux de refus : Modal (500px de large) à la place du BottomSheet - Vue détaillée dans un panneau latéral
Interactions¶
- Appuyer sur "Approuver" → approbation immédiate, la carte clignote en vert + glisse hors de l'écran
- Appuyer sur "Rejeter" → ouvre le BottomSheet/Modal pour le motif obligatoire
- Glisser à droite → approuver (avec retour haptique sur mobile)
- Glisser à gauche → refuser (ouvre le BottomSheet)
- Vue détaillée : mini calendrier d'équipe affichant les conflits
- Document manquant → bouton approuver désactivé avec icône cadenas
Micro-copy français¶
approval.approve: "Approuver"approval.reject: "Rejeter"approval.reason_label: "Motif du refus"approval.reason_placeholder: "Expliquez la raison du refus..."approval.reason_helper: "L'employé recevra ce motif dans sa notification."approval.confirm_reject: "Confirmer le refus"approval.doc_missing: "En attente du justificatif"approval.swipe_hint: "Glissez pour approuver ou rejeter"approval.team_impact: "{n} autre(s) personne(s) absente(s) sur cette période"error.rejection_required: "Veuillez indiquer le motif du refus."
Critères d'acceptation¶
AC-039: Manager sees full request context for approval
Given a leave request is pending
When I view it
Then I see: employee name, leave type, dates, number of days, employee's current balance, team calendar showing who else is off during that period
AC-040: Rejection requires mandatory reason
Given I reject a request
When I submit the rejection
Then I must provide a reason (mandatory text field) and the employee is notified with the reason
AC-041: Document check blocks approval
Given the leave type requires a supporting document (sick leave, maternity)
When the document is not uploaded
Then I cannot approve — the approve button is disabled with explanation
Règles OHADA & Réglementaires¶
- FR-ABS-060 (CT Art. 25.13) : L'approbation suit la hiérarchie de l'unité organisationnelle — le manager direct approuve, HR_MANAGER et DG peuvent approuver n'importe quelle demande.
- FR-ABS-061 (CT Art. 25.14) : Mécaniques d'approbation ou de refus — l'approbation déclenche la déduction du solde ; le refus nécessite un motif communiqué à l'employé.
- FR-ABS-052 (CT Art. 25.15) : Visibilité des documents — l'approbateur voit uniquement le nom du fichier ; HR/DG peuvent consulter le contenu du document.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §REST controller patterns, §Pessimistic locking, §Application Events, §Transaction boundariesdocs/standards/database-guidelines.md— §SELECT FOR UPDATE pattern, §BigDecimal for financial calculations, §Version-based optimistic concurrencydocs/standards/api-guidelines.md— §PATCH endpoint patterns, §ApiResponse envelope, §409 Conflict handlingdocs/standards/react-typescript-guidelines.md— §Component patterns, §CSS Modules, §i18n, §Gesture handlingdocs/standards/design-system.md— §BottomSheet, §Button, §Badge, §Toast, §DataTable, §Modaldocs/standards/offline-sync-guidelines.md— §Actions requiring connectivity, §Disabled state when offline
Exigences de test¶
Tests unitaires¶
LeaveRequestService.approveRequest(): PENDING_APPROVAL valide → APPROVED, entrée de grand livre créée, événement publiéLeaveRequestService.approveRequest(): status != PENDING_APPROVAL → 422LeaveRequestService.approveRequest(): PENDING_DOCUMENT (document manquant) → 422LeaveRequestService.approveRequest(): solde maintenant insuffisant → 422LeaveRequestService.approveRequest(): approbation concurrente (version mismatch) → 409LeaveRequestService.rejectRequest(): refus valide avec motif → REJECTED, notification envoyéeLeaveRequestService.rejectRequest(): motif vide → 422BalanceMutationService.deductBalance(): déduction correcte avec verrou pessimiste- RBAC : MANAGER peut uniquement approuver ses collaborateurs directs, pas d'autres équipes
Tests d'intégration¶
- PATCH
/api/v1/absence/leave-requests/{id}/approve→ 200, solde décrémenté en base - PATCH
/api/v1/absence/leave-requests/{id}/rejectavec motif → 200 - PATCH
/api/v1/absence/leave-requests/{id}/rejectsans motif → 422 - Approbation concurrente : deux PATCH approve simultanés → le premier réussit, le second reçoit 409
- Isolation multi-tenant : le manager du tenant A ne peut pas approuver une demande du tenant B
Ce que la QA validera¶
- La liste des approbations en attente s'affiche à 360px avec les boutons en ligne
- Glisser à droite approuve avec animation de clignotement vert
- Glisser à gauche ouvre le BottomSheet pour le motif du refus
- BottomSheet de refus : le bouton de confirmation est désactivé jusqu'à saisie du motif
- La vue détaillée affiche le mini calendrier d'équipe avec les absences qui se chevauchent
- Document manquant : bouton approuver désactivé avec icône cadenas et infobulle
- Hors connexion : boutons désactivés avec message de connexion
- Desktop : DataTable avec colonnes triables et flux de refus en modal
Hors périmètre¶
- Auto-approbation pour les DG (future)
- Chaînes d'approbation multi-niveaux (V2)
- Approbation groupée de plusieurs demandes (Enterprise V2)
- Création de demandes (AB-010)
- Annulation de demandes (AB-013)
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 — approbation filtrée par tenant, RBAC appliqué
- [ ] Offline write+sync fonctionne (Papillon) / N/A (Enterprise) — Approbation/refus désactivés hors connexion, liste en cache
- [ ] 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 — LeaveApproved publié, entrée de grand livre LEAVE_DEBIT, motif du refus enregistré