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_idmust 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> LeaveEmployeeProfileis always queried withtenantIdfilter- 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-balanceresponse. 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 calendairesBalanceBreakdown— Expandable detail section showing base + seniority + children - taken = availableBalanceEquivalent— 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 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
Testing Requirements¶
Unit Tests¶
BalanceQueryService.getMyBalance(): returns correct balance with all 3 units computed from ouvrablesBalanceQueryService.getMyBalance(): conversion formula — ouvrables=22 → calendaires=27.5, ouvres=18.3333BalanceQueryService.getMyBalance(): employee < 12 months →restrictedUnder12Months=true,availableAfterDatepopulatedBalanceQueryService.getEmployeeBalance(): withincludeHistory=true→ includes ledger entriesBalanceQueryService.getEmployeeBalance(): EMPLOYEE role cannot access other employee's balance → 403
Integration Tests¶
- GET
/api/v1/absence/my-balancereturns 200 with correctLeaveBalanceResponsestructure - GET
/api/v1/absence/employees/{id}/balancewith HR_MANAGER role → 200 - GET
/api/v1/absence/employees/{id}/balancewith 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)