Story AB-009 : Consultation du solde de congés (API Backend + Accueil Employé Frontend)¶
Module : absence-management Slice : VS-AB-010 de l'architecture Contexte produit : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priorité : 9 Dépend de : AB-002, AB-004 Peut être développé en parallèle avec : AB-010, AB-011 Ordre de merge : Après AB-004 Complexité estimée : S PRD User Stories : US-ABS-001, US-ABS-006
Objectif¶
Permettre aux employés de consulter leur solde de congés actuel (compteurs annuels, exceptionnels, maladie) avec le détail (base + ancienneté + enfants - pris) via une API et l'écran d'accueil employé (carte de solde S1). C'est le point d'entrée principal de l'employé dans le module Absence.
Périmètre Backend¶
Entités & Enregistrements¶
Enregistrements (DTOs en lecture seule) :
public record LeaveBalanceResponse(
AnnualBalance annual,
ExceptionalBalance exceptional,
SickBalance sick
) {}
public record AnnualBalance(
BigDecimal ouvrables,
BigDecimal calendaires, // ouvrables * 1.25
BigDecimal ouvres, // ouvrables * 5/6
BigDecimal baseAccrued,
BigDecimal seniorityBonus,
BigDecimal childrenBonus,
BigDecimal taken,
BigDecimal available,
boolean restrictedUnder12Months,
LocalDate availableAfterDate // null if not restricted
) {}
public record ExceptionalBalance(
int usedThisPeriod,
int remaining,
int cap // 10
) {}
public record SickBalance(
int usedThisRolling12m,
String phase // FULL_PAY, HALF_PAY, ZERO_PAY
) {}
Couche Repository¶
Utilise les repositories existants d'AB-002 :
- LeaveEmployeeProfileRepository.findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId)
Couche Service¶
BalanceQueryService.java :
- getMyBalance() : lit le solde en cache depuis LeaveEmployeeProfile, retourne dans les 3 unités
- getEmployeeBalance(UUID employeeId, boolean includeHistory) : idem, pour l'usage RH ; si includeHistory=true, inclut les entrées du grand livre
- Formules de conversion : calendaires = ouvrables * 1.25, ouvres = ouvrables * 5/6
- Tous les calculs utilisent BigDecimal avec RoundingMode.HALF_UP, scale(4)
Endpoints API¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/absence/my-balance |
— | LeaveBalanceResponse |
EMPLOYEE (self), MANAGER, HR_MANAGER, DG |
| GET | /api/v1/absence/employees/{employeeId}/balance |
?includeHistory=true |
LeaveBalanceResponse + LedgerEntries |
HR_MANAGER, DG |
Règles de validation¶
- Tous les calculs BigDecimal utilisent
RoundingMode.HALF_UP,scale(4) tenant_iddoit être présent dans toutes les requêtes (isolation tenant)- L'employé peut accéder uniquement à son propre solde via
/my-balance - HR/DG peuvent accéder au solde de n'importe quel employé via
/{employeeId}/balance
Considérations multi-tenant¶
- Les requêtes de solde sont filtrées par le contexte tenant courant via
ScopedValue<TenantContext> LeaveEmployeeProfileest toujours interrogé avec le filtretenantId- Fonctionne sur les 4 profils DB (shared, schema-per-tenant, DB-per-tenant, BYO-DB)
Périmètre Frontend¶
RAPPEL OFFLINE SYNC (Papillon) : Cette story est en lecture seule, mais les données de solde DOIVENT être mises en cache dans IndexedDB pour une consultation hors connexion. Le Service Worker doit mettre en cache la dernière réponse
/my-balanceréussie. 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 | Accueil employé — carte de solde + liste des demandes (AB-011) |
Composants¶
BalanceCard— Affichage du chiffre principal indiquant le solde disponible en jours calendairesBalanceBreakdown— Section de détail extensible montrant base + ancienneté + enfants - pris = disponibleBalanceEquivalent— Petit texte affichant l'équivalent en jours ouvrables
États UI (TOUS OBLIGATOIRES)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | 1 large shimmer rectangle (balance card shape) | — |
| Empty (new employee <12m) | Shows accrued amount + amber InlineBanner | "Vos conges seront disponibles a partir du {date}. Vos jours s'accumulent." |
| Empty (0 balance, no requests) | Balance shows "0" + EmptyState below | "Vous n'avez pas encore de demande de conge" / CTA: "Faire ma premiere demande" |
| Error | Toast + retry button | "Impossible de charger vos conges. Reessayez." |
| Offline | Cached data from IndexedDB + subtle amber banner | "Hors connexion -- derniere synchro : {time}" |
| Success | Balance card with hero number | — |
Comportement responsive¶
Mobile (360px — priorité Papillon) :
- Carte de solde pleine largeur avec chiffre principal (font-size-display)
- Détail extensible en dessous : "Acquis base : X JC / + Ancienneté : +Y JC / + Enfants (N) : +Z JC / - Pris : -W JC / = Disponible : T JC"
- En dessous du détail : bouton "+ Nouvelle demande" (primary, lg, fullWidth)
- Cibles de toucher >= 48px sur tous les éléments interactifs
Desktop (1024px+ — priorité Enterprise) : - Carte de solde + Statistiques rapides côte à côte dans une disposition 2 colonnes avec barre latérale - Détail toujours visible (non réduit)
Interactions¶
- Appuyer sur "Voir le détail" pour étendre/réduire le détail (mobile uniquement)
- Appuyer sur "+ Nouvelle demande" navigue vers
/absences/demande(AB-010) - Glisser vers le bas pour actualiser recharge le solde depuis l'API
Micro-copy français¶
balance.title: "Votre solde"balance.available: "jours calendaires"balance.equivalent: "≡ {n} jours ouvrables"balance.expand: "Voir le détail"balance.base: "Acquis base"balance.seniority: "Ancienneté"balance.children: "Enfants ({n})"balance.taken: "Pris"balance.total: "Disponible"balance.not_available: "Disponible après 12 mois de service"
Critères d'acceptation¶
AC-029: View leave balance by type
Given I open the Absence module
When I view my balance
Then I see balances for each leave type in jours calendaires (my tenant's configured deduction unit)
AC-030: New employee under 12 months sees restricted balance
Given I am a new employee with < 12 months service
When I view my balance
Then I see my accrued balance with a note: "Disponible apres 12 mois de service" (unless anticipated leave is configured)
AC-031: Balance breakdown shows all components
Given my balance includes anciennete bonus days
When I view the breakdown
Then I see: base accrual + anciennete bonus + children bonus = total
Règles OHADA & Réglementaires¶
- FR-ABS-003 (CT Art. 25.2) : Système à trois unités — le solde doit pouvoir être affiché en jours ouvrables, jours calendaires et jours ouvrés. Conversion :
calendaires = ouvrables * 1.25,ouvres = ouvrables * 5/6. - FR-ABS-006 (CT Art. 25.7) : Affichage de la restriction 12 mois — les employés ayant moins de 12 mois d'ancienneté voient leur solde acquis mais avec une note de restriction. Le solde n'est pas utilisable tant que le seuil de 12 mois n'est pas atteint (sauf si le tenant configure des congés anticipés).
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §Records & sealed types, §REST controller patternsdocs/standards/database-guidelines.md— §BigDecimal for financial calculations, §Multi-tenant repository patterndocs/standards/api-guidelines.md— §GET endpoint patterns, §ApiResponse envelopedocs/standards/react-typescript-guidelines.md— §Component patterns, §CSS Modules, §i18ndocs/standards/design-system.md— §BalanceCard, §Shimmer loading, §EmptyStatedocs/standards/offline-sync-guidelines.md— §IndexedDB caching for read-only data, §Offline banner pattern
Exigences de test¶
Tests unitaires¶
BalanceQueryService.getMyBalance(): retourne le solde correct avec les 3 unités calculées à partir des ouvrablesBalanceQueryService.getMyBalance(): formule de conversion — ouvrables=22 → calendaires=27.5, ouvres=18.3333BalanceQueryService.getMyBalance(): employé < 12 mois →restrictedUnder12Months=true,availableAfterDaterenseignéBalanceQueryService.getEmployeeBalance(): avecincludeHistory=true→ inclut les entrées du grand livreBalanceQueryService.getEmployeeBalance(): rôle EMPLOYEE ne peut pas accéder au solde d'un autre employé → 403
Tests d'intégration¶
- GET
/api/v1/absence/my-balanceretourne 200 avec la structureLeaveBalanceResponsecorrecte - GET
/api/v1/absence/employees/{id}/balanceavec le rôle HR_MANAGER → 200 - GET
/api/v1/absence/employees/{id}/balanceavec le rôle EMPLOYEE → 403 - Isolation multi-tenant : un employé du tenant A ne peut pas voir le solde du tenant B
Ce que la QA validera¶
- La carte de solde s'affiche correctement à 360px avec le chiffre principal
- Le détail s'étend/se réduit sur mobile
- Le nouvel employé (<12 mois) voit la bannière amber avec le message de restriction
- Hors connexion : le solde en cache s'affiche avec la bannière "Hors connexion"
- Desktop : disposition 2 colonnes carte + stats à 1024px+
Hors périmètre¶
- Création de demandes (AB-010)
- Historique des demandes (AB-011)
- Vue détaillée du grand livre pour les RH (AB-021)
- Modification ou ajustements manuels du solde
- Export ou reporting du solde
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 — requêtes de solde filtrées par le contexte tenant
- [ ] Offline write+sync fonctionne (Papillon) / N/A (Enterprise) — Solde en cache dans IndexedDB, bannière hors connexion affichée
- [ ] 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)