Conception complete (Phase 0) pour formation-hub Acadenice : - 19 docs Merise Agile + UML + GitOps + plans (tests/deploy/ops/api) cf docs/00-readme.md pour l'index complet - Stack Docker compose (Docmost + Baserow + Postgres + Redis + MinIO local FS) compose.yml + compose.staging.yml + compose.prod.yml - CI/CD GitHub Actions skeleton (ci, deploy-staging, deploy-prod) - Bridge service skeleton (Hono + TS + Biome + Vitest + zod + pino) - Templates GitHub : PR + 3 issue types + CODEOWNERS + dependabot.yml - Scripts ops : healthcheck, backup quotidien, smoke-test post-deploy - LICENSE AGPL-3.0 + SECURITY.md + CONTRIBUTING.md + CHANGELOG.md - Diagramme drawIO archi infra (XML importable dans diagrams.net) Decisions structurelles enregistrees : - Scope CFA + Agence avec entite PERSONNE pivot multi-roles (ADR-001) - Stack composite Docmost AGPL + Baserow MIT + bridge custom (ADR-001) - Path B : UX quasi-unified via Tiptap node-views custom (ADR-002) - Monorepo trunk-based development (ADR-003) - Postgres separe Docmost/Baserow (ADR-004) - Bridge stack Node 22 + Hono (ADR-005) - Repo neuf prefere a fork Docmost - Prod-like des le jour 1 (pas MVP)
359 lines
12 KiB
Markdown
359 lines
12 KiB
Markdown
# MLD — Modele Logique de Donnees
|
|
|
|
> Traduction du MCD en schema relationnel. Scope B (CFA + Agence + PERSONNE pivot).
|
|
> Implementation Baserow concrete : `15-baserow-mpd.md` (a venir).
|
|
|
|
## 1. Vue d'ensemble du schema relationnel
|
|
|
|
Decoupe en sous-vues pour eviter le spaghetti d'auto-layout : globale simplifiee + 3 zones + lien pedagogique.
|
|
|
|
### 1.1 Vue globale (PK/FK seuls)
|
|
|
|
```mermaid
|
|
erDiagram
|
|
personne ||--o{ attribution : "RESTRICT"
|
|
personne ||--o{ intervention : "RESTRICT"
|
|
formation ||--o{ bloc : "CASCADE"
|
|
bloc ||--o{ module : "CASCADE"
|
|
module ||--o{ attribution : "CASCADE"
|
|
client ||--o{ projet : "RESTRICT"
|
|
projet ||--o{ tache : "CASCADE"
|
|
tache ||--o{ intervention : "CASCADE"
|
|
projet }o--o| formation : "SET NULL"
|
|
```
|
|
|
|
### 1.2 Zone CFA — schema relationnel
|
|
|
|
```mermaid
|
|
erDiagram
|
|
formation ||--o{ bloc : "FK bloc_formation_id"
|
|
bloc ||--o{ module : "FK module_bloc_id"
|
|
module ||--o{ attribution : "FK attribution_module_id"
|
|
personne ||--o{ attribution : "FK attribution_personne_id"
|
|
|
|
formation {
|
|
INT formation_id PK
|
|
VARCHAR formation_nom UK
|
|
ENUM formation_filiere
|
|
DECIMAL heures_totales
|
|
ENUM statut
|
|
}
|
|
bloc {
|
|
INT bloc_id PK
|
|
INT bloc_formation_id FK
|
|
VARCHAR bloc_nom
|
|
DECIMAL heures_prevues
|
|
}
|
|
module {
|
|
INT module_id PK
|
|
INT module_bloc_id FK
|
|
VARCHAR module_nom
|
|
DECIMAL heures_prevues
|
|
ENUM statut
|
|
}
|
|
attribution {
|
|
INT attribution_id PK
|
|
INT attribution_module_id FK
|
|
INT attribution_personne_id FK
|
|
DECIMAL heures_attribuees
|
|
ENUM statut
|
|
}
|
|
personne {
|
|
INT personne_id PK
|
|
VARCHAR nom_prenom
|
|
}
|
|
```
|
|
|
|
### 1.3 Zone Agence — schema relationnel
|
|
|
|
```mermaid
|
|
erDiagram
|
|
client ||--o{ projet : "FK projet_client_id"
|
|
projet ||--o{ tache : "FK tache_projet_id"
|
|
tache ||--o{ intervention : "FK intervention_tache_id"
|
|
personne ||--o{ intervention : "FK intervention_personne_id"
|
|
|
|
client {
|
|
INT client_id PK
|
|
VARCHAR client_nom UK
|
|
ENUM statut
|
|
}
|
|
projet {
|
|
INT projet_id PK
|
|
INT projet_client_id FK
|
|
VARCHAR projet_nom
|
|
ENUM type
|
|
DECIMAL charge_heures
|
|
ENUM statut
|
|
}
|
|
tache {
|
|
INT tache_id PK
|
|
INT tache_projet_id FK
|
|
VARCHAR titre
|
|
DECIMAL charge_heures
|
|
ENUM statut
|
|
}
|
|
intervention {
|
|
INT intervention_id PK
|
|
INT intervention_tache_id FK
|
|
INT intervention_personne_id FK
|
|
DECIMAL heures
|
|
DATE intervention_date
|
|
}
|
|
personne {
|
|
INT personne_id PK
|
|
VARCHAR nom_prenom
|
|
}
|
|
```
|
|
|
|
### 1.4 Zone Personne pivot — table & FK sortantes
|
|
|
|
```mermaid
|
|
erDiagram
|
|
personne ||--o{ attribution : "FK personne_id"
|
|
personne ||--o{ intervention : "FK personne_id"
|
|
|
|
personne {
|
|
INT personne_id PK
|
|
VARCHAR personne_nom
|
|
VARCHAR personne_prenom
|
|
VARCHAR personne_email UK
|
|
DECIMAL capacite_annuelle
|
|
DECIMAL split_formation_pct
|
|
DECIMAL split_agence_pct
|
|
TEXT roles "multi-select"
|
|
ENUM statut
|
|
}
|
|
attribution {
|
|
INT attribution_id PK
|
|
INT module_id FK
|
|
INT personne_id FK
|
|
DECIMAL heures_attribuees
|
|
}
|
|
intervention {
|
|
INT intervention_id PK
|
|
INT tache_id FK
|
|
INT personne_id FK
|
|
DECIMAL heures
|
|
}
|
|
```
|
|
|
|
### 1.5 Lien pedagogique cross-zone
|
|
|
|
```mermaid
|
|
erDiagram
|
|
projet }o--o| formation : "FK projet_formation_id (SET NULL)"
|
|
|
|
projet {
|
|
INT projet_id PK
|
|
INT projet_formation_id FK "nullable"
|
|
}
|
|
formation {
|
|
INT formation_id PK
|
|
VARCHAR formation_nom
|
|
}
|
|
```
|
|
|
|
### Vue flowchart — navigation FK
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
P[personne]:::pivot
|
|
F[formation] --> B[bloc] --> M[module] --> A[attribution]
|
|
P --> A
|
|
C[client] --> Pr[projet] --> T[tache] --> I[intervention]
|
|
P --> I
|
|
Pr -.optionnel.-> F
|
|
|
|
classDef pivot fill:#FF825C,stroke:#333,color:#fff
|
|
classDef cfa fill:#FFB347,stroke:#333,color:#000
|
|
classDef agence fill:#5CB3FF,stroke:#333,color:#fff
|
|
class F,B,M,A cfa
|
|
class C,Pr,T,I agence
|
|
```
|
|
|
|
## 2. Regles de passage MCD → MLD (rappel)
|
|
|
|
1. Chaque entite → table.
|
|
2. Relation 1,N → la PK cote (1,1) devient FK cote (1,N).
|
|
3. Relation N,N porteuse → table associative avec PK composite (ou PK auto + UNIQUE composite).
|
|
4. Champs calcules → soit caches via triggers, soit recalcules par Baserow rollup.
|
|
|
|
## 3. Tables — definitions DDL
|
|
|
|
### Table `personne`
|
|
|
|
```
|
|
personne (
|
|
personne_id INT PK AUTO,
|
|
personne_nom VARCHAR(100) NOT NULL,
|
|
personne_prenom VARCHAR(100) NOT NULL,
|
|
personne_email VARCHAR(254) NOT NULL UNIQUE,
|
|
personne_telephone VARCHAR(20),
|
|
personne_capacite_annuelle DECIMAL(6,2) NOT NULL DEFAULT 0,
|
|
personne_split_formation_pct DECIMAL(4,1) NOT NULL DEFAULT 50.0,
|
|
personne_split_agence_pct DECIMAL(4,1) NOT NULL DEFAULT 50.0,
|
|
personne_roles VARCHAR(200) NOT NULL, -- csv ou table N:N selon Baserow
|
|
personne_statut ENUM NOT NULL DEFAULT 'actif'
|
|
)
|
|
CHECK (personne_split_formation_pct + personne_split_agence_pct = 100)
|
|
CHECK (personne_email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
|
|
INDEX idx_personne_statut ON personne(personne_statut)
|
|
```
|
|
|
|
### Tables CFA (formation, bloc, module, attribution)
|
|
|
|
```
|
|
formation (
|
|
formation_id INT PK AUTO,
|
|
formation_nom VARCHAR(200) NOT NULL UNIQUE,
|
|
formation_description TEXT,
|
|
formation_filiere ENUM('dev','graphisme','marketing','iot','cybersec'),
|
|
formation_heures_totales DECIMAL(6,2) NOT NULL DEFAULT 0,
|
|
formation_statut ENUM('draft','actif','termine','archive') NOT NULL DEFAULT 'draft',
|
|
formation_date_debut DATE,
|
|
formation_date_fin DATE,
|
|
formation_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
formation_updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
CHECK (formation_date_fin >= formation_date_debut OR formation_date_fin IS NULL)
|
|
|
|
bloc (
|
|
bloc_id INT PK AUTO,
|
|
bloc_formation_id INT NOT NULL FK → formation(formation_id) ON DELETE CASCADE,
|
|
bloc_nom VARCHAR(200) NOT NULL,
|
|
bloc_description TEXT,
|
|
bloc_heures_prevues DECIMAL(6,2) NOT NULL DEFAULT 0,
|
|
bloc_ordre INT NOT NULL DEFAULT 0,
|
|
UNIQUE (bloc_formation_id, bloc_nom)
|
|
)
|
|
INDEX idx_bloc_formation ON bloc(bloc_formation_id)
|
|
|
|
module (
|
|
module_id INT PK AUTO,
|
|
module_bloc_id INT NOT NULL FK → bloc(bloc_id) ON DELETE CASCADE,
|
|
module_nom VARCHAR(200) NOT NULL,
|
|
module_description TEXT,
|
|
module_heures_prevues DECIMAL(5,2) NOT NULL DEFAULT 0,
|
|
module_statut ENUM('a_attribuer','attribue','en_cours','realise','annule') NOT NULL DEFAULT 'a_attribuer'
|
|
)
|
|
INDEX idx_module_bloc ON module(module_bloc_id)
|
|
INDEX idx_module_statut ON module(module_statut)
|
|
|
|
attribution (
|
|
attribution_id INT PK AUTO,
|
|
attribution_module_id INT NOT NULL FK → module(module_id) ON DELETE CASCADE,
|
|
attribution_personne_id INT NOT NULL FK → personne(personne_id) ON DELETE RESTRICT,
|
|
attribution_heures_attribuees DECIMAL(5,2) NOT NULL,
|
|
attribution_heures_realisees DECIMAL(5,2) NOT NULL DEFAULT 0,
|
|
attribution_date_debut DATE,
|
|
attribution_date_fin DATE,
|
|
attribution_statut ENUM('planifie','en_cours','realise','annule') NOT NULL DEFAULT 'planifie'
|
|
)
|
|
CHECK (attribution_heures_attribuees > 0)
|
|
CHECK (attribution_heures_realisees >= 0)
|
|
INDEX idx_attribution_module ON attribution(attribution_module_id)
|
|
INDEX idx_attribution_personne ON attribution(attribution_personne_id)
|
|
INDEX idx_attribution_statut ON attribution(attribution_statut)
|
|
UNIQUE (attribution_module_id, attribution_personne_id, attribution_date_debut)
|
|
WHERE attribution_statut != 'annule' -- index partiel
|
|
```
|
|
|
|
### Tables Agence (client, projet, tache, intervention)
|
|
|
|
```
|
|
client (
|
|
client_id INT PK AUTO,
|
|
client_nom VARCHAR(200) NOT NULL UNIQUE,
|
|
client_contact_principal VARCHAR(200),
|
|
client_contact_email VARCHAR(254),
|
|
client_contact_telephone VARCHAR(20),
|
|
client_secteur VARCHAR(100),
|
|
client_notes TEXT,
|
|
client_statut ENUM('prospect','actif','inactif','archive') NOT NULL DEFAULT 'prospect',
|
|
client_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
|
|
projet (
|
|
projet_id INT PK AUTO,
|
|
projet_client_id INT NOT NULL FK → client(client_id) ON DELETE RESTRICT,
|
|
projet_nom VARCHAR(200) NOT NULL,
|
|
projet_description TEXT,
|
|
projet_type ENUM('site_web','app_mobile','api','infra','audit','support','autre'),
|
|
projet_charge_heures DECIMAL(7,2) NOT NULL DEFAULT 0,
|
|
projet_date_debut DATE,
|
|
projet_date_fin_prevue DATE,
|
|
projet_date_livraison DATE,
|
|
projet_statut ENUM('devis','en_cours','livre','cloture','abandonne') NOT NULL DEFAULT 'devis',
|
|
projet_formation_id INT FK → formation(formation_id) ON DELETE SET NULL,
|
|
projet_url VARCHAR(500),
|
|
projet_repository VARCHAR(500),
|
|
UNIQUE (projet_client_id, projet_nom)
|
|
)
|
|
INDEX idx_projet_client ON projet(projet_client_id)
|
|
INDEX idx_projet_statut ON projet(projet_statut)
|
|
INDEX idx_projet_formation ON projet(projet_formation_id)
|
|
|
|
tache (
|
|
tache_id INT PK AUTO,
|
|
tache_projet_id INT NOT NULL FK → projet(projet_id) ON DELETE CASCADE,
|
|
tache_titre VARCHAR(200) NOT NULL,
|
|
tache_description TEXT,
|
|
tache_charge_heures DECIMAL(5,2) NOT NULL DEFAULT 0,
|
|
tache_priorite ENUM('faible','normale','haute','critique'),
|
|
tache_statut ENUM('todo','in_progress','review','done','abandoned') NOT NULL DEFAULT 'todo',
|
|
tache_date_debut DATE,
|
|
tache_date_fin_prevue DATE
|
|
)
|
|
INDEX idx_tache_projet ON tache(tache_projet_id)
|
|
INDEX idx_tache_statut ON tache(tache_statut)
|
|
|
|
intervention (
|
|
intervention_id INT PK AUTO,
|
|
intervention_tache_id INT NOT NULL FK → tache(tache_id) ON DELETE CASCADE,
|
|
intervention_personne_id INT NOT NULL FK → personne(personne_id) ON DELETE RESTRICT,
|
|
intervention_heures DECIMAL(5,2) NOT NULL,
|
|
intervention_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
intervention_notes TEXT,
|
|
intervention_statut ENUM('planifie','realise','annule') NOT NULL DEFAULT 'realise'
|
|
)
|
|
CHECK (intervention_heures > 0)
|
|
INDEX idx_intervention_tache ON intervention(intervention_tache_id)
|
|
INDEX idx_intervention_personne ON intervention(intervention_personne_id)
|
|
INDEX idx_intervention_date ON intervention(intervention_date)
|
|
```
|
|
|
|
## 4. Comportement ON DELETE
|
|
|
|
| Relation | ON DELETE | Justification |
|
|
|----------|-----------|---------------|
|
|
| formation → bloc | CASCADE | Cycle de vie partage |
|
|
| bloc → module | CASCADE | Cycle de vie partage |
|
|
| module → attribution | CASCADE | Si module supprime, attributions deviennent orphelines |
|
|
| client → projet | RESTRICT | Empeche suppression client ayant projets |
|
|
| projet → tache | CASCADE | Cycle de vie partage |
|
|
| tache → intervention | CASCADE | Idem |
|
|
| personne → attribution / intervention | RESTRICT | Force a archiver `personne_statut = inactif` plutot que supprimer |
|
|
| projet → formation (lien pedagogique) | SET NULL | Suppression formation laisse le projet exister |
|
|
|
|
## 5. Mapping vers Baserow
|
|
|
|
| Concept SQL | Baserow equivalent |
|
|
|-------------|--------------------|
|
|
| Table | Database → Table |
|
|
| FK | `Link to table` (gere bidirec auto) |
|
|
| ENUM | `Single select` |
|
|
| MULTI_ENUM (personne_roles) | `Multiple select` |
|
|
| FK ON DELETE CASCADE | UI Baserow ou webhook → bridge |
|
|
| INDEX | Implicite sur `Link to table` |
|
|
| CHECK CONSTRAINT | Validation cote bridge ou formules de validation |
|
|
| Calculated rollup/formula | `Lookup` + `Formula` + `Count` |
|
|
|
|
Voir `15-baserow-mpd.md` (a venir) pour la traduction concrete table-par-table.
|
|
|
|
## 6. Questions ouvertes
|
|
|
|
- [ ] Materialiser les calculs ou recalculer a la lecture ? Baserow rollups = cache materialise auto. OK pour notre volumetrie.
|
|
- [ ] Soft-delete ou hard-delete ? Si Qualiopi exige tracabilite, soft-delete obligatoire (ajouter `*_deleted_at TIMESTAMPTZ NULLABLE` partout).
|
|
- [ ] Audit log par row ? Baserow a `Last modified by` + `Last modified time` natifs. Suffisant ou il faut un journal d'evenements separe ?
|
|
- [ ] Multi-tenant futur ? Pour l'instant Acadenice mono-instance. Si rachat / scaling, ajouter `tenant_id` a toutes les tables.
|