Aller au contenu

Story CP-005 : Structure Organisationnelle + Données de Référence

Module : control-plane Slice : Slice 5 from architecture Brand context : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority : 5 Depends on : CP-001, CP-002, CP-004 Estimated complexity : M


Objectif

Livrer le socle organisationnel et les données de référence dont dépendent tous les modules RH : l'arborescence hiérarchique des unités organisationnelles (Entité → Sites → Départements), le catalogue de postes avec création par mécanisme d'échappatoire, les catégories d'employés et les types de contrat. Ce slice établit les structures de données maîtresses. L'entité Employee (CRUD, matricule, hors ligne) est dans CP-017 et CP-018.


Périmètre Backend

Entités & Records

Entité OrgUnit (base tenant) :

Champ Type Contraintes
id UUID v7 PK
tenant_id UUID NOT NULL
name String NOT NULL, min 2, max 200
type OrgUnitType enum NOT NULL
parent_id UUID Nullable (null = Entité racine)
status OrgUnitStatus enum NOT NULL, défaut ACTIVE
created_date Instant NOT NULL
deactivated_date Instant Nullable

OrgUnitType : ENTITY, SITE, DEPARTMENT OrgUnitStatus : ACTIVE, INACTIVE

Entité Position (base tenant) :

Champ Type Contraintes
id UUID v7 PK
tenant_id UUID NOT NULL
title String NOT NULL, min 2, max 200
description String Nullable, max 500
category_id UUID NOT NULL, FK vers EmployeeCategory
department_id UUID Nullable, FK vers OrgUnit (DEPARTMENT)
status String NOT NULL, défaut "ACTIVE"
is_custom boolean NOT NULL, défaut false
created_date Instant NOT NULL

Entité EmployeeCategory (base tenant) :

Champ Type Contraintes
id UUID v7 PK
tenant_id UUID NOT NULL
country_code String(2) NOT NULL
name String NOT NULL, max 100
code String NOT NULL, max 10
description String Nullable
sort_order int NOT NULL
is_system boolean NOT NULL, défaut true
created_date Instant NOT NULL

Données de seed CI : Cadre (CAD), Agent de maîtrise (AM), Employé (EMP), Ouvrier (OUV)

Entité ContractType (base tenant) :

Champ Type Contraintes
id UUID v7 PK
tenant_id UUID NOT NULL
country_code String(2) NOT NULL
name String NOT NULL, max 200
code String NOT NULL, max 10
max_duration_months Integer Nullable
description String Nullable
is_system boolean NOT NULL, défaut true
status String NOT NULL, défaut "ACTIVE"
created_date Instant NOT NULL

Données de seed CI : CDI, CDD (max 24), Stage (max 24), Apprentissage (max 36), Journalier, CTT (max 24)

Remarque : L'entité Employee, l'entité ManagerHistory et tous les DTOs Employee sont dans CP-017. L'enum EmployeeStatus (PRE_HIRE, ACTIVE, SUSPENDED, TERMINATED) est défini dans CP-017.

DTOs :

// OrgUnit
public record OrgUnitTreeResponse(UUID id, String name, OrgUnitType type, String status,
    int employeeCount, List<OrgUnitTreeResponse> children) {}
public record CreateOrgUnitRequest(String name, OrgUnitType type, @Nullable UUID parentId) {}

// Position
public record PositionResponse(UUID id, String title, String categoryCode, String categoryName,
    @Nullable String departmentName, boolean isCustom) {}
public record CreatePositionRequest(String title, UUID categoryId, @Nullable UUID departmentId,
    @Nullable String description) {}

// Category
public record CategoryResponse(UUID id, String name, String code, String description, int sortOrder, boolean isSystem) {}

// ContractType
public record ContractTypeResponse(UUID id, String name, String code, @Nullable Integer maxDurationMonths,
    String description, boolean isSystem, String status) {}

Couche Repository

  • OrgUnitRepository.java — Tenant-aware : findByParentId, findAll, save
  • PositionRepository.java — Tenant-aware : findAll, findByTitle(search), save
  • EmployeeCategoryRepository.java — Tenant-aware : findAll, findByCode
  • ContractTypeRepository.java — Tenant-aware : findAll, findByCode

Remarque : EmployeeRepository et ManagerHistoryRepository sont dans CP-017.

Couche Service

OrgUnitService.java : - getTree(UUID tenantId) → List<OrgUnitTreeResponse> — Retourne l'arborescence complète avec le nombre d'employés - createUnit(CreateOrgUnitRequest) → OrgUnitTreeResponse — Valide les règles de hiérarchie (parent d'un Site = Entité, parent d'un Département = Entité ou Site), nom unique au sein du parent - deactivateUnit(UUID unitId) → void — Définit status=INACTIVE. N'affecte PAS les affectations d'employés existantes. Bloque les nouvelles affectations. Publie l'événement OrgUnitDeactivated.

PositionService.java : - list(UUID tenantId, @Nullable String search) → List<PositionResponse> — Recherche par titre - create(CreatePositionRequest) → PositionResponse — Crée un poste. Définit is_custom=true pour les postes créés via le mécanisme d'échappatoire.

Remarque : EmployeeService, MatriculeService et tous les endpoints employee sont dans CP-017.

Endpoints API

Méthode Chemin Corps de requête Réponse Auth
GET /api/v1/org-units/tree List Tout utilisateur authentifié
POST /api/v1/org-units CreateOrgUnitRequest OrgUnitTreeResponse ROLE_DG, ROLE_RH_MANAGER
PATCH /api/v1/org-units/{id}/deactivate 204 ROLE_DG, ROLE_RH_MANAGER
GET /api/v1/positions ?search= List Tout utilisateur authentifié
POST /api/v1/positions CreatePositionRequest PositionResponse ROLE_DG, ROLE_RH_MANAGER
GET /api/v1/categories List Tout utilisateur authentifié
GET /api/v1/contract-types List Tout utilisateur authentifié

Remarque : Tous les endpoints employee (/api/v1/employees/*) sont dans CP-017.

Règles de validation

  • OrgUnit : le parent d'un Site doit être ENTITY. Le parent d'un Département doit être ENTITY ou SITE. Nom unique au sein du parent.
  • Position : title min 2, max 200. category_id requis et doit exister.
  • EmployeeCategory : les catégories système (is_system=true) ne peuvent pas être supprimées, seulement désactivées.
  • Calculs financiers : N/A pour ce slice (aucun champ monétaire).

Considérations multi-tenant

  • TOUTES les entités de ce slice sont dans la base tenant et sont tenant-scoped
  • Chaque repository étend TenantAwareRepository

Migrations Flyway (base tenant)

  • V003__create_org_unit.sql — table org_units
  • V004__create_position_category.sql — tables positions, employee_categories, contract_types + données de seed CI

Remarque : V005__create_employee.sql (employees, matricule_counters) est dans CP-017. V006__create_manager_history.sql est dans CP-018.


Périmètre Frontend

Pages & Routes

  • /admin/settings/org-structure — Page arborescence organisationnelle
  • /admin/settings/categories — Gestion des catégories
  • /admin/settings/positions — Catalogue de postes
  • /admin/settings/contract-types — Liste des types de contrat

Remarque : Toutes les pages employee (/rh/employees/*) sont dans CP-017.

Composants

  • OrgTreePage.tsx, OrgTreeNode.tsx (spec composant dans design §19.4)
  • CategoryListPage.tsx
  • PositionCatalogPage.tsx, PositionCombobox.tsx (spec composant dans design §19.5)
  • ContractTypeListPage.tsx

Remarque : EmployeeListPage, EmployeeCard, EmployeeRow, EmployeeDetailPage, EmployeeFormPage sont dans CP-017.

États UI (TOUS REQUIS)

Remarque : Les états UI de la liste Employee sont dans CP-017.

Structure organisationnelle :

État Comportement Texte en Français
Loading Skeleton : contour de l'arbre
Empty Message + CTA "Votre structure organisationnelle est vide.\nCommencez par ajouter des départements." CTA : "Ajouter un département"
Error Toast rouge "Une erreur est survenue. Veuillez réessayer."
Offline (lecture) Arbre depuis IndexedDB, badge orange "Hors ligne — données locales"
Offline (écriture) Ajout d'unité mis en file d'attente pour sync "Unité enregistrée hors ligne ●"

Catalogue de postes :

État Comportement Texte en Français
Loading Skeleton : 5 éléments de poste
Empty Message "Aucun poste défini.\nLes postes seront créés automatiquement lorsque vous ajouterez des employés."
Error Toast rouge "Une erreur est survenue. Veuillez réessayer."
Offline Liste depuis IndexedDB "Hors ligne — données locales"

Comportement responsive

Remarque : Le comportement responsive de la liste Employee est dans CP-017.

Structure organisationnelle : - 360px : Liste indentée avec développer/réduire. Appui long → ajouter un enfant. Tap → bottom sheet avec les détails. - 1024px+ : Maître (40%, arbre avec filtre de recherche) + panneau de détail (60%, info de l'unité sélectionnée, édition en ligne, bouton "Ajouter un sous-département").

Interactions

Remarque : Les interactions de création d'employé (en ligne + hors ligne) sont dans CP-017 et CP-018.

Interactions arbre org (mobile) : - Tap sur nœud → basculer développer/réduire - Appui long sur parent → action "Ajouter sous [name]" - Tap sur nom du nœud → bottom sheet : nom, badge de type, statut, nombre d'employés - Dans le bottom sheet : modifier le nom, toggle de désactivation

Remarque : Les états des cartes employee (Actif/Pré-embauche/Suspendu/Résilié/Sync en attente) sont dans CP-017.


Critères d'acceptation

AC-036: Position escape-hatch creation
Given I am on the position catalog or employee form, position field
When I type "Technicien réseau" and no matching position exists
Then I see an option "Technicien réseau — Ajouter comme nouveau poste"
When I select it
Then the position is auto-created in the catalog (is_custom=true)
And I can select a category for the new position

AC-038: Org tree hierarchy rules
Given the root Entity "Acme SARL" exists
When I try to add a Site with parent = a Department
Then the validation rejects: Site parent must be the Entity

AC-042: Deactivated org unit blocks new assignments
Given department "Informatique" is deactivated
When I try to assign a new employee to "Informatique"
Then the validation rejects: cannot assign to inactive org unit
And existing employees in "Informatique" are NOT affected

AC-043: System categories cannot be deleted
Given the system category "Cadre" (is_system=true) exists
When I attempt to delete it
Then the operation is rejected
And I can only deactivate it

ACs déplacés : AC-034, AC-037, AC-040, AC-041 → CP-017. AC-035, AC-039, AC-044, AC-045 → CP-018.


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 d'employeur. Registre complet → module HR Core.
  • Limite CDD 2 ans (Art. 15.10) : Les contrats CDD dépassant 2 ans cumulés deviennent automatiquement CDI. La règle est stockée dans ContractType.max_duration_months. Alertes à 90/30/7 jours — application dans le module Employee Management, mais le modèle de données le prend en charge dès le départ.
  • Convention Collective Interprofessionnelle (CCI) : Définit 4 catégories d'employés (Cadre, AM, Employé, Ouvrier). Seedées en tant que catégories système. Affecte les taux de cotisation CNPS.
  • Décret 2024-901 : Les entreprises avec >10 travailleurs doivent élire des représentants du personnel (délégués du personnel). Le suivi du nombre d'employés permet cette vérification de seuil.
  • Les données employé sont des données personnelles (Loi 2013-450) : first_name, last_name, email, phone sont des données à caractère personnel (DCP). L'audit trail suit toutes les modifications. Les employés résiliés ne sont jamais supprimés définitivement (AUDCIF Art. 24).

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Entities, §UUID v7, §Optimistic locking, §Application Events, §Tree structures
  • 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
  • docs/standards/offline-sync-guidelines.md — §IndexedDB sync queue, §UUID v7 client generation, §Conflict detection, §"Numéro en attente" pattern

Exigences de test

Tests unitaires

  • OrgUnitService : validation de la hiérarchie (parent d'un Site = Entité, parent d'un Département = Entité/Site)
  • OrgUnitService : unicité du nom au sein du parent
  • OrgUnitService : la désactivation n'affecte pas les affectations existantes
  • PositionService : la création par mécanisme d'échappatoire définit is_custom=true
  • EmployeeCategoryService : les catégories système ne peuvent pas être supprimées

Tests d'intégration

  • GET /api/v1/org-units/tree — structure d'arbre correcte avec le nombre d'employés
  • POST /api/v1/org-units — validation de la hiérarchie
  • POST /api/v1/positions — création par mécanisme d'échappatoire
  • GET /api/v1/categories — données de seed présentes
  • GET /api/v1/contract-types — données de seed présentes
  • Isolation tenant : toutes les entités scopées au tenant

Ce que le QA validera

  • Arbre org : développer/réduire, ajouter un enfant, désactiver
  • Catalogue de postes : recherche, création par mécanisme d'échappatoire
  • Liste des catégories : les catégories système ne peuvent pas être supprimées
  • La liste des types de contrat s'affiche correctement
  • Toutes les pages s'affichent correctement à 360px, 768px, 1024px

Hors périmètre

  • CRUD Employee + Matricule — CP-017
  • Employee hors ligne + Hiérarchie Manager + Transition PRE_HIRE — CP-018
  • Dossier complet de l'employé (photo, contacts d'urgence, informations 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 pour l'employé — V2
  • Réorganisation de l'arbre org (glisser-déposer) — V2
  • Support multi-entités (plusieurs entités juridiques par tenant) — 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
  • [ ] Les 5 états UI implémentés (loading, empty, error, offline, 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)
  • [ ] Données de seed présentes (catégories CI + types de contrat)
  • [ ] 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