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)
12 KiB
12 KiB
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)
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
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
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
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
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
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)
- Chaque entite → table.
- Relation 1,N → la PK cote (1,1) devient FK cote (1,N).
- Relation N,N porteuse → table associative avec PK composite (ou PK auto + UNIQUE composite).
- 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 NULLABLEpartout). - Audit log par row ? Baserow a
Last modified by+Last modified timenatifs. Suffisant ou il faut un journal d'evenements separe ? - Multi-tenant futur ? Pour l'instant Acadenice mono-instance. Si rachat / scaling, ajouter
tenant_ida toutes les tables.