Aller au contenu

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_id doit ê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>
  • LeaveEmployeeProfile est toujours interrogé avec le filtre tenantId
  • 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-balance ré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 calendaires
  • BalanceBreakdown — Section de détail extensible montrant base + ancienneté + enfants - pris = disponible
  • BalanceEquivalent — 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 patterns
  • docs/standards/database-guidelines.md — §BigDecimal for financial calculations, §Multi-tenant repository pattern
  • docs/standards/api-guidelines.md — §GET endpoint patterns, §ApiResponse envelope
  • docs/standards/react-typescript-guidelines.md — §Component patterns, §CSS Modules, §i18n
  • docs/standards/design-system.md — §BalanceCard, §Shimmer loading, §EmptyState
  • docs/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 ouvrables
  • BalanceQueryService.getMyBalance() : formule de conversion — ouvrables=22 → calendaires=27.5, ouvres=18.3333
  • BalanceQueryService.getMyBalance() : employé < 12 mois → restrictedUnder12Months=true, availableAfterDate renseigné
  • BalanceQueryService.getEmployeeBalance() : avec includeHistory=true → inclut les entrées du grand livre
  • BalanceQueryService.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-balance retourne 200 avec la structure LeaveBalanceResponse correcte
  • GET /api/v1/absence/employees/{id}/balance avec le rôle HR_MANAGER → 200
  • GET /api/v1/absence/employees/{id}/balance avec 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 any TypeScript
  • [ ] 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)