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, savePositionRepository.java— Tenant-aware : findAll, findByTitle(search), saveEmployeeCategoryRepository.java— Tenant-aware : findAll, findByCodeContractTypeRepository.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— tableorg_unitsV004__create_position_category.sql— tablespositions,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.sqlest 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.tsxPositionCatalogPage.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 structuresdocs/standards/database-guidelines.md— §Self-referencing FKs, §Counter tables (FOR UPDATE), §Tenant-scoped unique constraintsdocs/standards/api-guidelines.md— §CRUD patterns, §Pagination, §Filteringdocs/standards/react-typescript-guidelines.md— §List/card/table components, §Form components, §Combobox pattern, §FABdocs/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
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