Story AB-015 : Demande de permission exceptionnelle (Backend + Frontend)¶
@assignee: @joel23p
Module : absence-management Slice : VS-AB-005 (extension exceptional) de l'architecture Contexte produit : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priorité : 15 Dépend de : AB-010, AB-008 Peut être développé en parallèle avec : AB-014, AB-016 Ordre de merge : Après AB-010 et AB-008 Complexité estimée : M PRD User Stories : US-ABS-007
Objectif¶
Étendre le flux de création de demande de congés pour prendre en charge les demandes de permission exceptionnelle avec l'assistant en 2 étapes : l'étape 1 sélectionne le type d'événement (grandes cartes tactiles avec icônes affichant les durées CCI ou configurées par le tenant), l'étape 2 remplit automatiquement les dates et nécessite un document justificatif.
Périmètre Backend¶
Entités & Enregistrements¶
Enregistrement de demande (étend CreateLeaveRequestRequest d'AB-010) :
// Additional fields when leaveType=EXCEPTIONAL
public record CreateLeaveRequestRequest(
@NotNull LeaveType leaveType, // EXCEPTIONAL
@NotNull LocalDate startDate,
@NotNull LocalDate endDate, // auto-computed, read-only
boolean halfDayStart,
boolean halfDayEnd,
@Size(max = 500) String reason,
// Exceptional-specific:
@NotNull String eventTypeCode, // MARRIAGE_SELF, BIRTH_CHILD, etc.
UUID supportingDocumentId // optional at creation
) {}
Enums :
public enum ExceptionalEventType {
MARRIAGE_SELF, // Mariage du salarie — CCI min: 4 jours
MARRIAGE_CHILD, // Mariage d'un enfant — CCI min: 2 jours
BIRTH_CHILD, // Naissance d'un enfant — CCI min: 2 jours
DEATH_SPOUSE, // Deces du conjoint — CCI min: 5 jours
DEATH_CHILD, // Deces d'un enfant — CCI min: 5 jours
DEATH_PARENT, // Deces pere/mere — CCI min: 5 jours
DEATH_SIBLING, // Deces frere/soeur — CCI min: 3 jours
DEATH_IN_LAW, // Deces beau-pere/belle-mere — CCI min: 3 jours
MOVE // Demenagement — CCI min: 1 jour
}
Couche Repository¶
Utilise les repositories existants d'AB-002 + AB-008 :
- ExceptionalLeaveConfigRepository.findByTenantIdAndEventTypeCode(UUID tenantId, String eventTypeCode) — d'AB-008
- LeaveRequestRepository — sauvegarde/requête standard
- LeaveBalanceLedgerRepository — pour vérifier les jours exceptionnels utilisés cette période
Couche Service¶
LeaveRequestService.java — étend createRequest() pour le type EXCEPTIONAL :
1. Rechercher la durée dans ExceptionalLeaveConfig :
- Si le tenant a une configuration personnalisée pour ce type d'événement → utiliser durationDays du tenant
- Sinon → utiliser la durée CCI par défaut
2. Valider le plafond annuel : totalExceptionalTakenThisPeriod + numberOfDays <= 10 (FR-ABS-042, FR-ABS-PERM-02)
3. Calculer automatiquement endDate = startDate + duration (en jours ouvrables, en ignorant les week-ends et jours fériés)
4. Si supportingDocumentId EST NULL → statut = PENDING_DOCUMENT
5. Si supportingDocumentId N'EST PAS NULL → statut = PENDING_APPROVAL
6. Publier LeaveRequestCreated
ExceptionalLeaveConfigService.java (d'AB-008, utilisé ici) :
- getDurationForEvent(UUID tenantId, String eventTypeCode) : retourne la configuration du tenant ou la durée CCI par défaut
- validateMinimumDuration(String eventTypeCode, int configuredDays) : bloque en dessous du minimum CCI → 422
Endpoints API¶
Note : Utilise le même endpoint POST qu'AB-010 (POST /api/v1/absence/leave-requests), étendu pour gérer leaveType=EXCEPTIONAL avec eventTypeCode.
Pas de nouveaux endpoints. La liste des types d'événements et les durées sont servies par les endpoints de configuration d'AB-008.
Règles de validation¶
- FR-ABS-PERM-01 : Application du minimum CCI — le tenant ne peut pas configurer une durée inférieure au minimum CCI pour n'importe quel type d'événement → 422
"La duree minimale legale (CCI) pour cet evenement est de {X} jours." - FR-ABS-PERM-02 / FR-ABS-042 : Plafond annuel de 10 jours pour toutes les permissions exceptionnelles combinées → 422
"Vous avez atteint le plafond annuel de 10 jours de permissions exceptionnelles." eventTypeCodedoit être une valeur valide de l'enumExceptionalEventType→ 422endDateest calculée automatiquement — si fournie par le client, elle est ignorée (le serveur calcule)- Document justificatif requis — même logique PENDING_DOCUMENT qu'AB-014
Considérations multi-tenant¶
- La recherche de durée est filtrée par tenant : la configuration du tenant surpasse les valeurs CCI par défaut
- La vérification du plafond annuel interroge les entrées du grand livre dans la période de référence courante du tenant
- Toutes les requêtes incluent
tenantIddepuisScopedValue<TenantContext> - 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 de permission exceptionnelle DOIT fonctionner hors connexion. La liste des types d'événements et les durées CCI sont mises en cache dans IndexedDB au premier chargement. Les durées configurées par le tenant sont mises en cache à la synchronisation. Le formulaire enregistre un DRAFT dans IndexedDB à la soumission hors connexion. Les pièces jointes sont stockées comme blobs dans IndexedDB, téléversées à la reconnexion. Voir
docs/standards/offline-sync-guidelines.md.
Pages & Routes¶
Pas de nouvelles routes. Étend le formulaire de demande de congés existant d'AB-010.
| Route | Page | Description |
|---|---|---|
/absences/demande |
LeaveRequestPage (étendu) | Assistant en 2 étapes quand type=EXCEPTIONAL |
Composants¶
Étape 1 — Sélection de l'événement :
- StepIndicator — Indicateur d'étape (steps=2, current=1)
- EventTypeCard — Grande carte tactile avec icône, nom de l'événement et durée en jours
- EventTypeList — Grille de EventTypeCards (5 premières visibles, le reste derrière un expandeur)
- RemainingDaysHelper — Texte d'aide affichant "Restant cette année: {remaining}/10 jours"
- ExpanderButton — Bascule "Plus d'événements" pour les types d'événements restants
Étape 2 — Dates + Document :
- StepIndicator — Indicateur d'étape (steps=2, current=2)
- EventSummary — Carte de résumé de la sélection de l'étape 1 (icône, nom, durée)
- DatePicker — Date de début uniquement (date de fin calculée automatiquement, en lecture seule avec icône cadenas)
- AutoComputedDate — Affichage en lecture seule de la date de fin avec icône cadenas
- FileUpload — Zone de téléversement de document (d'AB-014, obligatoire pour les permissions exceptionnelles)
- Button — Bouton de soumission
États UI (TOUS OBLIGATOIRES)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Shimmer on event type cards | — |
| Empty | N/A (event types always available from CCI defaults) | — |
| Error (cap reached) | Inline error below event cards | "Vous avez atteint le plafond annuel de 10 jours de permissions exceptionnelles." |
| Error (validation) | Red inline below field | Various validation messages |
| Offline | Form works, saved as DRAFT | "Demande enregistree -- sera envoyee automatiquement" |
| Success | Check animation + Toast + navigate to /absences |
"Demande envoyee !" |
Comportement responsive¶
Mobile (360px — priorité Papillon) : - Étape 1 : plein écran, StepIndicator en haut, cartes de types d'événements en liste colonne unique - Chaque carte : icône (gauche) + nom de l'événement + durée en jours (droite). Cibles de toucher >= 48px - 5 premiers événements visibles, expandeur "Plus d'événements" pour les 4 restants - Texte d'aide "Restant cette année: {remaining}/10 jours" sous les cartes - Étape 2 : plein écran, StepIndicator en haut, résumé de l'étape 1, DatePicker de date de début, date de fin calculée automatiquement (lecture seule avec icône cadenas), zone FileUpload, bouton de soumission (primary, lg, fullWidth) - Légende sous le bouton de soumission : "Vous pouvez envoyer sans document. La demande restera en attente du justificatif."
Desktop (1024px+ — priorité Enterprise) : - Étape 1 : cartes de types d'événements en grille 2 colonnes dans une modal/panneau (600px de large) - Étape 2 : même flux de formulaire, espacement plus compact - Tous les types d'événements visibles (pas besoin d'expandeur à plus grande largeur)
Interactions¶
- Appuyer sur une carte de type d'événement → état sélectionné (superposition de coche) + le bouton "Suivant" apparaît
- Appuyer sur "Suivant" → glissement vers l'étape 2 avec animation vers la gauche
- Étape 2 : seule la date de début est modifiable. La date de fin est calculée automatiquement : "Fin calculée : début + {n} jours ouvrables"
- Appuyer sur "Retour" à l'étape 2 → retour vers l'étape 1 avec glissement, sélection préservée
- Téléversement de fichier : même comportement qu'AB-014 (tap → sélecteur de fichier, glisser-déposer sur desktop)
- Soumission : validation du plafond côté client, puis POST
Micro-copy français¶
exceptional.step1_title: "Quel événement ?"exceptional.step2_title: "Dates et justificatif"exceptional.remaining: "Restant cette année : {remaining}/10 jours"exceptional.more_events: "Plus d'événements"exceptional.next: "Suivant"exceptional.back: "Retour"exceptional.end_computed: "Fin calculée : début + {n} jours ouvrables"exceptional.doc_required: "Justificatif requis"exceptional.doc_optional_caption: "Vous pouvez envoyer sans document. La demande restera en attente du justificatif."exceptional.cap_reached: "Vous avez atteint le plafond annuel de 10 jours de permissions exceptionnelles."exceptional.cci_minimum_block: "La durée minimale légale (CCI) pour cet événement est de {X} jours."- Libellés des types d'événements :
event.MARRIAGE_SELF: "Mariage du salarié"event.MARRIAGE_CHILD: "Mariage d'un enfant"event.BIRTH_CHILD: "Naissance d'un enfant"event.DEATH_SPOUSE: "Décès du conjoint"event.DEATH_CHILD: "Décès d'un enfant"event.DEATH_PARENT: "Décès père/mère"event.DEATH_SIBLING: "Décès frère/sœur"event.DEATH_IN_LAW: "Décès beau-père/belle-mère"event.MOVE: "Déménagement"
Critères d'acceptation¶
AC-048: System pre-fills duration from CCI or tenant config
Given I select "Permission exceptionnelle" as leave type
When I choose the event type (mariage, naissance, deces, etc.)
Then the system pre-fills the number of days based on the CCI minimum for CI (or tenant's configured duration if higher)
AC-049: Tenant-configured durations override CCI
Given the tenant has configured more generous durations (above CCI minimum)
When I select the event type
Then the system pre-fills the tenant's duration
AC-050: CCI minimum blocks below-minimum configuration
Given the tenant tries to configure a duration below the CCI minimum
When they save
Then the system blocks with: "La duree minimale legale (CCI) pour cet evenement est de X jours."
Règles OHADA & Réglementaires¶
- FR-ABS-PERM-01 (CCI Art. 46) : Application du minimum CCI — chaque type d'événement a une durée minimale imposée par la CCI. Les tenants peuvent configurer des durées supérieures mais jamais inférieures. Le système bloque toute configuration en dessous du minimum.
- FR-ABS-PERM-02 / FR-ABS-042 (CCI Art. 46) : Plafond annuel de 10 jours ouvrables pour toutes les permissions exceptionnelles combinées par période de référence. Une fois atteint, aucune autre demande de permission exceptionnelle n'est acceptée.
- Durées CCI Art. 46 : Mariage salarié=4j, Mariage enfant=2j, Naissance=2j, Décès conjoint/enfant/parent=5j, Décès frère-sœur/beau-parent=3j, Déménagement=1j.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §REST controller patterns, §Enum handling, §Application Eventsdocs/standards/database-guidelines.md— §Multi-tenant repository pattern, §BigDecimal for day computationsdocs/standards/api-guidelines.md— §POST endpoint patterns, §ApiResponse envelope, §422 validation errorsdocs/standards/react-typescript-guidelines.md— §Component patterns, §CSS Modules, §i18n, §Multi-step form patternsdocs/standards/design-system.md— §Card (selectable variant), §StepIndicator, §DatePicker, §FileUpload, §Buttondocs/standards/offline-sync-guidelines.md— §Cached reference data (event types), §Offline form submission, §Blob storage in IndexedDB
Exigences de test¶
Tests unitaires¶
LeaveRequestService.createRequest(): EXCEPTIONAL + MARRIAGE_SELF → durée=4j depuis la valeur CCI par défautLeaveRequestService.createRequest(): EXCEPTIONAL + MARRIAGE_SELF + configuration tenant 6j → durée=6j (surcharge tenant)LeaveRequestService.createRequest(): total exceptionnel utilisé=8j + demande de 3j → 422 (plafond dépassé)LeaveRequestService.createRequest(): total exceptionnel utilisé=8j + demande de 2j → autorisé (exactement au plafond)ExceptionalLeaveConfigService.validateMinimumDuration(): configuré 2j pour MARRIAGE_SELF (CCI min=4j) → 422ExceptionalLeaveConfigService.validateMinimumDuration(): configuré 5j pour MARRIAGE_SELF (CCI min=4j) → autorisé- Calcul automatique de la date de fin : startDate=Lundi + 4 jours ouvrables → endDate=Jeudi (même semaine)
- Calcul automatique de la date de fin : startDate=Jeudi + 4 jours ouvrables → endDate=Mardi (ignore le week-end)
Tests d'intégration¶
- POST avec type EXCEPTIONAL + code d'événement valide → 201, durée depuis la configuration
- POST avec type EXCEPTIONAL + plafond dépassé → 422
- POST avec type EXCEPTIONAL + pas de document → 201, status=PENDING_DOCUMENT
- POST avec eventTypeCode invalide → 422
- Multi-tenant : la configuration du tenant A n'affecte pas les durées du tenant B
Ce que la QA validera¶
- Étape 1 : les cartes de types d'événements s'affichent à 360px avec icônes et durées
- 5 premiers événements visibles, l'expandeur "Plus d'événements" fonctionne
- Le texte d'aide "Restant cette année" se met à jour correctement
- Plafond atteint → erreur en ligne, cartes désactivées
- Étape 2 : sélecteur de date de début, date de fin calculée automatiquement et en lecture seule
- La date de fin affiche "Fin calculée : début + {n} jours ouvrables"
- La zone FileUpload apparaît avec le texte d'aide correct
- Soumettre sans document → toast PENDING_DOCUMENT
- Hors connexion : le formulaire fonctionne, types d'événements depuis le cache, enregistré comme DRAFT
- Desktop : grille 2 colonnes pour les types d'événements dans une modal
Hors périmètre¶
- Gestion de la configuration des permissions exceptionnelles (AB-008)
- Flux d'approbation (AB-012)
- Infrastructure de téléversement de documents (utilise l'API MinIO de la plateforme)
- Types d'événements personnalisés au-delà des 9 types définis par la CCI (V2)
- Permissions exceptionnelles en demi-journée (toujours des jours entiers selon la CCI)
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 — surcharges de configuration tenant correctement filtrées, vérification du plafond par tenant
- [ ] Offline write+sync fonctionne (Papillon) / N/A (Enterprise) — Types d'événements en cache, formulaire enregistre un DRAFT hors connexion, blobs 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é avec eventTypeCode