Aller au contenu

Story AB-010: Leave Request Creation — Annual & Unpaid (Backend API + Frontend Form)

@assignee: @joel23p

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


Objective

Enable employees to submit annual leave and unpaid leave requests via the backend API and frontend form (S2 — simple form variant), including offline request creation with auto-sync, client-side validation with soft warnings (15-day notice, 14-day consecutive block), and hard blocks (insufficient balance, <12 months service).


Backend Scope

Entities & Records

Request record:

public record CreateLeaveRequestRequest(
    @NotNull LeaveType leaveType,        // ANNUAL or UNPAID (this story)
    @NotNull LocalDate startDate,
    @NotNull LocalDate endDate,
    boolean halfDayStart,                 // afternoon only
    boolean halfDayEnd,                   // morning only
    @Size(max = 500) String reason
) {}

Response record:

public record LeaveRequestResponse(
    UUID id,
    LeaveType leaveType,
    LeaveRequestStatus status,
    LocalDate startDate,
    LocalDate endDate,
    boolean halfDayStart,
    boolean halfDayEnd,
    BigDecimal numberOfDays,
    String reason,
    boolean isRetroactive,
    List<SoftWarning> warnings,          // soft warnings (not blocking)
    Instant createdAt
) {}

public record SoftWarning(
    String code,    // SHORT_NOTICE, NO_14_DAY_BLOCK
    String message  // French human-readable message
) {}

Repository Layer

Uses existing repositories from AB-002: - LeaveRequestRepository.save(LeaveRequest request) - LeaveRequestRepository.findOverlappingRequests(UUID tenantId, UUID employeeId, LocalDate start, LocalDate end) - LeaveEmployeeProfileRepository.findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId)

Service Layer

LeaveRequestService.java — createRequest(): 1. Resolve approver via ApproverResolutionService (org-unit-based, uses manager_id from CachedEmployeeData) 2. Validate via LeaveValidationService 3. Compute days via DayComputationService (business days, exclude public holidays, handle half-days) 4. Set status to PENDING_APPROVAL (no document needed for ANNUAL/UNPAID) 5. Set isRetroactive = true if startDate < today (FR-ABS-043) 6. Publish LeaveRequestCreated 7. Return response with any soft warnings

LeaveValidationService.java: - Hard blocks (return 422): - FR-ABS-040: ANNUAL only after 12 months service - FR-ABS-041: ANNUAL balance >= days requested - FR-ABS-046: overlap with approved leave of same-priority type - startDate > endDate → 422 - Soft warnings (returned in response, NOT blocking): - FR-ABS-044: startDate < today + 15 days → short notice warning - FR-ABS-045: no 14-day consecutive block taken this period → reminder

ApproverResolutionService.java: - Resolves approver from CachedEmployeeData.managerId (org-unit-based routing) - Falls back to HR_MANAGER if no direct manager configured

DayComputationService.java: - Computes business days between start and end dates - Excludes public holidays (from PublicHolidayRepository) - Handles half-days: subtract 0.5 for halfDayStart and/or halfDayEnd (FR-ABS-031)

API Endpoints

Method Path Request Response Auth
POST /api/v1/absence/leave-requests CreateLeaveRequestRequest LeaveRequestResponse (201) EMPLOYEE (self), HR_MANAGER (any)

Validation Rules

  • FR-ABS-040: ANNUAL only after 12 months service → 422 "Vous devez completer 12 mois de service avant de prendre un conge annuel."
  • FR-ABS-041: ANNUAL balance >= days requested → 422 "Solde insuffisant. Votre solde disponible est de {balance} jours."
  • FR-ABS-043: retroactive flag if startDate < today
  • FR-ABS-044: startDate < today + 15 days → soft warning (not blocking)
  • FR-ABS-045: no 14-day block this period → soft warning (not blocking)
  • FR-ABS-046: overlap with approved leave → 422 blocked for same-priority types
  • All day computations use BigDecimal with RoundingMode.HALF_UP, scale(4)

Multi-Tenant Considerations

  • All queries include tenantId from ScopedValue<TenantContext>
  • Public holiday lookup is tenant-scoped (tenants may have custom holidays)
  • Approver resolution uses tenant's org structure
  • Works across all 4 DB profiles

Frontend Scope

OFFLINE SYNC REMINDER (Papillon — CRITICAL): This story involves data mutations. The form MUST work identically offline. On submit offline: save to IndexedDB as DRAFT with sync-pending status. On reconnect: Service Worker auto-submits via POST. If server validation fails: push notification with reason + link to edit. Auto-save: every field change persisted to IndexedDB — form survives app restart, tab close, crash. See docs/standards/offline-sync-guidelines.md for the full offline write+sync pattern.

Pages & Routes

Route Page Description
/absences/demande LeaveRequestPage Leave request creation form (simple variant for ANNUAL/UNPAID)

Components

  • LeaveRequestForm — Main form container with auto-save logic
  • Select — Type selector (ANNUAL, UNPAID for this story)
  • DatePicker — Start date and end date pickers (side by side on mobile)
  • Checkbox — Half-day toggles (revealed after dates are filled)
  • DayComputationCard — Computed days display with primary-subtle background
  • InlineBanner — Soft warning banners (amber)
  • Input (textarea) — Reason field (optional)
  • Button — Submit button (primary, lg, fullWidth)
  • AutoSaveIndicator — Caption showing draft save status

UI States (ALL REQUIRED)

State Behavior French Copy
Loading N/A (form is empty initially)
Empty Form with placeholders "Choisir un type de conge"
Error (hard block) Red inline error below field "Solde insuffisant. Votre solde disponible est de {balance} jours." / "Vous devez completer 12 mois de service..."
Error (server) Toast "Impossible d'envoyer la demande. Reessayez."
Warning (soft) Amber InlineBanner below trigger field "Attention : le preavis legal est de 15 jours avant le debut du conge." / "Rappel : la loi impose un conge principal d'au moins 14 jours ouvrables consecutifs par an."
Offline Form works identically, saved as DRAFT "Demande enregistree -- sera envoyee automatiquement"
Success Check animation + Toast + navigate to /absences "Demande envoyee !"

Responsive Behavior

Mobile (360px — Papillon primary): - Full-screen scrollable form - Type Select → Date pickers (side by side) → Half-day checkboxes (revealed after dates filled) → Day computation card (primary-subtle bg) → Soft warning banners (amber) → Reason textarea → Submit button (primary, lg, fullWidth) - Auto-save caption at bottom - Touch targets >= 48px

Desktop (1024px+ — Enterprise primary): - Modal (600px wide) or side panel - Same form flow, more compact spacing

Interactions

  • Selecting type populates available date ranges
  • Filling both dates triggers day computation (immediate, client-side)
  • Half-day checkboxes appear only after both dates are set
  • Day computation card updates in real-time as dates/half-days change
  • Shows {n} jours calendaires + Solde apres demande : {n} JC + dont {n} jour(s) ferie(s) non compte(s)
  • Submit: client-side validation first, then POST. On success: check animation + toast + navigate
  • Auto-save: every field change → IndexedDB. Form survives app restart

French Micro-Copy

  • form.type_label: "Type de conge"
  • form.type_placeholder: "Choisir un type de conge"
  • form.start_date: "Date de debut"
  • form.end_date: "Date de fin"
  • form.half_day_start: "Demi-journee debut (apres-midi uniquement)"
  • form.half_day_end: "Demi-journee fin (matin uniquement)"
  • form.reason: "Motif (optionnel)"
  • form.reason_placeholder: "Decrivez la raison de votre demande..."
  • form.days_computed: "{n} jours calendaires"
  • form.balance_after: "Solde apres demande : {n} JC"
  • form.holiday_excluded: "dont {n} jour(s) ferie(s) non compte(s)"
  • form.submit: "Envoyer la demande"
  • form.autosave: "Brouillon sauvegarde automatiquement"
  • info.retroactive: "Demande retroactive -- sera signalee dans l'historique d'audit."

Acceptance Criteria

AC-032: Employee can view balance and submit leave request
Given I am an employee with an active account
When I open the Absence module
Then I see my current leave balance (by type, in jours calendaires)

AC-033: Offline request creation with auto-sync
Given I fill out a leave request form offline
When connectivity returns
Then the request is auto-submitted to my manager for approval

AC-034: Soft warning for short notice period
Given I submit a request
When the start date is < 15 days from today
Then I see a soft warning about the 15-day notice period but can still submit

AC-035: Soft warning for 14-day consecutive block
Given I submit a request for annual leave and have NOT yet taken a 14-consecutive-day block this year
Then I see a soft warning about the legal 14-day minimum requirement

OHADA & Regulatory Rules

  • FR-ABS-030 (CT Art. 25.9): Employee submits leave request to direct manager via org-unit hierarchy.
  • FR-ABS-031 (CT Art. 25.10): Half-day computation — start day afternoon-only or end day morning-only deducts 0.5 from total days.
  • FR-ABS-040 (CT Art. 25.7): ANNUAL leave requires 12 months of continuous service. Hard block.
  • FR-ABS-041: Balance must be sufficient for the requested number of days. Hard block.
  • FR-ABS-043: Retroactive requests (start_date < today) are flagged in audit trail.
  • FR-ABS-044 (CT Art. 25.11): 15-day advance notice is recommended. Soft warning only.
  • FR-ABS-045 (CCI Art. 25.12): At least one 14-consecutive-jours-ouvrables block per reference period. Soft warning reminder.
  • FR-ABS-046: Overlap with same-priority approved leave is blocked. Hard block.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §REST controller patterns, §Validation, §Application Events
  • docs/standards/database-guidelines.md — §BigDecimal for financial calculations, §Multi-tenant repository pattern
  • docs/standards/api-guidelines.md — §POST endpoint patterns, §ApiResponse envelope, §Validation error format
  • docs/standards/react-typescript-guidelines.md — §Form patterns, §CSS Modules, §i18n, §Error handling
  • docs/standards/design-system.md — §Select, §DatePicker, §Button, §InlineBanner, §Toast
  • docs/standards/offline-sync-guidelines.md — §Offline form submission, §IndexedDB auto-save, §Service Worker sync, §Conflict resolution

Testing Requirements

Unit Tests

  • LeaveValidationService: ANNUAL + <12 months service → 422 with correct French message
  • LeaveValidationService: ANNUAL + insufficient balance → 422 with balance in message
  • LeaveValidationService: start_date < today+15 → soft warning returned, not blocking
  • LeaveValidationService: no 14-day block this period → soft warning returned
  • LeaveValidationService: overlap with approved leave → 422
  • DayComputationService: 2026-03-02 to 2026-03-13 → correct business days minus holidays
  • DayComputationService: half-day start + half-day end → correctly subtracts 1.0 from total
  • ApproverResolutionService: employee with manager → returns manager_id
  • ApproverResolutionService: employee without manager → falls back to HR_MANAGER
  • LeaveRequestService.createRequest(): valid request → status=PENDING_APPROVAL, event published

Integration Tests

  • POST /api/v1/absence/leave-requests with valid ANNUAL request → 201 with correct response
  • POST with insufficient balance → 422 with validation error in ApiResponse envelope
  • POST with EMPLOYEE role for own request → 201
  • POST with EMPLOYEE role for another employee → 403
  • Multi-tenant: request created in tenant A not visible in tenant B

What QA Will Validate

  • Form renders at 360px with correct field order and spacing
  • Half-day checkboxes appear only after dates are filled
  • Day computation card updates in real-time
  • Soft warnings appear as amber banners, do not block submission
  • Hard blocks show red inline errors, disable submit
  • Offline: form works, saves as DRAFT, auto-syncs on reconnect
  • Auto-save: close tab, reopen → form state preserved
  • Success: check animation + toast + navigates to /absences

Out of Scope

  • Sick leave form variant (AB-014)
  • Exceptional leave form variant (AB-015)
  • Maternity leave form variant (AB-016)
  • Document upload (AB-014)
  • Approval flow (AB-012)
  • Request cancellation (AB-013)

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
  • [ ] Offline write+sync works (Papillon) / N/A (Enterprise) — Form saves as DRAFT offline, auto-syncs on reconnect, auto-save persists to IndexedDB
  • [ ] No TypeScript any
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries for every data mutation — LeaveRequestCreated published, retroactive flag logged