Aller au contenu

Story AB-011: Leave Request List + Detail View (Backend + Frontend)

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


Objective

Enable employees to view their leave request history (grouped by status: Active, Pending, Past) with cursor-based pagination and role-aware filtering, plus a detail view for individual requests showing dates, status, approval timeline.


Backend Scope

Entities & Records

Response records:

public record LeaveRequestListResponse(
    List<LeaveRequestSummary> items,
    String nextCursor,       // null if no more pages
    int totalCount
) {}

public record LeaveRequestSummary(
    UUID id,
    LeaveType leaveType,
    LeaveRequestStatus status,
    LocalDate startDate,
    LocalDate endDate,
    BigDecimal numberOfDays,
    Instant createdAt
) {}

public record LeaveRequestDetailResponse(
    UUID id,
    LeaveType leaveType,
    LeaveRequestStatus status,
    LocalDate startDate,
    LocalDate endDate,
    boolean halfDayStart,
    boolean halfDayEnd,
    BigDecimal numberOfDays,
    String reason,
    boolean isRetroactive,
    String rejectionReason,              // null unless REJECTED
    Instant createdAt,
    Instant approvedAt,                  // null unless APPROVED
    String approverName,                 // null unless APPROVED/REJECTED
    UUID supportingDocumentId,           // null unless document attached
    List<StatusTimelineEntry> timeline
) {}

public record StatusTimelineEntry(
    LeaveRequestStatus status,
    Instant timestamp,
    String actorName,
    String note               // rejection reason, etc.
) {}

Repository Layer

LeaveRequestRepository (extend from AB-002): - findByTenantIdAndEmployeeIdWithCursor(UUID tenantId, UUID employeeId, LeaveRequestStatus status, LocalDate from, LocalDate to, String cursor, int limit) — cursor-based pagination using keyset (not offset) - findByTenantIdWithCursor(UUID tenantId, ...) — for HR/DG access (all employees) - findByTenantIdAndManagerIdWithCursor(UUID tenantId, UUID managerId, ...) — for MANAGER (direct reports + own) - countByTenantIdAndEmployeeIdAndStatus(UUID tenantId, UUID employeeId, LeaveRequestStatus status) — for badge counts

Service Layer

LeaveRequestQueryService.java: - getMyRequests(filters, cursor): EMPLOYEE → own requests only - getRequests(filters, cursor): MANAGER → direct reports + own; HR_MANAGER/DG → all - getRequestDetail(UUID requestId): returns detail with timeline, respects RBAC - Cursor-based pagination: keyset pagination on (created_at, id) composite key — no offset drift

API Endpoints

Method Path Request Response Auth
GET /api/v1/absence/leave-requests ?status=...&from=...&to=...&sort=createdAt,desc&cursor=...&limit=20 LeaveRequestListResponse EMPLOYEE (own), MANAGER (direct reports+own), HR_MANAGER/DG (all)
GET /api/v1/absence/leave-requests/{id} LeaveRequestDetailResponse EMPLOYEE (own), MANAGER (if subordinate), HR_MANAGER, DG

Validation Rules

  • EMPLOYEE can only see own requests
  • MANAGER can see direct reports + own requests
  • HR_MANAGER and DG can see all requests for the tenant
  • Cursor must be a valid opaque token (base64-encoded created_at:id)
  • limit capped at 50 (default 20)
  • Date range from/to are optional filters

Multi-Tenant Considerations

  • All queries include tenantId from ScopedValue<TenantContext>
  • Cursor tokens are tenant-scoped (cannot be used cross-tenant)
  • Works across all 4 DB profiles

Frontend Scope

OFFLINE SYNC REMINDER (Papillon): This story is primarily read-only, but request list data MUST be cached in IndexedDB for offline viewing. Cache the most recent page of requests. DRAFT requests (created offline in AB-010) must appear in the list with a sync-pending icon. Display the "Hors connexion" banner with last sync time when offline.

Pages & Routes

Route Page Description
/absences AbsenceHome (history section) Request list grouped by status
/absences/demande/{id} LeaveRequestDetail Individual request detail + timeline

Components

S1 — History Section (on AbsenceHome): - RequestCard — Card showing leave type icon, dates, day count, status Badge - StatusSection — Grouped section with header ("En cours", "En attente", "Passees") - Badge — Status badge with color mapping (see below) - InfiniteScroll — Triggers cursor pagination on scroll bottom

S3 — Detail View: - RequestDetailCard — Full info card (type, status, dates, duration, half-day, reason, dates) - StatusTimeline — Vertical timeline showing status history - DocumentIndicator — Shows attached document status - RejectionCard — Red-tinted card showing rejection reason (if REJECTED)

UI States (ALL REQUIRED)

State Behavior French Copy
Loading 3 card-shaped shimmer rectangles
Empty (no requests) EmptyState with calendar+sunshine illustration "Vous n'avez pas encore de demande de conge" / CTA: "Faire ma premiere demande"
Error Toast + retry "Impossible de charger vos conges. Reessayez."
Offline Cached data from IndexedDB + amber banner "Hors connexion -- derniere synchro : {time}"
Success (after form) New card at top with scale-bounce entrance animation

Responsive Behavior

Mobile (360px — Papillon primary): - Cards grouped by status sections: "En cours" (APPROVED, IN_PROGRESS), "En attente" (PENDING_APPROVAL, PENDING_DOCUMENT, DRAFT), "Passees" (COMPLETED, REJECTED, CANCELLED) - Each card: leave type icon + dates + day count + status Badge - Tap card → navigate to detail view - Infinite scroll for pagination - Touch targets >= 48px

Desktop (1024px+ — Enterprise primary): - DataTable replaces cards with columns: Type, Dates, Jours, Statut - Tabs: "En cours (N)" / "En attente (N)" / "Passees" - Sortable columns, clickable rows → detail view in side panel or modal - Status filter dropdown + date range picker in toolbar

Interactions

  • Tap/click card or row → navigate to /absences/demande/{id}
  • Scroll to bottom → load next page via cursor pagination
  • Pull-to-refresh (mobile) → reload from API
  • Filter by status (desktop tabs, mobile section headers)
  • Back button from detail → return to list with scroll position preserved

Status Badge Mapping

Status Color French Label
APPROVED success (green) "Approuve"
PENDING_APPROVAL warning (amber) "En attente d'approbation"
PENDING_DOCUMENT warning (amber) "En attente de justificatif"
REJECTED error (red) "Rejete"
CANCELLED error (red) "Annule"
IN_PROGRESS info (blue) "En cours"
COMPLETED neutral (gray) "Termine"
DRAFT info (blue) "Brouillon"

Acceptance Criteria

AC-036: Role-based request visibility
Given I am an employee
When I view my leave requests
Then I see only my own requests; managers see direct reports + own; HR/DG see all

AC-037: Cursor-based pagination
Given there are more than 20 leave requests
When I scroll to the bottom of the list
Then additional requests are loaded via cursor pagination (no offset drift)

AC-038: Filter by status and date range
Given I am viewing my leave request list
When I filter by status=APPROVED and date range 2026-01-01 to 2026-06-30
Then only matching requests are shown

OHADA & Regulatory Rules

  • No specific OHADA rules for list/detail display. Request data was created and validated per OHADA rules in AB-010.
  • Status transitions follow the state machine defined in AB-003.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §REST controller patterns, §Cursor-based pagination
  • docs/standards/database-guidelines.md — §Keyset pagination pattern, §Multi-tenant repository pattern
  • docs/standards/api-guidelines.md — §GET endpoint patterns, §ApiResponse envelope, §Pagination response format
  • docs/standards/react-typescript-guidelines.md — §Component patterns, §CSS Modules, §i18n, §Infinite scroll
  • docs/standards/design-system.md — §Badge, §Card, §EmptyState, §DataTable, §Shimmer loading
  • docs/standards/offline-sync-guidelines.md — §IndexedDB caching for read-only data, §Offline banner pattern

Testing Requirements

Unit Tests

  • LeaveRequestQueryService.getMyRequests(): EMPLOYEE role → returns only own requests
  • LeaveRequestQueryService.getRequests(): MANAGER role → returns direct reports + own
  • LeaveRequestQueryService.getRequests(): HR_MANAGER role → returns all tenant requests
  • LeaveRequestQueryService.getRequestDetail(): EMPLOYEE accessing own request → 200
  • LeaveRequestQueryService.getRequestDetail(): EMPLOYEE accessing other's request → 403
  • Cursor parsing: valid base64 cursor → correct offset applied
  • Cursor parsing: invalid cursor → 400

Integration Tests

  • GET /api/v1/absence/leave-requests with EMPLOYEE role → returns only own, 200
  • GET /api/v1/absence/leave-requests?status=APPROVED&from=2026-01-01&to=2026-06-30 → filtered results
  • GET /api/v1/absence/leave-requests?cursor={valid} → next page of results
  • GET /api/v1/absence/leave-requests/{id} with correct role → 200 with timeline
  • Multi-tenant isolation: requests from tenant A not visible in tenant B

What QA Will Validate

  • Request cards render at 360px grouped by status sections
  • Tap card navigates to detail view with correct data
  • Infinite scroll loads next page seamlessly
  • Detail view shows timeline with status history
  • REJECTED detail shows rejection reason in red card
  • Desktop: DataTable with sortable columns and tabs
  • Offline: cached requests display with "Hors connexion" banner
  • DRAFT requests (from offline creation) show sync-pending icon
  • New request after form submission appears with entrance animation

Out of Scope

  • Creating requests (AB-010)
  • Cancelling requests (AB-013)
  • Approval actions (AB-012)
  • Document upload (AB-014)
  • Manager's team calendar view (AB-012 detail)

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 — requests scoped to tenant context, RBAC enforced
  • [ ] Offline write+sync works (Papillon) / N/A (Enterprise) — Cached request list in IndexedDB, offline banner, DRAFT cards with sync icon
  • [ ] 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)