Aller au contenu

Story CP-017 : CRUD Employé + Matricule

Module : control-plane Slice : Slice 5 from architecture (split from CP-005) Brand context : Papillon HR Suite (mobile-first, offline-first, warm UX) Priority : 5 Depends on : CP-005 Estimated complexity : L


Objectif

Livrer l'entité Employee avec CRUD en ligne, matricule assigné par le serveur (immuable), filtrage par statut et le fallback d'approbateur par défaut. Cette story crée le modèle de données employé principal et l'expérience de gestion des employés en ligne. La création hors ligne, l'historique des managers et la transition PRE_HIRE sont dans CP-018.


Périmètre Backend

Entités & Records

Entité Employee (base tenant) :

Champ Type Contraintes
id UUID v7 PK, généré côté client
tenant_id UUID NOT NULL
matricule String Nullable (assigné par le serveur, unique par tenant)
first_name String NOT NULL, min 2, max 100
last_name String NOT NULL, min 2, max 100
email String Nullable (tous les employés n'ont pas d'email)
phone String Nullable
contract_type_id UUID NOT NULL, FK vers ContractType
org_unit_id UUID NOT NULL, FK vers OrgUnit
position_id UUID NOT NULL, FK vers Position
category_id UUID NOT NULL, FK vers EmployeeCategory (dérivé du poste ou défini directement)
hire_date LocalDate NOT NULL
manager_id UUID Nullable, FK vers Employee (auto-référencement)
status EmployeeStatus enum NOT NULL
status_effective_date LocalDate NOT NULL
version long NOT NULL, défaut 0 (verrouillage optimiste)
created_date Instant NOT NULL
updated_date Instant NOT NULL

Enum EmployeeStatus :

public enum EmployeeStatus { PRE_HIRE, ACTIVE, SUSPENDED, TERMINATED }

Remarque : ON_LEAVE n'est PAS un statut. Il est dérivé : SI status=ACTIVE ET qu'un congé approuvé chevauche aujourd'hui, l'UI affiche le badge "En congé".

DTOs :

public record EmployeeResponse(UUID id, String matricule, String firstName, String lastName,
    String email, String phone, String contractTypeCode, String orgUnitName, String positionTitle,
    String categoryCode, LocalDate hireDate, @Nullable UUID managerId,
    @Nullable String managerName, EmployeeStatus status, LocalDate statusEffectiveDate) {}

public record EmployeeListResponse(List<EmployeeResponse> employees, int total) {}

public record CreateEmployeeRequest(UUID id, String firstName, String lastName,
    @Nullable String email, @Nullable String phone, UUID contractTypeId, UUID orgUnitId,
    UUID positionId, UUID categoryId, LocalDate hireDate, @Nullable UUID managerId) {}

public record UpdateEmployeeRequest(@Nullable String firstName, @Nullable String lastName,
    @Nullable String email, @Nullable String phone, @Nullable UUID contractTypeId,
    @Nullable UUID orgUnitId, @Nullable UUID positionId, @Nullable UUID categoryId,
    @Nullable LocalDate hireDate, @Nullable UUID managerId, long version) {}

Couche Repository

  • EmployeeRepository.java — Tenant-aware :
  • findAll(filters, Pageable) → Page<Employee>
  • findById(UUID) → Optional<Employee>
  • findByEmail(String) → Optional<Employee>
  • findByManagerId(UUID) → List<Employee>
  • countByStatus(EmployeeStatus) → int
  • save(Employee) → Employee

Couche Service

EmployeeService.java : - list(filters, Pageable) → EmployeeListResponse — Filtrable par statut, org_unit, catégorie, texte de recherche - getById(UUID) → EmployeeResponse - create(CreateEmployeeRequest) → EmployeeResponse — Valide le format UUID v7 côté client. Assigne le matricule via MatriculeService. Définit le statut en fonction de hire_date (futur = PRE_HIRE, aujourd'hui/passé = ACTIVE). Publie l'événement EmployeeCreated. Valide que org_unit est ACTIVE. Valide que contract_type, position, category existent et sont actifs. - update(UUID, UpdateEmployeeRequest) → EmployeeResponse — Verrouillage optimiste via le champ version. Le matricule est immuable (ignoré si présent dans la requête). Publie l'événement EmployeeUpdated. - getManagerOrDefault(UUID employeeId) → ManagerResponse — Retourne le manager de l'employé, ou l'approbateur par défaut du tenant si manager_id est null.

MatriculeService.java : - assignMatricule(UUID tenantId, LocalDate hireDate) → String — Numérotation séquentielle par tenant, thread-safe. Utilise FOR UPDATE sur une table de compteur partitionnée par année-mois. Format : {settings.matriculePrefix}-{YEAR}-{MM}-{zeroPadded(counter, settings.matriculePadding)}. Exemple : ALT-2026-03-001, ALT-2026-03-002. Suit la règle 15 de CLAUDE.md : {PREFIX}-{YEAR}-{MM}-{SEQ} où PREFIX = trigramme du code entreprise (3-6 caractères alphanumériques majuscules, défini dans TenantSettings.matriculePrefix lors du provisionnement). Le matricule est immuable une fois assigné. - Table de compteur : matricule_counters(tenant_id, year_month, prefix, padding, last_number) — initialisée à 1 par année-mois lors de la première utilisation.

Endpoints API

Méthode Chemin Corps de requête Réponse Auth
GET /api/v1/employees ?status=&orgUnit=&search= EmployeeListResponse ROLE_DG, ROLE_RH_MANAGER, ROLE_MANAGER (subordonnés directs uniquement)
GET /api/v1/employees/{id} EmployeeResponse ROLE_DG, ROLE_RH_MANAGER, ROLE_MANAGER (si subordonné direct), soi-même
POST /api/v1/employees CreateEmployeeRequest EmployeeResponse ROLE_DG, ROLE_RH_MANAGER
PATCH /api/v1/employees/{id} UpdateEmployeeRequest EmployeeResponse ROLE_DG, ROLE_RH_MANAGER
GET /api/v1/employees/{id}/manager {managerId, managerName} ou approbateur par défaut Tout utilisateur authentifié

Règles de validation

  • Format UUID v7 validé (généré côté client)
  • first_name : min 2, max 100
  • last_name : min 2, max 100
  • email : format valide si fourni, unique par tenant
  • phone : unique par tenant si fourni
  • hire_date : pas plus d'1 an dans le futur
  • contract_type_id, org_unit_id, position_id : doivent référencer des entités ACTIVE dans le même tenant
  • Matricule : assigné par le serveur, immuable une fois défini — les requêtes de mise à jour ne peuvent pas le modifier
  • Verrouillage optimiste : le champ version doit correspondre à la version en base (409 si obsolète)

Considérations multi-tenant

  • L'entité Employee est dans la base tenant et tenant-scopée
  • EmployeeRepository étend TenantAwareRepository
  • Les compteurs de matricule sont par tenant

Migrations Flyway (Base Tenant)

  • V005__create_employee.sql — table employees, table matricule_counters

Remarque : La table manager_history est dans CP-018.


Périmètre Frontend

Pages & Routes

  • /rh/employees — Liste des employés
  • /rh/employees/:id — Détail employé
  • /rh/employees/new — Formulaire de création d'employé
  • /rh/employees/:id/edit — Formulaire de modification d'employé

Composants

  • EmployeeListPage.tsx — Liste filtrable avec chips de filtres de statut
  • EmployeeCard.tsx — Élément de liste employé (vue carte mobile)
  • EmployeeRow.tsx — Ligne de tableau employé (vue bureau)
  • EmployeeDetailPage.tsx — Vue détail employé unique
  • EmployeeFormPage.tsx — Formulaire de création et modification d'employé

États UI (TOUS REQUIS)

Liste des employés :

État Comportement Texte en Français
Loading Squelette : 5 cartes employé
Empty Illustration + message + CTA "Aucun employe pour le moment.\nAjoutez votre premier collaborateur !" CTA: "Ajouter un employe"
Error Toast rouge "Une erreur est survenue. Veuillez reessayer."
Success (création en ligne) Toast vert "Employe cree"

Remarque : Les états hors ligne (badge orange, toast hors ligne) sont dans CP-018.

États des cartes employé :

État Visuel
Actif Point vert
Pré-embauche Point bleu + "Embauche le DD/MM"
Suspendu Point orange + badge "Suspendu"
Fin de contrat Point gris + badge "Fin de contrat"

Remarque : L'état synchronisation en attente (bordure gauche orange + icône sync) est dans CP-018.

Comportement responsive

Liste des employés : - 360px : Liste de cartes. Chaque carte : avatar, nom, matricule (ou "Numero en attente" si pas encore assigné), poste, département, point de statut. FAB "Nouvel employe" en bas. - 768px : Grille de cartes à deux colonnes. - 1024px+ : Vue tableau avec colonnes : Matricule, Nom complet, Poste, Département, Catégorie, Contrat, Statut, Manager. Tri par colonne. Barre multi-filtres.

Formulaire employé : - 360px : Colonne unique, tous les champs empilés. - 1024px+ : Formulaire à deux colonnes.

Interactions

Création d'employé (en ligne) : 1. Appuyer sur "Nouvel employe" → page de formulaire 2. Remplir les champs. Champ Poste = PositionCombobox de CP-005 (rechercher existant OU saisir nouveau → création automatique) 3. La Catégorie se remplit automatiquement depuis le poste sélectionné (modifiable si nécessaire) 4. Champ Manager = combobox de recherche d'employé 5. Soumettre → UUID v7 généré côté client → POST /api/v1/employees → matricule assigné → toast "Employe cree"


Critères d'acceptation

AC-034: Create employee online
Given I am RH Manager and online
When I submit the employee form with valid data
Then the employee is created with a UUID v7 ID (client-generated)
And the server assigns a matricule (e.g., "ALT-2026-03-001")
And I see the toast "Employe cree"

AC-037: Employee list with status filters
Given 12 employees exist (10 active, 1 pre-hire, 1 terminated)
When I view the employee list with no filter
Then I see "12 employe(s)" header
When I filter by status "Actif"
Then I see only the 10 active employees

AC-040: Matricule is immutable
Given employee "Jean" has matricule "ALT-2026-03-003"
When any update is made to the employee
Then the matricule remains "ALT-2026-03-003" (cannot be changed)

AC-041: Employee without manager routes to default approver
Given employee "Jean" has no manager (manager_id=null)
When GET /api/v1/employees/{id}/manager is called
Then it returns the tenant's default approver user

Règles OHADA & Réglementaires

  • Code du Travail Art. 92.3 (Registre d'employeur) : L'entité Employee stocke les champs de base requis pour le registre de l'employeur. Registre complet dans le module HR Core.
  • Limite CDD 2 ans (Art. 15.10) : Les contrats CDD dépassant 2 ans cumulatifs deviennent automatiquement CDI. La règle est stockée dans ContractType.max_duration_months (depuis CP-005). Alertes à 90/30/7 jours — application dans le module Employee Management, mais le modèle de données le supporte dès le premier jour.
  • Convention Collective Interprofessionnelle (CCI) : Les catégories d'employés (Cadre, AM, Employé, Ouvrier) de CP-005 affectent les taux de cotisations CNPS.
  • Décret 2024-901 : Le suivi du nombre d'employés (countByStatus) permet le contrôle du seuil de >10 travailleurs pour les élections des représentants du personnel.
  • Les données employé sont des données personnelles (Loi 2013-450) : first_name, last_name, email, phone sont des DCP. Le journal d'audit suit toutes les modifications. Les employés licenciés ne sont jamais supprimés définitivement (AUDCIF Art. 24).

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Entités, §UUID v7, §Verrouillage optimiste, §Application Events
  • docs/standards/database-guidelines.md — §FK auto-référençantes, §Tables de compteur (FOR UPDATE), §Contraintes d'unicité tenant-scopées
  • docs/standards/api-guidelines.md — §Patterns CRUD, §Pagination, §Filtrage
  • docs/standards/react-typescript-guidelines.md — §Composants liste/carte/tableau, §Composants formulaire, §Pattern Combobox, §FAB

Exigences de test

Tests unitaires

  • MatriculeService : numérotation séquentielle, zéro-padding, préfixe personnalisé
  • MatriculeService : thread-safety (appels concurrents produisent des matricules uniques)
  • EmployeeService.create : statut PRE_HIRE pour hire_date futur, ACTIVE pour aujourd'hui/passé
  • EmployeeService.create : valide le format UUID v7 côté client
  • EmployeeService.create : rejette l'assignation à une unité organisationnelle inactive
  • EmployeeService.update : immuabilité du matricule (ignoré à la mise à jour)
  • EmployeeService.update : verrouillage optimiste (version obsolète → 409)
  • EmployeeService.getManagerOrDefault : retourne l'approbateur par défaut quand manager_id est null

Tests d'intégration

  • POST /api/v1/employees — flux de création complet avec assignation de matricule
  • POST /api/v1/employees — email en doublon dans le tenant → 409
  • GET /api/v1/employees — pagination, filtrage, recherche
  • GET /api/v1/employees/{id}/manager — avec et sans manager
  • Isolation tenant : toutes les entités scopées au tenant
  • Verrouillage optimiste : mises à jour concurrentes → 409 sur version obsolète

Ce que le QA validera

  • La liste des employés s'affiche correctement sur tous les points de rupture (cartes sur mobile, tableau sur bureau)
  • Le formulaire de création d'employé fonctionne en ligne
  • Combobox de poste : rechercher existant, créer nouveau (échappatoire depuis CP-005)
  • La catégorie se remplit automatiquement depuis la sélection du poste
  • Le matricule est assigné et affiché après la création
  • Les filtres de statut fonctionnent sur la liste des employés
  • Les points de statut s'affichent correctement pour chaque état employé

Hors périmètre

  • Création d'employé hors ligne + sync — CP-018
  • Historisation de la hiérarchie de managers — CP-018
  • Transition automatique PRE_HIRE — CP-018
  • Mode hors ligne de la liste des employés — CP-018
  • Dossier employé complet (photo, contacts d'urgence, coordonnées bancaires, documents) — Module HR Core
  • Workflows d'onboarding/offboarding employé — Module Employee Management
  • Import en masse d'employés — V2
  • Vue profil en libre-service employé — V2

Définition de Done

  • [ ] Tous les endpoints backend implémentés et retournant les réponses correctes
  • [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px, 1024px
  • [ ] Tous les états UI implémentés (loading, empty, error, success)
  • [ ] Les micro-textes français correspondent exactement à la spec design
  • [ ] Tous les critères d'acceptation passent manuellement
  • [ ] Tests unitaires écrits et passants
  • [ ] Tests d'intégration écrits et passants
  • [ ] Isolation multi-tenant vérifiée (aucune fuite de données inter-tenant)
  • [ ] L'assignation de matricule est thread-safe et unique
  • [ ] Pas de any TypeScript — tous les types explicites
  • [ ] Pas de types Java bruts — tous les génériques explicites
  • [ ] Les réponses API respectent le format d'enveloppe standard