Aller au contenu

Story CP-017: Employee CRUD + 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

⚠️ Pre-existing event stubs (2026-04-22) — The record classes EmployeeCreated, EmployeeUpdated, and EmployeeTerminated were created ahead of this story on branch feat/control-plane-employee-events to unblock consumers (AB-003, AB-022, QR-002). This story still owns publishing these events (wiring them into the service layer, @TransactionalEventListener contracts, and integration tests). Do not re-create the records; extend them only if the Control Plane needs additional fields.


Objective

Deliver the Employee entity with online CRUD, server-assigned matricule (immutable), status filtering, and the default approver fallback. This story creates the core employee data model and the online employee management experience. Offline creation, manager history, and PRE_HIRE transition are in CP-018.


Backend Scope

Entities & Records

Employee entity (tenant DB):

Field Type Constraints
id UUID v7 PK, client-generated
tenant_id UUID NOT NULL
matricule String Nullable (assigned by server, unique within tenant)
first_name String NOT NULL, min 2, max 100
last_name String NOT NULL, min 2, max 100
email String Nullable (not all employees have email)
phone String Nullable
contract_type_id UUID NOT NULL, FK to ContractType
org_unit_id UUID NOT NULL, FK to OrgUnit
position_id UUID NOT NULL, FK to Position
category_id UUID NOT NULL, FK to EmployeeCategory (derived from position or directly set)
hire_date LocalDate NOT NULL
manager_id UUID Nullable, FK to Employee (self-referencing)
status EmployeeStatus enum NOT NULL
status_effective_date LocalDate NOT NULL
version long NOT NULL, default 0 (optimistic locking)
created_date Instant NOT NULL
updated_date Instant NOT NULL

EmployeeStatus enum:

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

Note: ON_LEAVE is NOT a status. It's derived: IF status=ACTIVE AND an approved leave overlaps today, the UI shows "En conge" badge.

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) {}

Repository Layer

  • 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

Service Layer

EmployeeService.java: - list(filters, Pageable) → EmployeeListResponse — Filterable by status, org_unit, category, search text - getById(UUID) → EmployeeResponse - create(CreateEmployeeRequest) → EmployeeResponse — Validates UUID v7 format from client. Assigns matricule via MatriculeService. Sets status based on hire_date (future = PRE_HIRE, today/past = ACTIVE). Publishes EmployeeCreated event. Validates org_unit is ACTIVE. Validates contract_type, position, category exist and are active. - update(UUID, UpdateEmployeeRequest) → EmployeeResponse — Optimistic locking via version field. Matricule is immutable (ignored if present in request). Publishes EmployeeUpdated event. - getManagerOrDefault(UUID employeeId) → ManagerResponse — Returns employee's manager, or tenant's default approver if manager_id is null.

MatriculeService.java: - assignMatricule(UUID tenantId, LocalDate hireDate) → String — Thread-safe, per-tenant sequential numbering. Uses FOR UPDATE on a counter table partitioned by year-month. Format: {settings.matriculePrefix}-{YEAR}-{MM}-{zeroPadded(counter, settings.matriculePadding)}. Example: ALT-2026-03-001, ALT-2026-03-002. Follows CLAUDE.md rule 15: {PREFIX}-{YEAR}-{MM}-{SEQ} where PREFIX = company code trigram (3-6 uppercase alphanumeric chars, set in TenantSettings.matriculePrefix during provisioning). Matricule is immutable once assigned. - Counter table: matricule_counters(tenant_id, year_month, prefix, padding, last_number) — initialized at 1 per year-month during first use.

API Endpoints

Method Path Request Body Response Auth
GET /api/v1/employees ?status=&orgUnit=&search= EmployeeListResponse ROLE_DG, ROLE_RH_MANAGER, ROLE_MANAGER (direct reports only)
GET /api/v1/employees/{id} EmployeeResponse ROLE_DG, ROLE_RH_MANAGER, ROLE_MANAGER (if direct report), self
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} or default approver Any authenticated

Validation Rules

  • UUID v7 format validated (client-generated)
  • first_name: min 2, max 100
  • last_name: min 2, max 100
  • email: valid format if provided, unique within tenant
  • phone: unique within tenant if provided
  • hire_date: not more than 1 year in the future
  • contract_type_id, org_unit_id, position_id: must reference ACTIVE entities within the same tenant
  • Matricule: assigned by server, immutable once set — update requests cannot change it
  • Optimistic locking: version field must match current DB version (409 on stale)

Multi-Tenant Considerations

  • Employee entity is in the tenant DB and tenant-scoped
  • EmployeeRepository extends TenantAwareRepository
  • Matricule counters are per-tenant

Flyway Migrations (Tenant DB)

  • V005__create_employee.sqlemployees table, matricule_counters table

Note: manager_history table is in CP-018.


Frontend Scope

Pages & Routes

  • /rh/employees — Employee list
  • /rh/employees/:id — Employee detail
  • /rh/employees/new — Employee creation form
  • /rh/employees/:id/edit — Employee edit form

Components

  • EmployeeListPage.tsx — Filterable list with status filter chips
  • EmployeeCard.tsx — Employee list item (mobile card view)
  • EmployeeRow.tsx — Employee table row (desktop view)
  • EmployeeDetailPage.tsx — Single employee detail view
  • EmployeeFormPage.tsx — Create and edit employee form

UI States (ALL REQUIRED)

Employee List:

State Behavior French Copy
Loading Skeleton: 5 employee cards
Empty Illustration + message + CTA "Aucun employe pour le moment.\nAjoutez votre premier collaborateur !" CTA: "Ajouter un employe"
Error Red toast "Une erreur est survenue. Veuillez reessayer."
Success (create online) Green toast "Employe cree"

Note: Offline states (orange badge, offline toast) are in CP-018.

Employee card states:

State Visual
Active Green dot
Pre-hire Blue dot + "Embauche le DD/MM"
Suspended Orange dot + "Suspendu" badge
Terminated Grey dot + "Fin de contrat" badge

Note: Pending sync state (orange left border + sync icon) is in CP-018.

Responsive Behavior

Employee List: - 360px: Card list. Each card: avatar, name, matricule (or "Numero en attente" if not yet assigned), position, department, status dot. FAB "Nouvel employe" at bottom. - 768px: Two-column card grid. - 1024px+: Table view with columns: Matricule, Nom complet, Poste, Departement, Categorie, Contrat, Statut, Manager. Column sorting. Multi-filter bar.

Employee Form: - 360px: Single column, all fields stacked. - 1024px+: Two column form.

Interactions

Employee creation (online): 1. Tap "Nouvel employe" --> form page 2. Fill fields. Position field = PositionCombobox from CP-005 (search existing OR type new --> auto-creates) 3. Category auto-fills from selected position (editable if needed) 4. Manager field = employee search combobox 5. Submit --> UUID v7 generated client-side --> POST /api/v1/employees --> matricule assigned --> toast "Employe cree"


Acceptance Criteria

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

OHADA & Regulatory Rules

  • Code du Travail Art. 92.3 (Registre d'employeur): The Employee entity stores baseline fields required for the employer register. Full register in HR Core module.
  • CDD 2-year limit (Art. 15.10): CDD contracts exceeding 2 years cumulative automatically become CDI. The rule is stored in ContractType.max_duration_months (from CP-005). Alerts at 90/30/7 days — enforcement in Employee Management module, but the data model supports it from day one.
  • Convention Collective Interprofessionnelle (CCI): Employee categories (Cadre, AM, Employe, Ouvrier) from CP-005 affect CNPS contribution rates.
  • Decret 2024-901: Employee count tracking (countByStatus) enables the >10 workers threshold check for staff representative elections.
  • Employee data is personal data (Loi 2013-450): first_name, last_name, email, phone are PII. Audit trail tracks all modifications. Terminated employees are never hard-deleted (AUDCIF Art. 24).

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Entities, §UUID v7, §Optimistic locking, §Application Events
  • docs/standards/database-guidelines.md — §Self-referencing FKs, §Counter tables (FOR UPDATE), §Tenant-scoped unique constraints
  • docs/standards/api-guidelines.md — §CRUD patterns, §Pagination, §Filtering
  • docs/standards/react-typescript-guidelines.md — §List/card/table components, §Form components, §Combobox pattern, §FAB

Testing Requirements

Unit Tests

  • MatriculeService: sequential numbering, zero-padding, custom prefix
  • MatriculeService: thread-safety (concurrent calls produce unique matricules)
  • EmployeeService.create: PRE_HIRE status for future hire_date, ACTIVE for today/past
  • EmployeeService.create: validates UUID v7 format from client
  • EmployeeService.create: rejects inactive org unit assignment
  • EmployeeService.update: matricule immutability (ignored in update)
  • EmployeeService.update: optimistic locking (stale version → 409)
  • EmployeeService.getManagerOrDefault: returns default approver when manager_id is null

Integration Tests

  • POST /api/v1/employees — full creation flow with matricule assignment
  • POST /api/v1/employees — duplicate email within tenant → 409
  • GET /api/v1/employees — pagination, filtering, search
  • GET /api/v1/employees/{id}/manager — with and without manager
  • Tenant isolation: all entities scoped to tenant
  • Optimistic locking: concurrent updates → 409 on stale version

What QA Will Validate

  • Employee list displays correctly on all breakpoints (cards on mobile, table on desktop)
  • Employee creation form works online
  • Position combobox: search existing, create new (escape hatch from CP-005)
  • Category auto-fills from position selection
  • Matricule assigned and displayed after creation
  • Status filters work on employee list
  • Status dots display correctly for each employee state

Out of Scope

  • Employee offline creation + sync — CP-018
  • Manager hierarchy historization — CP-018
  • PRE_HIRE automatic transition — CP-018
  • Employee list offline mode — CP-018
  • Full employee dossier (photo, emergency contacts, bank info, documents) — HR Core module
  • Employee onboarding/offboarding workflows — Employee Management module
  • Bulk employee import — V2
  • Employee self-service profile view — V2

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] All frontend pages render correctly at 360px, 768px, 1024px
  • [ ] All UI states implemented (loading, empty, error, 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 (no cross-tenant data leaks)
  • [ ] Matricule assignment is thread-safe and unique
  • [ ] No TypeScript any — all types explicit
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries created for every data mutation