Aller au contenu

Story AB-009: Leave Balance View (Backend API + Frontend Employee Home)

Module: absence-management Slice: VS-AB-010 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 9 Depends on: AB-002, AB-004 Can develop concurrently with: AB-010, AB-011 Merge order: After AB-004 Estimated complexity: S PRD User Stories: US-ABS-001, US-ABS-006


Objective

Enable employees to view their current leave balance (annual, exceptional, sick counters) with breakdown (base + seniority + children - taken) via an API and the Employee Home screen (S1 balance card). This is the employee's primary entry point into the Absence module.


Backend Scope

Entities & Records

Records (read-only DTOs):

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
) {}

Repository Layer

Uses existing repositories from AB-002: - LeaveEmployeeProfileRepository.findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId)

Service Layer

BalanceQueryService.java: - getMyBalance(): reads cached balance from LeaveEmployeeProfile, returns in all 3 units - getEmployeeBalance(UUID employeeId, boolean includeHistory): same, for HR use; if includeHistory=true, includes ledger entries - Conversion formulas: calendaires = ouvrables * 1.25, ouvres = ouvrables * 5/6 - All calculations use BigDecimal with RoundingMode.HALF_UP, scale(4)

API Endpoints

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

Validation Rules

  • All BigDecimal calculations use RoundingMode.HALF_UP, scale(4)
  • tenant_id must be present in all queries (tenant isolation)
  • Employee can only access their own balance via /my-balance
  • HR/DG can access any employee's balance via /{employeeId}/balance

Multi-Tenant Considerations

  • Balance queries are scoped to the current tenant context via ScopedValue<TenantContext>
  • LeaveEmployeeProfile is always queried with tenantId filter
  • Works across all 4 DB profiles (shared, schema-per-tenant, DB-per-tenant, BYO-DB)

Frontend Scope

OFFLINE SYNC REMINDER (Papillon): This story is read-only, but balance data MUST be cached in IndexedDB for offline viewing. The Service Worker must cache the last successful /my-balance response. Display the "Hors connexion" banner with last sync time when offline.

Pages & Routes

Route Page Description
/absences AbsenceHome Employee home — balance card + request list (AB-011)

Components

  • BalanceCard — Hero number display showing available leave balance in jours calendaires
  • BalanceBreakdown — Expandable detail section showing base + seniority + children - taken = available
  • BalanceEquivalent — Small text showing equivalent in jours ouvrables

UI States (ALL REQUIRED)

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

Responsive Behavior

Mobile (360px — Papillon primary): - Balance card full-width with hero number (font-size-display) - Expandable breakdown below: "Acquis base : X JC / + Anciennete : +Y JC / + Enfants (N) : +Z JC / - Pris : -W JC / = Disponible : T JC" - Below breakdown: "+ Nouvelle demande" button (primary, lg, fullWidth) - Touch targets >= 48px on all interactive elements

Desktop (1024px+ — Enterprise primary): - Balance card + Quick Stats side by side in 2-column layout within sidebar - Breakdown always visible (not collapsed)

Interactions

  • Tap "Voir le detail" to expand/collapse breakdown (mobile only)
  • Tap "+ Nouvelle demande" navigates to /absences/demande (AB-010)
  • Pull-to-refresh reloads balance from API

French Micro-Copy

  • balance.title: "Votre solde"
  • balance.available: "jours calendaires"
  • balance.equivalent: "≡ {n} jours ouvrables"
  • balance.expand: "Voir le detail"
  • balance.base: "Acquis base"
  • balance.seniority: "Anciennete"
  • balance.children: "Enfants ({n})"
  • balance.taken: "Pris"
  • balance.total: "Disponible"
  • balance.not_available: "Disponible apres 12 mois de service"

Acceptance Criteria

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

OHADA & Regulatory Rules

  • FR-ABS-003 (CT Art. 25.2): Three-unit system — balance must be displayable in jours ouvrables, jours calendaires, and jours ouvres. Conversion: calendaires = ouvrables * 1.25, ouvres = ouvrables * 5/6.
  • FR-ABS-006 (CT Art. 25.7): 12-month restriction display — employees with less than 12 months of service see their accrued balance but with a restriction note. Balance is not usable until the 12-month threshold is met (unless tenant configures anticipated leave).

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

Testing Requirements

Unit Tests

  • BalanceQueryService.getMyBalance(): returns correct balance with all 3 units computed from ouvrables
  • BalanceQueryService.getMyBalance(): conversion formula — ouvrables=22 → calendaires=27.5, ouvres=18.3333
  • BalanceQueryService.getMyBalance(): employee < 12 months → restrictedUnder12Months=true, availableAfterDate populated
  • BalanceQueryService.getEmployeeBalance(): with includeHistory=true → includes ledger entries
  • BalanceQueryService.getEmployeeBalance(): EMPLOYEE role cannot access other employee's balance → 403

Integration Tests

  • GET /api/v1/absence/my-balance returns 200 with correct LeaveBalanceResponse structure
  • GET /api/v1/absence/employees/{id}/balance with HR_MANAGER role → 200
  • GET /api/v1/absence/employees/{id}/balance with EMPLOYEE role → 403
  • Multi-tenant isolation: employee in tenant A cannot see balance from tenant B

What QA Will Validate

  • Balance card renders correctly at 360px with hero number
  • Breakdown expands/collapses on mobile
  • New employee (<12m) sees amber banner with restriction message
  • Offline: cached balance displays with "Hors connexion" banner
  • Desktop layout: 2-column card + stats layout at 1024px+

Out of Scope

  • Request creation (AB-010)
  • Request history list (AB-011)
  • Ledger detail view for HR (AB-021)
  • Balance modification or manual adjustments
  • Balance export or reporting

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] All frontend pages render correctly at 360px, 768px, 1024px
  • [ ] All 5 UI states implemented (loading, empty, error, offline, success)
  • [ ] French micro-copy matches design spec exactly
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Multi-tenant isolation verified — balance queries scoped to tenant context
  • [ ] Offline write+sync works (Papillon) / N/A (Enterprise) — Cached balance in IndexedDB, offline banner displayed
  • [ ] No TypeScript any
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries for every data mutation — N/A (read-only story)