Aller au contenu

Story AB-021: HR Employee Balance Detail (Backend + Frontend)

Module: absence-management Slice: VS-AB-010 (HR extension) + VS-AB-014 (dashboard link) from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 21 Depends on: AB-009 Concurrent with: AB-020 Merge order: Any order with AB-020 Estimated complexity: S PRD User Stories: US-ABS-008


Objective

Provide HR managers and DGs with a detailed view of any employee's leave balances, including the full ledger of accruals, deductions, adjustments, carry-overs, and sick leave phase tracking. Accessible from the HR Dashboard and team calendar. This is the primary HR tool for understanding an employee's leave history and current entitlements.


Backend Scope

Entities & Records

No new entities. This story extends the query layer over existing models from AB-009 (LeaveEmployeeProfile, ledger entries).

DTOs:

public record EmployeeBalanceDetailResponse(
    // Employee profile
    String employeeName,
    LocalDate hireDate,
    int seniorityYears,
    String category,                    // cadre / non-cadre
    int childrenUnder21Count,

    // Annual leave breakdown
    BigDecimal acquisBase,              // base accrual (JC)
    BigDecimal senioriteBonusDays,      // anciennete bonus
    BigDecimal childrenBonusDays,       // children bonus
    BigDecimal totalAcquis,             // total acquired
    BigDecimal taken,                   // total taken
    BigDecimal available,               // net available (JC)

    // Exceptional leave
    BigDecimal exceptionalUsed,         // days used
    int exceptionalCap,                 // 10 days
    int exceptionalPercentage,          // progress bar %

    // Sick leave (rolling 12 months)
    BigDecimal sickDaysUsed,
    String currentPhase,                // FULL_PAY, HALF_PAY, ZERO_PAY
    int daysRemainingBeforeNextPhase
) {}

public record LedgerEntryResponse(
    UUID id,
    LocalDate date,
    String transactionType,             // enum code
    String transactionLabel,            // French label
    BigDecimal amount,                  // signed (+/-)
    String unit,                        // "JC"
    @Nullable BigDecimal runningBalance,
    @Nullable String reason
) {}

public record LedgerEntriesResponse(
    List<LedgerEntryResponse> entries,
    @Nullable String nextCursor,
    int totalCount
) {}

Repository Layer

No new repositories. Uses existing repositories from AB-009 with extended query methods: - Ledger query with pagination (cursor-based), date range filter, transaction type filter

Service Layer

BalanceQueryService.java (extend from AB-009): - getEmployeeBalanceDetail(UUID tenantId, UUID employeeId) -> EmployeeBalanceDetailResponse — Full balance breakdown with employee profile info, annual/exceptional/sick leave summaries - getLedgerEntries(UUID tenantId, UUID employeeId, LedgerFilter filters) -> LedgerEntriesResponse — Paginated ledger with filters: - from / to: date range (optional) - type: transaction type filter (optional, e.g., "ANNUAL") - cursor: cursor-based pagination - Default sort: chronological descending (most recent first)

API Endpoints

Method Path Request Body Response Auth
GET /api/v1/absence/employees/{employeeId}/balance ?includeHistory=true EmployeeBalanceDetailResponse (+ optional ledger preview) HR_MANAGER, DG
GET /api/v1/absence/employees/{employeeId}/balance/ledger ?from=...&to=...&type=ANNUAL&cursor=... LedgerEntriesResponse (paginated) HR_MANAGER, DG

Validation Rules

  • employeeId must exist and belong to the current tenant
  • from / to date filters: from must be before or equal to to
  • type filter must be a valid transaction type
  • Only HR_MANAGER or DG roles can access these endpoints

Multi-Tenant Considerations

  • All queries scoped by tenant_id from TenantResolutionFilter
  • Employee must belong to the authenticated user's tenant
  • Ledger entries are tenant-isolated by design (FK to tenant-scoped LeaveEmployeeProfile)

Flyway Migrations

No new migrations. Uses existing ledger tables from AB-009/AB-012.


Frontend Scope

Pages & Routes

  • /absences/employes/:id/solde — HR Employee Balance Detail page
  • Access: HR_MANAGER, DG

Components

  • EmployeeBalanceDetailPage.tsx — Main page orchestrating all sections
  • EmployeeProfileCard.tsx — Employee identity: name, hire date, seniority, category, children count
  • AnnualLeaveSection.tsx — Hero balance display (JC) + breakdown (acquis base, anciennete, enfants, pris, disponible)
  • ExceptionalLeaveSection.tsx — Progress bar "Utilisees : {n} / 10 jours" with percentage
  • SickLeaveSection.tsx — Days used, current phase badge (variant by phase), days remaining
  • LedgerSection.tsx — Chronological ledger list with "Voir tout" link
  • LedgerEntryRow.tsx — Single ledger entry: date, label, signed amount with +/- and JC unit
  • LedgerFullPage.tsx — Full paginated ledger view (route: /absences/employes/:id/solde/grand-livre)

UI States (ALL REQUIRED)

State Behavior French Copy
Loading Skeleton: profile card + 3 balance sections + ledger list
Empty (new employee) Balance shows 0, ledger section empty state "Aucune transaction enregistree"
Error Toast + retry button "Impossible de charger le solde de l'employe."
Offline Cached data displayed + offline banner "Hors connexion -- derniere synchro : {time}"
Success N/A (read-only page)

Responsive Behavior

  • 360px (mobile): Single column, stacked sections:
  • Employee profile card with avatar icon, name, hire date, seniority badge, category, children count
  • "Conge annuel" section: large hero balance number (JC), breakdown rows below
  • "Permissions exceptionnelles" section: progress bar with label
  • "Conge maladie (12 mois glissants)" section: phase badge + days info
  • "Grand livre" section: last 5 ledger entries, "Voir tout" link at bottom
  • "Ajuster le solde" button (secondary, full-width) at bottom
  • 768px (tablet): Same stacked layout, wider cards.
  • 1024px+ (desktop): 2-column layout:
  • Left column: employee profile card + balance sections
  • Right column: ledger entries in DataTable with sort/filter columns (date, type, amount)
  • "Ajuster le solde" button in page header area

Interactions

  • Navigate to adjustment: Tap "Ajuster le solde" button to navigate to /absences/employes/:id/ajustement (AB-020), pre-filling employee context
  • Ledger "Voir tout": Navigates to full paginated ledger view with date range filter and transaction type filter
  • Offline cache: Page loads from IndexedDB cache when offline. Banner shows last sync time. Data may be stale.
  • Phase badge colors:
  • FULL_PAY: Badge variant=success (green)
  • HALF_PAY: Badge variant=warning (amber)
  • ZERO_PAY: Badge variant=error (red)

Ledger transaction type labels (French):

Transaction Type French Label
MONTHLY_ACCRUAL "Acquisition mensuelle"
SENIORITY_BONUS "Bonus anciennete"
CHILDREN_BONUS "Bonus enfants"
LEAVE_DEBIT "Deduction conge"
CANCEL_CREDIT "Credit annulation"
EARLY_RETURN_CREDIT "Credit retour anticipe"
OVERLAP_CREDIT "Credit chevauchement"
CARRY_OVER "Report"
EXPIRY "Expiration"
MANUAL_ADJUSTMENT "Ajustement manuel"
SETTLEMENT "Solde de tout compte"

Acceptance Criteria

AC-067: HR can view employee profile and balance breakdown
Given I am authenticated as HR_MANAGER or DG
When I navigate to /absences/employes/{id}/solde for an employee in my tenant
Then I see: employee profile (name, hire date, seniority, category, children under 21)
And annual leave breakdown (acquis base, anciennete bonus, children bonus, taken, available)
And exceptional leave (used / 10 cap with progress bar)
And sick leave (days used, current phase with color-coded badge, days remaining before next phase)

AC-068: Full ledger history is visible
Given I view an employee's balance detail
When I expand the ledger section or navigate to the full ledger view
Then I see the chronological list of all ledger transactions (accruals, deductions, adjustments, carry-overs)
And each entry shows: date, French label, signed amount in JC, and optional reason
And entries are paginated with cursor-based navigation

AC-069: Navigate to adjustment from detail
Given I am viewing an employee's balance detail
When I tap "Ajuster le solde"
Then I navigate to /absences/employes/{id}/ajustement (AB-020)
And the employee context (id, name, current balance) is pre-filled

OHADA & Regulatory Rules

  • FR-ABS-120: Ledger transaction types — all types must be displayed with correct French labels
  • FR-ABS-121: Ledger entry fields — date, transaction_type, amount, reason must be visible to HR
  • FR-ABS-122: Ledger is append-only and immutable — UI must not offer edit/delete on ledger entries
  • FR-ABS-123: Corrections are new entries — if a MANUAL_ADJUSTMENT corrects a prior error, it appears as a separate entry in the ledger

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — Service layer, cursor-based pagination
  • docs/standards/database-guidelines.md — Read queries, tenant isolation, BigDecimal for balances
  • docs/standards/api-guidelines.md — GET with query parameters, standard ApiResponse envelope, cursor pagination format
  • docs/standards/react-typescript-guidelines.md — Page composition, skeleton loading, offline cache display
  • docs/standards/design-system.md — Badge (variant=success/warning/error), ProgressBar, Card, DataTable, Button components
  • docs/standards/offline-sync-guidelines.md — Read-only cache pattern (display cached data offline with staleness banner)
  • docs/standards/multi-datasource-patterns.md — Tenant-scoped data access via JdbcClient

Testing Requirements

Unit Tests

  • BalanceQueryService: getEmployeeBalanceDetail returns correct breakdown
  • BalanceQueryService: getLedgerEntries with date range filter
  • BalanceQueryService: getLedgerEntries with transaction type filter
  • BalanceQueryService: cursor-based pagination returns correct page
  • BalanceQueryService: sick leave phase calculation (FULL_PAY, HALF_PAY, ZERO_PAY)

Integration Tests

  • GET /api/v1/absence/employees/{id}/balance — full response structure validation
  • GET /api/v1/absence/employees/{id}/balance/ledger — pagination and filtering
  • GET endpoint — 403 for EMPLOYE role (unauthorized for HR detail view)
  • Tenant isolation: employee balance from tenant A not visible to tenant B HR

What QA Will Validate

  • Balance detail page renders correctly on mobile (360px), tablet (768px), desktop (1024px+)
  • Desktop 2-column layout with DataTable on right
  • Phase badges show correct colors (green/amber/red)
  • Progress bar for exceptional leave displays correct percentage
  • Ledger entries show French labels for all transaction types
  • "Voir tout" navigates to full paginated ledger
  • "Ajuster le solde" navigates to AB-020 adjustment form
  • Offline: cached data displayed with staleness banner
  • Empty state: new employee with no transactions shows "Aucune transaction enregistree"

Out of Scope

  • Editing employee profile data — HR Core module responsibility
  • Exporting ledger to CSV/PDF — Enterprise V2
  • Employee self-service balance view — AB-009 (different route, different auth scope)
  • Ledger entry edit/delete — not allowed per FR-ABS-122 (immutable ledger)
  • Historical balance snapshots — V2 (point-in-time balance reconstruction)

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 (no cross-tenant data leaks)
  • [ ] Offline write+sync works (Papillon) — read-only cache with staleness banner
  • [ ] No TypeScript any — all types explicit
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries created for every data mutation — N/A (read-only endpoints)