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 tenantexistsByCountryCodeAndHolidayDate(String countryCode, LocalDate date) -> booleanCompanyNonWorkingDayRepository.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
PublicHolidayUpdatedprovenant 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. SiisRecurring=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
yearest obligatoire pour l'endpoint « holidays » - La valeur par défaut de
countryCodeest « CI » s'il n'est pas fourni dayDateest obligatoire pour la création d'un jour chôménameest 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_idpeut être nul. Les jours fériés au niveau de la plateforme onttenant_id = NULL. Les remplacements par le locataire onttenant_iddéfini. - Jours chômés de l'entreprise : toujours limités par
tenant_id isHoliday()etisNonWorkingDay()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 modifiablesHolidayList.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 suppressionNonWorkingDayForm.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énementsdocs/standards/api-guidelines.md— Enveloppe ApiResponse, paramètres de requêtedocs/standards/database-guidelines.md— Conception de tables multi-tenant, tenant_id pouvant être nul pour les données de la plateformedocs/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
anyen 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