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 |
| 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 :
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) → intsave(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 100last_name: min 2, max 100email: format valide si fourni, unique par tenantphone: unique par tenant si fournihire_date: pas plus d'1 an dans le futurcontract_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
versiondoit 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— tableemployees, tablematricule_counters
Remarque : La table
manager_historyest 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 statutEmployeeCard.tsx— Élément de liste employé (vue carte mobile)EmployeeRow.tsx— Ligne de tableau employé (vue bureau)EmployeeDetailPage.tsx— Vue détail employé uniqueEmployeeFormPage.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 Eventsdocs/standards/database-guidelines.md— §FK auto-référençantes, §Tables de compteur (FOR UPDATE), §Contraintes d'unicité tenant-scopéesdocs/standards/api-guidelines.md— §Patterns CRUD, §Pagination, §Filtragedocs/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
anyTypeScript — 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