Wiki/docs/07-merise-mld.md
Corentin JOGUET 668576cdc4 chore: initial commit — formation-hub conception phase
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)
2026-05-07 12:16:19 +02:00

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)

  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.