Aller au contenu

Story AB-015: Permission Exceptionnelle Request (Backend + Frontend)

@assignee: @joel23p

Module: absence-management Slice: VS-AB-005 (exceptional extension) from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 15 Depends on: AB-010, AB-008 Can develop concurrently with: AB-014, AB-016 Merge order: After AB-010 and AB-008 Estimated complexity: M PRD User Stories: US-ABS-007


Objective

Extend the leave request creation flow to support permission exceptionnelle requests with the 2-step wizard: Step 1 selects the event type (large tappable cards with icons showing CCI or tenant-configured durations), Step 2 auto-fills dates and requires a supporting document.


Backend Scope

Entities & Records

Request record (extends CreateLeaveRequestRequest from AB-010):

// Additional fields when leaveType=EXCEPTIONAL
public record CreateLeaveRequestRequest(
    @NotNull LeaveType leaveType,         // EXCEPTIONAL
    @NotNull LocalDate startDate,
    @NotNull LocalDate endDate,           // auto-computed, read-only
    boolean halfDayStart,
    boolean halfDayEnd,
    @Size(max = 500) String reason,
    // Exceptional-specific:
    @NotNull String eventTypeCode,        // MARRIAGE_SELF, BIRTH_CHILD, etc.
    UUID supportingDocumentId             // optional at creation
) {}

Enums:

public enum ExceptionalEventType {
    MARRIAGE_SELF,       // Mariage du salarie — CCI min: 4 jours
    MARRIAGE_CHILD,      // Mariage d'un enfant — CCI min: 2 jours
    BIRTH_CHILD,         // Naissance d'un enfant — CCI min: 2 jours
    DEATH_SPOUSE,        // Deces du conjoint — CCI min: 5 jours
    DEATH_CHILD,         // Deces d'un enfant — CCI min: 5 jours
    DEATH_PARENT,        // Deces pere/mere — CCI min: 5 jours
    DEATH_SIBLING,       // Deces frere/soeur — CCI min: 3 jours
    DEATH_IN_LAW,        // Deces beau-pere/belle-mere — CCI min: 3 jours
    MOVE                 // Demenagement — CCI min: 1 jour
}

Repository Layer

Uses existing repositories from AB-002 + AB-008: - ExceptionalLeaveConfigRepository.findByTenantIdAndEventTypeCode(UUID tenantId, String eventTypeCode) — from AB-008 - LeaveRequestRepository — standard save/query - LeaveBalanceLedgerRepository — for checking exceptional days used this period

Service Layer

LeaveRequestService.java — extends createRequest() for EXCEPTIONAL type: 1. Look up duration from ExceptionalLeaveConfig: - If tenant has custom config for this event type → use tenant's durationDays - Else → use CCI default duration 2. Validate annual cap: totalExceptionalTakenThisPeriod + numberOfDays <= 10 (FR-ABS-042, FR-ABS-PERM-02) 3. Auto-compute endDate = startDate + duration (in jours ouvrables, skipping weekends and holidays) 4. If supportingDocumentId IS NULL → status = PENDING_DOCUMENT 5. If supportingDocumentId IS NOT NULL → status = PENDING_APPROVAL 6. Publish LeaveRequestCreated

ExceptionalLeaveConfigService.java (from AB-008, used here): - getDurationForEvent(UUID tenantId, String eventTypeCode): returns tenant config or CCI default - validateMinimumDuration(String eventTypeCode, int configuredDays): blocks below CCI minimum → 422

API Endpoints

Note: Uses the same POST endpoint as AB-010 (POST /api/v1/absence/leave-requests), extended to handle leaveType=EXCEPTIONAL with eventTypeCode.

No new endpoints. The event type list and durations are served by AB-008's config endpoints.

Validation Rules

  • FR-ABS-PERM-01: CCI minimum enforcement — tenant cannot configure a duration below the CCI minimum for any event type → 422 "La duree minimale legale (CCI) pour cet evenement est de {X} jours."
  • FR-ABS-PERM-02 / FR-ABS-042: Annual cap of 10 jours for all permissions exceptionnelles combined → 422 "Vous avez atteint le plafond annuel de 10 jours de permissions exceptionnelles."
  • eventTypeCode must be a valid ExceptionalEventType enum value → 422
  • endDate is auto-computed — if provided by client, it is ignored (server computes)
  • Supporting document required (justificatif) — same PENDING_DOCUMENT logic as AB-014

Multi-Tenant Considerations

  • Duration lookup is tenant-scoped: tenant config overrides CCI defaults
  • Annual cap check queries ledger entries within the current reference period for the tenant
  • All queries include tenantId from ScopedValue<TenantContext>
  • Works across all 4 DB profiles

Frontend Scope

OFFLINE SYNC REMINDER (Papillon — CRITICAL): This story involves data mutations. The permission exceptionnelle form MUST work offline. Event type list and CCI durations are cached in IndexedDB at first load. Tenant-configured durations are cached on sync. Form saves as DRAFT to IndexedDB on submit offline. File attachments stored as blobs in IndexedDB, uploaded on reconnect. See docs/standards/offline-sync-guidelines.md.

Pages & Routes

No new routes. Extends the existing leave request form from AB-010.

Route Page Description
/absences/demande LeaveRequestPage (extended) 2-step wizard when type=EXCEPTIONAL

Components

Step 1 — Event Selection: - StepIndicator — Step indicator (steps=2, current=1) - EventTypeCard — Large tappable card with icon, event name, and duration in days - EventTypeList — Grid of EventTypeCards (first 5 visible, rest behind expander) - RemainingDaysHelper — Helper text showing "Restant cette annee: {remaining}/10 jours" - ExpanderButton — "Plus d'evenements" toggle for remaining event types

Step 2 — Dates + Document: - StepIndicator — Step indicator (steps=2, current=2) - EventSummary — Summary card of step 1 selection (icon, name, duration) - DatePicker — Start date only (end date auto-calculated, read-only with lock icon) - AutoComputedDate — Read-only end date display with lock icon - FileUpload — Document upload zone (from AB-014, required for exceptional) - Button — Submit button

UI States (ALL REQUIRED)

State Behavior French Copy
Loading Shimmer on event type cards
Empty N/A (event types always available from CCI defaults)
Error (cap reached) Inline error below event cards "Vous avez atteint le plafond annuel de 10 jours de permissions exceptionnelles."
Error (validation) Red inline below field Various validation messages
Offline Form works, saved as DRAFT "Demande enregistree -- sera envoyee automatiquement"
Success Check animation + Toast + navigate to /absences "Demande envoyee !"

Responsive Behavior

Mobile (360px — Papillon primary): - Step 1: Full-screen, StepIndicator at top, event type cards in single-column list - Each card: icon (left) + event name + duration in days (right). Touch targets >= 48px - First 5 events visible, "Plus d'evenements" expander for remaining 4 - "Restant cette annee: {remaining}/10 jours" helper text below cards - Step 2: Full-screen, StepIndicator at top, summary of step 1, start date DatePicker, auto-computed end date (read-only with lock icon), FileUpload zone, submit button (primary, lg, fullWidth) - Caption below submit: "Vous pouvez envoyer sans document. La demande restera en attente du justificatif."

Desktop (1024px+ — Enterprise primary): - Step 1: Event type cards in 2-column grid within modal/panel (600px wide) - Step 2: Same form flow, more compact spacing - All event types visible (no expander needed at wider viewport)

Interactions

  • Tap event type card → selected state (checkmark overlay) + "Suivant" button appears
  • Tap "Suivant" → slide to Step 2 with left animation
  • Step 2: only start date is editable. End date auto-calculates: "Fin calculee: debut + {n} jours ouvrables"
  • Tap "Retour" on Step 2 → slide back to Step 1, selection preserved
  • File upload: same behavior as AB-014 (tap → file picker, drag-drop on desktop)
  • Submit: client-side cap validation, then POST

French Micro-Copy

  • exceptional.step1_title: "Quel evenement ?"
  • exceptional.step2_title: "Dates et justificatif"
  • exceptional.remaining: "Restant cette annee : {remaining}/10 jours"
  • exceptional.more_events: "Plus d'evenements"
  • exceptional.next: "Suivant"
  • exceptional.back: "Retour"
  • exceptional.end_computed: "Fin calculee : debut + {n} jours ouvrables"
  • exceptional.doc_required: "Justificatif requis"
  • exceptional.doc_optional_caption: "Vous pouvez envoyer sans document. La demande restera en attente du justificatif."
  • exceptional.cap_reached: "Vous avez atteint le plafond annuel de 10 jours de permissions exceptionnelles."
  • exceptional.cci_minimum_block: "La duree minimale legale (CCI) pour cet evenement est de {X} jours."
  • Event type labels:
  • event.MARRIAGE_SELF: "Mariage du salarie"
  • event.MARRIAGE_CHILD: "Mariage d'un enfant"
  • event.BIRTH_CHILD: "Naissance d'un enfant"
  • event.DEATH_SPOUSE: "Deces du conjoint"
  • event.DEATH_CHILD: "Deces d'un enfant"
  • event.DEATH_PARENT: "Deces pere/mere"
  • event.DEATH_SIBLING: "Deces frere/soeur"
  • event.DEATH_IN_LAW: "Deces beau-pere/belle-mere"
  • event.MOVE: "Demenagement"

Acceptance Criteria

AC-048: System pre-fills duration from CCI or tenant config
Given I select "Permission exceptionnelle" as leave type
When I choose the event type (mariage, naissance, deces, etc.)
Then the system pre-fills the number of days based on the CCI minimum for CI (or tenant's configured duration if higher)

AC-049: Tenant-configured durations override CCI
Given the tenant has configured more generous durations (above CCI minimum)
When I select the event type
Then the system pre-fills the tenant's duration

AC-050: CCI minimum blocks below-minimum configuration
Given the tenant tries to configure a duration below the CCI minimum
When they save
Then the system blocks with: "La duree minimale legale (CCI) pour cet evenement est de X jours."

OHADA & Regulatory Rules

  • FR-ABS-PERM-01 (CCI Art. 46): CCI minimum enforcement — each event type has a CCI-mandated minimum duration. Tenants may configure higher durations but never lower. System blocks below-minimum configuration.
  • FR-ABS-PERM-02 / FR-ABS-042 (CCI Art. 46): Annual cap of 10 jours ouvrables for all permissions exceptionnelles combined per reference period. Once reached, no further exceptional leave requests are accepted.
  • CCI Art. 46 durations: Mariage salarie=4j, Mariage enfant=2j, Naissance=2j, Deces conjoint/enfant/parent=5j, Deces frere-soeur/beau-parent=3j, Demenagement=1j.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §REST controller patterns, §Enum handling, §Application Events
  • docs/standards/database-guidelines.md — §Multi-tenant repository pattern, §BigDecimal for day computations
  • docs/standards/api-guidelines.md — §POST endpoint patterns, §ApiResponse envelope, §422 validation errors
  • docs/standards/react-typescript-guidelines.md — §Component patterns, §CSS Modules, §i18n, §Multi-step form patterns
  • docs/standards/design-system.md — §Card (selectable variant), §StepIndicator, §DatePicker, §FileUpload, §Button
  • docs/standards/offline-sync-guidelines.md — §Cached reference data (event types), §Offline form submission, §Blob storage in IndexedDB

Testing Requirements

Unit Tests

  • LeaveRequestService.createRequest(): EXCEPTIONAL + MARRIAGE_SELF → duration=4j from CCI default
  • LeaveRequestService.createRequest(): EXCEPTIONAL + MARRIAGE_SELF + tenant config 6j → duration=6j (tenant override)
  • LeaveRequestService.createRequest(): total exceptional used=8j + requesting 3j → 422 (cap exceeded)
  • LeaveRequestService.createRequest(): total exceptional used=8j + requesting 2j → allowed (exactly at cap)
  • ExceptionalLeaveConfigService.validateMinimumDuration(): configured 2j for MARRIAGE_SELF (CCI min=4j) → 422
  • ExceptionalLeaveConfigService.validateMinimumDuration(): configured 5j for MARRIAGE_SELF (CCI min=4j) → allowed
  • End date auto-computation: startDate=Monday + 4 jours ouvrables → endDate=Thursday (same week)
  • End date auto-computation: startDate=Thursday + 4 jours ouvrables → endDate=Tuesday (skips weekend)

Integration Tests

  • POST with EXCEPTIONAL type + valid event code → 201, duration from config
  • POST with EXCEPTIONAL type + cap exceeded → 422
  • POST with EXCEPTIONAL type + no document → 201, status=PENDING_DOCUMENT
  • POST with invalid eventTypeCode → 422
  • Multi-tenant: tenant A config does not affect tenant B's durations

What QA Will Validate

  • Step 1: event type cards render at 360px with icons and durations
  • First 5 events visible, "Plus d'evenements" expander works
  • "Restant cette annee" helper updates correctly
  • Cap reached → inline error, cards disabled
  • Step 2: start date picker, end date auto-computed and read-only
  • End date shows "Fin calculee: debut + {n} jours ouvrables"
  • FileUpload zone appears with correct helper text
  • Submit without doc → PENDING_DOCUMENT toast
  • Offline: form works, event types from cache, saved as DRAFT
  • Desktop: 2-column grid for event types in modal

Out of Scope

  • Permission exceptionnelle config management (AB-008)
  • Approval flow (AB-012)
  • Document upload infrastructure (uses platform MinIO API)
  • Custom event types beyond the 9 CCI-defined types (V2)
  • Partial-day permissions exceptionnelles (always full days per CCI)

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 — tenant config overrides scoped correctly, cap check per tenant
  • [ ] Offline write+sync works (Papillon) / N/A (Enterprise) — Event types cached, form saves DRAFT offline, blobs in 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 with eventTypeCode