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) limitcapped at 50 (default 20)- Date range
from/toare optional filters
Multi-Tenant Considerations¶
- All queries include
tenantIdfromScopedValue<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 paginationdocs/standards/database-guidelines.md— §Keyset pagination pattern, §Multi-tenant repository patterndocs/standards/api-guidelines.md— §GET endpoint patterns, §ApiResponse envelope, §Pagination response formatdocs/standards/react-typescript-guidelines.md— §Component patterns, §CSS Modules, §i18n, §Infinite scrolldocs/standards/design-system.md— §Badge, §Card, §EmptyState, §DataTable, §Shimmer loadingdocs/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 requestsLeaveRequestQueryService.getRequests(): MANAGER role → returns direct reports + ownLeaveRequestQueryService.getRequests(): HR_MANAGER role → returns all tenant requestsLeaveRequestQueryService.getRequestDetail(): EMPLOYEE accessing own request → 200LeaveRequestQueryService.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-requestswith 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)