Story AB-010 : Création de demande de congés — Annuels & Sans solde (API Backend + Formulaire Frontend)¶
@assignee: @joel23p
Module : absence-management Slice : VS-AB-005 de l'architecture Contexte produit : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priorité : 10 Dépend de : AB-003, AB-005, AB-009 Peut être développé en parallèle avec : AB-011 Ordre de merge : Après AB-009 Complexité estimée : M PRD User Stories : US-ABS-001, US-ABS-007
Objectif¶
Permettre aux employés de soumettre des demandes de congés annuels et sans solde via l'API backend et le formulaire frontend (S2 — variante formulaire simple), incluant la création de demandes hors connexion avec synchronisation automatique, la validation côté client avec avertissements doux (préavis de 15 jours, bloc de 14 jours consécutifs) et les blocages durs (solde insuffisant, moins de 12 mois d'ancienneté).
Périmètre Backend¶
Entités & Enregistrements¶
Enregistrement de demande :
public record CreateLeaveRequestRequest(
@NotNull LeaveType leaveType, // ANNUAL or UNPAID (this story)
@NotNull LocalDate startDate,
@NotNull LocalDate endDate,
boolean halfDayStart, // afternoon only
boolean halfDayEnd, // morning only
@Size(max = 500) String reason
) {}
Enregistrement de réponse :
public record LeaveRequestResponse(
UUID id,
LeaveType leaveType,
LeaveRequestStatus status,
LocalDate startDate,
LocalDate endDate,
boolean halfDayStart,
boolean halfDayEnd,
BigDecimal numberOfDays,
String reason,
boolean isRetroactive,
List<SoftWarning> warnings, // soft warnings (not blocking)
Instant createdAt
) {}
public record SoftWarning(
String code, // SHORT_NOTICE, NO_14_DAY_BLOCK
String message // French human-readable message
) {}
Couche Repository¶
Utilise les repositories existants d'AB-002 :
- LeaveRequestRepository.save(LeaveRequest request)
- LeaveRequestRepository.findOverlappingRequests(UUID tenantId, UUID employeeId, LocalDate start, LocalDate end)
- LeaveEmployeeProfileRepository.findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId)
Couche Service¶
LeaveRequestService.java — createRequest() :
1. Résoudre l'approbateur via ApproverResolutionService (basé sur l'unité organisationnelle, utilise manager_id depuis CachedEmployeeData)
2. Valider via LeaveValidationService
3. Calculer les jours via DayComputationService (jours ouvrables, exclure les jours fériés, gérer les demi-journées)
4. Définir le statut à PENDING_APPROVAL (aucun document requis pour ANNUAL/UNPAID)
5. Définir isRetroactive = true si startDate < today (FR-ABS-043)
6. Publier LeaveRequestCreated
7. Retourner la réponse avec les éventuels avertissements doux
LeaveValidationService.java :
- Blocages durs (retourne 422) :
- FR-ABS-040 : ANNUAL uniquement après 12 mois d'ancienneté
- FR-ABS-041 : solde ANNUAL >= jours demandés
- FR-ABS-046 : chevauchement avec un congé approuvé du même type de priorité
- startDate > endDate → 422
- Avertissements doux (retournés dans la réponse, NON bloquants) :
- FR-ABS-044 : startDate < today + 15 days → avertissement de préavis court
- FR-ABS-045 : aucun bloc de 14 jours consécutifs pris cette période → rappel
ApproverResolutionService.java :
- Résout l'approbateur depuis CachedEmployeeData.managerId (routage basé sur l'unité organisationnelle)
- Repli sur HR_MANAGER si aucun manager direct configuré
DayComputationService.java :
- Calcule les jours ouvrables entre les dates de début et de fin
- Exclut les jours fériés (depuis PublicHolidayRepository)
- Gère les demi-journées : soustrait 0.5 pour halfDayStart et/ou halfDayEnd (FR-ABS-031)
Endpoints API¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| POST | /api/v1/absence/leave-requests |
CreateLeaveRequestRequest |
LeaveRequestResponse (201) |
EMPLOYEE (self), HR_MANAGER (any) |
Règles de validation¶
- FR-ABS-040 : ANNUAL uniquement après 12 mois d'ancienneté → 422
"Vous devez completer 12 mois de service avant de prendre un conge annuel." - FR-ABS-041 : solde ANNUAL >= jours demandés → 422
"Solde insuffisant. Votre solde disponible est de {balance} jours." - FR-ABS-043 : indicateur rétroactif si
startDate < today - FR-ABS-044 :
startDate < today + 15 days→ avertissement doux (non bloquant) - FR-ABS-045 : aucun bloc de 14 jours cette période → avertissement doux (non bloquant)
- FR-ABS-046 : chevauchement avec un congé approuvé → 422 bloqué pour les types de même priorité
- Tous les calculs de jours utilisent
BigDecimalavecRoundingMode.HALF_UP,scale(4)
Considérations multi-tenant¶
- Toutes les requêtes incluent
tenantIddepuisScopedValue<TenantContext> - La consultation des jours fériés est filtrée par tenant (les tenants peuvent avoir des jours fériés personnalisés)
- La résolution de l'approbateur utilise la structure organisationnelle du tenant
- Fonctionne sur les 4 profils DB
Périmètre Frontend¶
RAPPEL OFFLINE SYNC (Papillon — CRITIQUE) : Cette story implique des mutations de données. Le formulaire DOIT fonctionner identiquement hors connexion. À la soumission hors connexion : enregistrer dans IndexedDB avec le statut DRAFT en attente de synchronisation. À la reconnexion : le Service Worker soumet automatiquement via POST. Si la validation serveur échoue : notification push avec la raison + lien pour modifier. Sauvegarde automatique : chaque changement de champ est persisté dans IndexedDB — le formulaire survit au redémarrage de l'application, à la fermeture de l'onglet, aux crashes. Voir
docs/standards/offline-sync-guidelines.mdpour le pattern complet offline write+sync.
Pages & Routes¶
| Route | Page | Description |
|---|---|---|
/absences/demande |
LeaveRequestPage | Formulaire de création de demande de congés (variante simple pour ANNUAL/UNPAID) |
Composants¶
LeaveRequestForm— Conteneur principal du formulaire avec logique de sauvegarde automatiqueSelect— Sélecteur de type (ANNUAL, UNPAID pour cette story)DatePicker— Sélecteurs de date de début et de fin (côte à côte sur mobile)Checkbox— Bascules demi-journée (affichées après le remplissage des dates)DayComputationCard— Affichage des jours calculés avec fondprimary-subtleInlineBanner— Bannières d'avertissement doux (amber)Input(textarea) — Champ motif (optionnel)Button— Bouton de soumission (primary, lg, fullWidth)AutoSaveIndicator— Légende affichant l'état de sauvegarde du brouillon
États UI (TOUS OBLIGATOIRES)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | N/A (form is empty initially) | — |
| Empty | Form with placeholders | "Choisir un type de conge" |
| Error (hard block) | Red inline error below field | "Solde insuffisant. Votre solde disponible est de {balance} jours." / "Vous devez completer 12 mois de service..." |
| Error (server) | Toast | "Impossible d'envoyer la demande. Reessayez." |
| Warning (soft) | Amber InlineBanner below trigger field | "Attention : le preavis legal est de 15 jours avant le debut du conge." / "Rappel : la loi impose un conge principal d'au moins 14 jours ouvrables consecutifs par an." |
| Offline | Form works identically, saved as DRAFT | "Demande enregistree -- sera envoyee automatiquement" |
| Success | Check animation + Toast + navigate to /absences |
"Demande envoyee !" |
Comportement responsive¶
Mobile (360px — priorité Papillon) :
- Formulaire défilable plein écran
- Sélecteur de type → sélecteurs de date (côte à côte) → cases à cocher demi-journée (affichées après le remplissage des dates) → carte de calcul des jours (fond primary-subtle) → bannières d'avertissement doux (amber) → textarea motif → bouton de soumission (primary, lg, fullWidth)
- Légende de sauvegarde automatique en bas
- Cibles de toucher >= 48px
Desktop (1024px+ — priorité Enterprise) : - Modal (600px de large) ou panneau latéral - Même flux de formulaire, espacement plus compact
Interactions¶
- La sélection du type renseigne les plages de dates disponibles
- Le remplissage des deux dates déclenche le calcul des jours (immédiat, côté client)
- Les cases à cocher demi-journée apparaissent uniquement après que les deux dates sont définies
- La carte de calcul des jours se met à jour en temps réel au changement des dates/demi-journées
- Affiche
{n} jours calendaires+Solde après demande : {n} JC+dont {n} jour(s) férié(s) non compté(s) - Soumission : validation côté client d'abord, puis POST. En cas de succès : animation de coche + toast + navigation
- Sauvegarde automatique : chaque changement de champ → IndexedDB. Le formulaire survit au redémarrage de l'application
Micro-copy français¶
form.type_label: "Type de congé"form.type_placeholder: "Choisir un type de congé"form.start_date: "Date de début"form.end_date: "Date de fin"form.half_day_start: "Demi-journée début (après-midi uniquement)"form.half_day_end: "Demi-journée fin (matin uniquement)"form.reason: "Motif (optionnel)"form.reason_placeholder: "Décrivez la raison de votre demande..."form.days_computed: "{n} jours calendaires"form.balance_after: "Solde après demande : {n} JC"form.holiday_excluded: "dont {n} jour(s) férié(s) non compté(s)"form.submit: "Envoyer la demande"form.autosave: "Brouillon sauvegardé automatiquement"info.retroactive: "Demande rétroactive — sera signalée dans l'historique d'audit."
Critères d'acceptation¶
AC-032: Employee can view balance and submit leave request
Given I am an employee with an active account
When I open the Absence module
Then I see my current leave balance (by type, in jours calendaires)
AC-033: Offline request creation with auto-sync
Given I fill out a leave request form offline
When connectivity returns
Then the request is auto-submitted to my manager for approval
AC-034: Soft warning for short notice period
Given I submit a request
When the start date is < 15 days from today
Then I see a soft warning about the 15-day notice period but can still submit
AC-035: Soft warning for 14-day consecutive block
Given I submit a request for annual leave and have NOT yet taken a 14-consecutive-day block this year
Then I see a soft warning about the legal 14-day minimum requirement
Règles OHADA & Réglementaires¶
- FR-ABS-030 (CT Art. 25.9) : L'employé soumet sa demande de congé à son manager direct via la hiérarchie de l'unité organisationnelle.
- FR-ABS-031 (CT Art. 25.10) : Calcul des demi-journées — le premier jour l'après-midi uniquement ou le dernier jour le matin uniquement déduit 0.5 du total des jours.
- FR-ABS-040 (CT Art. 25.7) : Les congés ANNUELS nécessitent 12 mois d'ancienneté continue. Blocage dur.
- FR-ABS-041 : Le solde doit être suffisant pour le nombre de jours demandés. Blocage dur.
- FR-ABS-043 : Les demandes rétroactives (start_date < today) sont signalées dans la piste d'audit.
- FR-ABS-044 (CT Art. 25.11) : Un préavis de 15 jours est recommandé. Avertissement doux uniquement.
- FR-ABS-045 (CCI Art. 25.12) : Au moins un bloc de 14 jours ouvrables consécutifs par période de référence. Rappel par avertissement doux.
- FR-ABS-046 : Le chevauchement avec un congé approuvé de même priorité est bloqué. Blocage dur.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §REST controller patterns, §Validation, §Application Eventsdocs/standards/database-guidelines.md— §BigDecimal for financial calculations, §Multi-tenant repository patterndocs/standards/api-guidelines.md— §POST endpoint patterns, §ApiResponse envelope, §Validation error formatdocs/standards/react-typescript-guidelines.md— §Form patterns, §CSS Modules, §i18n, §Error handlingdocs/standards/design-system.md— §Select, §DatePicker, §Button, §InlineBanner, §Toastdocs/standards/offline-sync-guidelines.md— §Offline form submission, §IndexedDB auto-save, §Service Worker sync, §Conflict resolution
Exigences de test¶
Tests unitaires¶
LeaveValidationService: ANNUAL + <12 mois d'ancienneté → 422 avec le message français correctLeaveValidationService: ANNUAL + solde insuffisant → 422 avec le solde dans le messageLeaveValidationService: start_date < today+15 → avertissement doux retourné, non bloquantLeaveValidationService: aucun bloc de 14 jours cette période → avertissement doux retournéLeaveValidationService: chevauchement avec un congé approuvé → 422DayComputationService: 2026-03-02 au 2026-03-13 → jours ouvrables corrects moins les jours fériésDayComputationService: demi-journée début + demi-journée fin → soustrait correctement 1.0 du totalApproverResolutionService: employé avec manager → retourne manager_idApproverResolutionService: employé sans manager → repli sur HR_MANAGERLeaveRequestService.createRequest(): demande valide → status=PENDING_APPROVAL, événement publié
Tests d'intégration¶
- POST
/api/v1/absence/leave-requestsavec une demande ANNUAL valide → 201 avec la réponse correcte - POST avec solde insuffisant → 422 avec l'erreur de validation dans l'envelope ApiResponse
- POST avec le rôle EMPLOYEE pour sa propre demande → 201
- POST avec le rôle EMPLOYEE pour un autre employé → 403
- Multi-tenant : la demande créée dans le tenant A n'est pas visible dans le tenant B
Ce que la QA validera¶
- Le formulaire s'affiche à 360px avec l'ordre des champs et l'espacement corrects
- Les cases à cocher demi-journée apparaissent uniquement après le remplissage des dates
- La carte de calcul des jours se met à jour en temps réel
- Les avertissements doux apparaissent comme bannières amber, sans bloquer la soumission
- Les blocages durs affichent des erreurs en ligne rouges, désactivent la soumission
- Hors connexion : le formulaire fonctionne, enregistré comme DRAFT, synchronisation automatique à la reconnexion
- Sauvegarde automatique : fermer l'onglet, rouvrir → état du formulaire préservé
- Succès : animation de coche + toast + navigation vers
/absences
Hors périmètre¶
- Variante formulaire congé maladie (AB-014)
- Variante formulaire permission exceptionnelle (AB-015)
- Variante formulaire congé maternité (AB-016)
- Téléversement de document (AB-014)
- Flux d'approbation (AB-012)
- Annulation de demande (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 — demandes filtrées par le contexte tenant
- [ ] Offline write+sync fonctionne (Papillon) / N/A (Enterprise) — Le formulaire enregistre un DRAFT hors connexion, synchronisation automatique à la reconnexion, sauvegarde automatique dans IndexedDB
- [ ] 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 — LeaveRequestCreated publié, indicateur rétroactif enregistré