Aller au contenu

User Story QR-005 : Jours fériés + Jours chômés de l'entreprise

@assignee: @m-kouassi

Module : qr-attendance.
Segment : QR-T1-004.
Contexte de la marque : [LES DEUX] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite.
Priorité : 5.
Dépend de : QR-001.
Peut être développé en parallèle avec : QR-002, QR-003, QR-004.
Ordre de fusion : Avant QR-009 (le tableau de bord a besoin des jours fériés pour la détection des absences).
Complexité estimée : S.
Histoires utilisateur PRD : N/A — prend en charge US-QR-010 (tableau de bord), US-QR-011 (rapports), US-QR-012 (heures supplémentaires).


Objectif

Fournir au module QR Attendance un calendrier des jours fériés (jours fériés CI pré-enregistrés par la plateforme) et permettre aux RH de gérer les jours chômés spécifiques à l'entreprise. Les jours fériés et les jours chômés de l'entreprise sont utilisés en aval pour déterminer si une présence est attendue à une date donnée : les jours fériés et les jours chômés, les employés ne sont pas signalés comme absents, et tout scan est classé comme heures supplémentaires SUNDAY_HOLIDAY.


Portée du backend

Migrations Flyway

V001_009__attendance_public_holiday.sql :

CREATE TABLE attendance_public_holiday (
    id UUID PRIMARY KEY,
    country_code VARCHAR(3) NOT NULL DEFAULT 'CI',
    year INTEGER NOT NULL,
    holiday_date DATE NOT NULL,
    name_fr VARCHAR(200) NOT NULL,
    holiday_code VARCHAR(50),
    is_fixed BOOLEAN NOT NULL DEFAULT true,
    source VARCHAR(20) NOT NULL DEFAULT 'PLATFORM',
    tenant_id UUID,
    synced_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(country_code, year, holiday_date, COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'))
);
CREATE INDEX idx_holiday_country_year ON attendance_public_holiday(country_code, year, holiday_date);

V001_010__présence_entreprise_jour_non_ouvrable.sql

:

CREATE TABLE attendance_company_non_working_day (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    day_date DATE NOT NULL,
    name VARCHAR(200) NOT NULL,
    is_recurring BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(tenant_id, day_date)
);
CREATE INDEX idx_company_nwd_tenant ON attendance_company_non_working_day(tenant_id, day_date);

V001_011__attendance_ci_holidays_seed.sql

(données de départ) :

-- CI public holidays 2026-2027 (fixed dates)
INSERT INTO attendance_public_holiday (id, country_code, year, holiday_date, name_fr, holiday_code, is_fixed, source) VALUES
(gen_random_uuid(), 'CI', 2026, '2026-01-01', 'Jour de l''An', 'NEW_YEAR', true, 'PLATFORM'),
(gen_random_uuid(), 'CI', 2026, '2026-05-01', 'Fete du Travail', 'LABOUR_DAY', true, 'PLATFORM'),
(gen_random_uuid(), 'CI', 2026, '2026-08-07', 'Fete nationale', 'INDEPENDENCE_DAY', true, 'PLATFORM'),
(gen_random_uuid(), 'CI', 2026, '2026-11-15', 'Journee nationale de la Paix', 'PEACE_DAY', true, 'PLATFORM'),
(gen_random_uuid(), 'CI', 2026, '2026-12-25', 'Noel', 'CHRISTMAS', true, 'PLATFORM'),
(gen_random_uuid(), 'CI', 2026, '2026-11-01', 'Toussaint', 'ALL_SAINTS', true, 'PLATFORM'),
(gen_random_uuid(), 'CI', 2026, '2026-08-15', 'Assomption', 'ASSUMPTION', true, 'PLATFORM');
-- Moveable Islamic holidays for 2026 (approximate -- to be confirmed annually)
-- Tabaski, Maouloud, Laylat al-Qadr, etc. -- dates updated by platform admin

Entités et enregistrements

« AttendancePublicHoliday » (partagée — au niveau de la plateforme, avec possibilité de modification au niveau du tenant) :

Field Type Constraints
id UUID PK
countryCode String NOT NULL, default "CI", max 3
year int NOT NULL
holidayDate LocalDate NOT NULL
nameFr String NOT NULL, max 200
holidayCode String Nullable, max 50 (e.g., "NEW_YEAR", "LABOUR_DAY")
isFixed boolean NOT NULL, default true
source String NOT NULL, "PLATFORM" or "TENANT_OVERRIDE"
tenantId UUID Nullable (null = platform-level, set = tenant override)
syncedAt OffsetDateTime NOT NULL

CompanyNonWorkingDay (base de données du tenant) :

Field Type Constraints
id UUID PK
tenantId UUID NOT NULL
dayDate LocalDate NOT NULL, UNIQUE with tenantId
name String NOT NULL, max 200
isRecurring boolean NOT NULL, default false
createdAt OffsetDateTime NOT NULL

DTO :

public record HolidayResponse(
    UUID id,
    String countryCode,
    int year,
    LocalDate holidayDate,
    String nameFr,
    String holidayCode,
    boolean isFixed,
    String source
) {}

public record NonWorkingDayResponse(
    UUID id,
    LocalDate dayDate,
    String name,
    boolean isRecurring,
    OffsetDateTime createdAt
) {}

public record CreateNonWorkingDayRequest(
    @NotNull LocalDate dayDate,
    @NotBlank @Size(max = 200) String name,
    boolean isRecurring
) {}

Couche de référentiel

  • AttendancePublicHolidayRepository.java :
  • findByCountryCodeAndYear(String countryCode, int year) -> List<ATTENDANCEPUBLICHOLIDAY> — Renvoie les jours fériés au niveau de la plateforme (tenantId est NULL)
  • findByCountryCodeAndYearAndTenantId(String countryCode, int year, UUID tenantId) -> List<ATTENDANCEPUBLICHOLIDAY> — Renvoie les remplacements spécifiques au tenant
  • existsByCountryCodeAndHolidayDate(String countryCode, LocalDate date) -> boolean
  • CompanyNonWorkingDayRepository.java :
  • findByTenantIdAndYear(UUID tenantId, int year) -> List<CompanyNonWorkingDay> — Requête personnalisée filtrant par EXTRACT(YEAR FROM day_date)
  • findByTenantIdAndDayDate(UUID tenantId, LocalDate date) -> Optional<CompanyNonWorkingDay>
  • existsByTenantIdAndDayDate(UUID tenantId, LocalDate date) -> boolean

Couche de service

HolidayService.java:

  • listHolidays(String countryCode, int year) -> List<HOLIDAYRESPONSE> — Renvoie tous les jours fériés pour le pays et l'année donnés (au niveau de la plateforme)
  • isHoliday(UUID tenantId, LocalDate date) -> boolean — Vérifie si la date est un jour férié (plateforme ou redéfinition par le locataire)
  • isNonWorkingDay(UUID tenantId, LocalDate date) -> boolean — Combine les jours fériés et les jours chômés de l'entreprise. Renvoie true si la date correspond à l'un ou l'autre.

HolidayEventListener.java:

  • Écoute l'événement PublicHolidayUpdated provenant du module ABSMGT (événement d'application Spring Modulith)
  • Actualise le cache des jours fériés locaux lorsque le module de gestion des absences met à jour les jours fériés

CompanyNonWorkingDayService.java:

  • createNonWorkingDay(UUID tenantId, CreateNonWorkingDayRequest) -> NonWorkingDayResponse — Crée un jour chômé de l'entreprise. Si isRecurring=true, le jour s'applique à la même date chaque année. Publie un événement d'audit.
  • listNonWorkingDays(UUID tenantId, int year) -> List<NONWORKINGDAYRESPONSE> — Renvoie tous les jours chômés de l'entreprise pour l'année (y compris les jours récurrents de toute année correspondant à la date)
  • deleteNonWorkingDay(UUID tenantId, UUID id) -> void — Supprime un jour chômé de l'entreprise. Publie un événement d'audit.

Endpoints API

Method Path Request Body Response Auth
GET /api/v1/qrcontr/holidays ?year=2026&countryCode=CI ApiResponse> HR, Manager
POST /api/v1/qrcontr/company-non-working-days CreateNonWorkingDayRequest ApiResponse HR (P2)
GET /api/v1/qrcontr/company-non-working-days ?year=2026 ApiResponse> HR, Manager
DELETE /api/v1/qrcontr/company-non-working-days/{id} ApiResponse HR (P2)

Règles de validation

  • Le paramètre de requête year est obligatoire pour l'endpoint « holidays »
  • La valeur par défaut de countryCode est « CI » s'il n'est pas fourni
  • dayDate est obligatoire pour la création d'un jour chômé
  • name est obligatoire, 200 caractères maximum
  • Date en double par locataire pour les jours chômés -> erreur 409
  • Impossible de créer un jour chômé à une date qui est déjà un jour férié (avertissement informatif, pas un blocage)

Considérations multi-tenant

  • Jours fériés : tenant_id peut être nul. Les jours fériés au niveau de la plateforme ont tenant_id = NULL. Les remplacements par le locataire ont tenant_id défini.
  • Jours chômés de l'entreprise : toujours limités par tenant_id
  • isHoliday() et isNonWorkingDay() combinent les deux sources pour un locataire donné
  • Toutes les requêtes concernant les jours chômés de l'entreprise DOIVENT être filtrées par tenant_id

Portée du frontend

Pages et routes

  • /qrcontr/holidays — Calendrier des jours fériés + jours chômés de l'entreprise (RH)

Composants

  • HolidayCalendarPage.tsx — Vue annuelle avec les jours fériés mis en évidence, jours chômés de l'entreprise modifiables
  • HolidayList.tsx — Liste des jours fériés pour l'année sélectionnée (en lecture seule)
  • NonWorkingDayList.tsx — Liste des jours chômés de l'entreprise avec action de suppression
  • NonWorkingDayForm.tsx — Sélecteur de date + champ de nom + bouton de répétition

États de l'interface utilisateur (TOUS OBLIGATOIRES)

State Behavior French Copy
Loading Calendar skeleton
Empty Calendar with no custom days, public holidays shown "Aucun jour non travaille specifique a l'entreprise."
Error Inline error "Une erreur est survenue. Reessayez."
Offline View cached holidays, edit disabled "Disponible en ligne uniquement"
Success Day highlighted on calendar "Jour non travaille ajoute."

Comportement adaptatif

  • 360px (mobile) : affichage du calendrier mensuel, défilable. Formulaire des jours fériés sous forme de volet en bas de page.
  • 768px (tablette) : vue mensuelle avec panneau latéral pour les détails et formulaire des jours fériés
  • 1024px+ (ordinateur de bureau) : aperçu de l'année complète, modification en ligne des jours fériés

Interactions

  • Navigation par année : « < 2025 | 2026 | 2027 >" arrows to switch year
  • Jours fériés : mis en évidence dans le calendrier par un badge de couleur. Appuyez pour afficher le nom. En lecture seule.
  • Jours chômés de l'entreprise : badge de couleur différente. Appuyez pour afficher le nom + option de suppression.
  • Ajouter un jour chômé : FAB (mobile) ou bouton « Ajouter » (ordinateur) -> formulaire avec sélecteur de date, nom, bouton de récurrence
  • Bouton de récurrence : libellé « Récurrent chaque année » avec un bouton bascule
  • Confirmation de suppression : « Supprimer ce jour non ouvré ? » avec les boutons « Annuler » + « Supprimer »

Critères d'acceptation

AC-001: Public holiday calendar display
Given I am HR viewing the holiday calendar
When the page loads for year 2026 and country CI
Then I see all pre-loaded public holidays for Cote d'Ivoire with their French names and dates

AC-002: Company non-working day management
Given I am HR
When I add a company-specific non-working day with name "Anniversaire de l'entreprise" on date 2026-06-15
Then the day is saved and treated identically to a public holiday for attendance purposes
And I can mark it as recurring (annual) or one-time

OHADA et règles réglementaires

  • FR-QR-080 : Calendrier des jours fériés en Côte d'Ivoire géré par ALTARYS, mis à jour chaque année.
  • FR-QR-082 : Les jours fériés, aucune présence n'est attendue et les employés ne sont pas signalés comme absents. Si un employé pointe, les heures sont classées comme heures supplémentaires « SUNDAY_HOLIDAY ».
  • Les jours fériés islamiques (Aïd al-Adha, Mawlid) sont mobiles — les dates changent chaque année et sont mises à jour par l'administrateur de la plateforme.

Normes et conventions

  • docs/standards/java-spring-guidelines.md — Enregistrements d'entités, couche de service, écouteurs d'événements
  • docs/standards/api-guidelines.md — Enveloppe ApiResponse, paramètres de requête
  • docs/standards/database-guidelines.md — Conception de tables multi-tenant, tenant_id pouvant être nul pour les données de la plateforme
  • docs/standards/react-typescript-guidelines.md — Structure des composants, modèles de calendrier

Exigences de test

Tests unitaires

  • HolidayService.isHoliday : renvoie true pour une date de jour férié de la plateforme
  • HolidayService.isHoliday : renvoie true pour une date de remplacement définie par le tenant
  • HolidayService.isNonWorkingDay : renvoie true pour un jour chômé de l'entreprise
  • HolidayService.isNonWorkingDay : renvoie true pour un jour férié
  • HolidayService.isNonWorkingDay : renvoie false pour un jour ouvrable normal
  • Un jour chômé récurrent correspond à la même date d'une année à l'autre

Tests d'intégration

  • GET /holidays?year=2026&countryCode=CI -> renvoie les jours fériés prédéfinis pour le code pays CI
  • POST /company-non-working-days -> crée un jour chômé
  • POST /company-non-working-days avec une date en double -> 409
  • GET /company-non-working-days?year=2026 -> renvoie tous les jours fériés de l'année, y compris les jours fériés récurrents
  • DELETE /company-non-working-days/{id} -> supprime un jour férié
  • Isolation des locataires : les jours fériés du locataire A ne sont pas visibles pour le locataire B

Ce que le contrôle qualité va valider

  • Le calendrier des jours fériés s'affiche correctement sur tous les points de rupture (360px, 768px, 1024px)
  • Les jours fériés de la CI pour 2026 s'affichent avec les noms français corrects
  • La navigation par année fonctionne
  • Ajout d'un jour chômé de l'entreprise avec date et nom
  • Le bouton de récurrence fonctionne
  • Suppression d'un jour chômé avec confirmation
  • L'état vide affiche un message d'aide
  • L'état hors ligne affiche les données mises en cache et bloque les modifications

Hors du périmètre

  • Classification des heures supplémentaires pendant les jours fériés (QR-018)
  • Détection des absences à l'aide des jours fériés (QR-009)
  • Prise en charge des jours fériés dans plusieurs pays au-delà de la France (V2)
  • Modification des dates des jours fériés (gérée par l'administrateur de la plateforme, et non par le service RH du tenant)

Définition de « terminé »

  • [ ] Tous les endpoints backend sont implémentés et renvoient des réponses correctes
  • [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px et 1024px
  • [ ] Les 5 états de l'interface utilisateur sont implémentés (chargement, vide, erreur, hors ligne, succès)
  • [ ] Les micro-textes en français correspondent exactement aux spécifications de conception
  • [ ] Tous les critères d'acceptation sont validés manuellement
  • [ ] Tests unitaires rédigés et réussis
  • [ ] Tests d'intégration rédigés et réussis
  • [ ] Isolation multi-tenant vérifiée
  • [ ] Données de base pour les jours fériés 2026-2027 chargées
  • [ ] Pas de any en TypeScript, pas de types bruts en Java
  • [ ] Les réponses API respectent le format d'enveloppe standard
  • [ ] Entrées de piste d'audit pour les modifications des jours non ouvrables de l'entreprise