# MPD — Modele Physique de Donnees (Baserow) > Implementation concrete dans Baserow : 9 tables avec types exacts, formules, vues, permissions. > Ce doc est **actionnable** : Corentin l'ouvre cote a cote avec Baserow et cree les tables une par une. > Source : `07-merise-mld.md` (MLD relationnel) + `05-data-dictionary.md`. ## 1. Setup initial ### 1.1 Hierarchie Baserow ``` Workspace : Acadenice formation-hub └── Database : formation-hub ├── Tables CFA : formation, bloc, module, attribution ├── Tables Agence : client, projet, tache, intervention └── Table pivot : personne ``` ### 1.2 Order de creation (dependances FK) Creer les tables dans cet ordre — chaque table dependant des precedentes via FK : 1. **personne** (aucune dep) 2. **client** (aucune dep) 3. **formation** (aucune dep) 4. **bloc** (FK → formation) 5. **projet** (FK → client, optionnel FK → formation) 6. **module** (FK → bloc) 7. **tache** (FK → projet) 8. **attribution** (FK → module + personne) 9. **intervention** (FK → tache + personne) ### 1.3 Conventions Baserow | Concept | Implementation Baserow | |---------|------------------------| | ID auto-increment (PK) | Baserow `id` natif (genere auto) | | Nom du field | `snake_case`, prefixes par mnemonique d'entite (ex `formation_nom`, `personne_email`) | | Field "Primary" | Baserow oblige a avoir un Primary field — choisir le nom le plus explicite (ex `formation_nom`) | | Foreign Key | Field type `Link to table` | | ENUM | Field type `Single select` avec options exactes | | MULTI ENUM | Field type `Multiple select` | | Champ calcule (formula) | Field type `Formula` | | Champ derive d'une relation | Field type `Lookup` ou `Count` | | Audit timestamps | Fields `Created on` + `Last modified time` natifs | | Audit acteur | Fields `Created by` + `Last modified by` natifs | ### 1.4 API token Apres creation des tables : - Settings → API tokens → Create token - Permissions : `read`/`create`/`update`/`delete` sur la database `formation-hub` - Stocker dans `.env` cote bridge : `BASEROW_API_TOKEN=...` --- ## 2. Table `personne` **Primary field** : `personne_nom` (texte affiche par defaut dans les links) | # | Field | Type Baserow | Parametres | Description | |---|-------|-------------|------------|-------------| | 1 | `personne_nom` | Text | — | Nom de famille (Primary) | | 2 | `personne_prenom` | Text | — | Prenom | | 3 | `personne_email` | Email | — | Email pro (unique recommande, validation cote bridge) | | 4 | `personne_telephone` | Phone number | — | Telephone (optionnel) | | 5 | `personne_capacite_annuelle` | Number | Decimal places: 2 | Heures totales/an | | 6 | `personne_split_formation_pct` | Number | Decimal places: 1, default 50 | % capacite alloue formation | | 7 | `personne_split_agence_pct` | Number | Decimal places: 1, default 50 | % capacite alloue agence | | 8 | `personne_roles` | Multiple select | Options: `formateur`, `developpeur`, `admin`, `direction`, `support` | Roles cumules | | 9 | `personne_statut` | Single select | Options: `actif` (default), `inactif` | Statut | | 10 | `personne_attributions` | Link to table | Lien vers `attribution` (champ inverse auto-cree apres creation de attribution) | Toutes les attributions de cette personne | | 11 | `personne_interventions` | Link to table | Lien vers `intervention` (apres creation) | Toutes les interventions | | 12 | `personne_heures_attribuees_formation` | Formula | `sum(lookup('personne_attributions', 'attribution_heures_attribuees_active'))` | Rollup des attributions actives | | 13 | `personne_heures_attribuees_agence` | Formula | `sum(lookup('personne_interventions', 'intervention_heures_active'))` | Rollup des interventions actives | | 14 | `personne_heures_restantes_formation` | Formula | `(field('personne_capacite_annuelle') * field('personne_split_formation_pct') / 100) - field('personne_heures_attribuees_formation')` | Capacite formation restante | | 15 | `personne_heures_restantes_agence` | Formula | `(field('personne_capacite_annuelle') * field('personne_split_agence_pct') / 100) - field('personne_heures_attribuees_agence')` | Capacite agence restante | | 16 | `personne_heures_restantes_total` | Formula | `field('personne_capacite_annuelle') - field('personne_heures_attribuees_formation') - field('personne_heures_attribuees_agence')` | Capacite totale restante | **Vues recommandees** : - `Tous` (grid, default) — tableau complet - `Actifs` (grid, filtre `personne_statut = actif`) - `Formateurs` (grid, filtre `personne_roles contient formateur`) - `Developpeurs` (grid, filtre `personne_roles contient developpeur`) - `Capacite restante` (grid, sort `personne_heures_restantes_total ascending`) --- ## 3. Table `formation` **Primary field** : `formation_nom` | # | Field | Type Baserow | Parametres | Description | |---|-------|-------------|------------|-------------| | 1 | `formation_nom` | Text | — | Nom (Primary, unique conseille) | | 2 | `formation_description` | Long text | rich text autorise | Description longue | | 3 | `formation_filiere` | Single select | `dev`, `graphisme`, `marketing`, `iot`, `cybersec` | Filiere | | 4 | `formation_heures_totales` | Number | Decimal places: 2 | Heures totales prevues | | 5 | `formation_statut` | Single select | `draft` (default), `actif`, `termine`, `archive` | Cycle de vie | | 6 | `formation_date_debut` | Date | format `YYYY-MM-DD` | Date debut | | 7 | `formation_date_fin` | Date | — | Date fin | | 8 | `formation_blocs` | Link to table | Lien vers `bloc` (apres creation bloc) | Blocs de la formation | | 9 | `formation_projets_pedagogiques` | Link to table | Lien vers `projet` (optionnel) | Projets agence lies en pedagogique | | 10 | `formation_heures_attribuees` | Formula | `sum(lookup('formation_blocs', 'bloc_heures_prevues'))` | Rollup heures des blocs | | 11 | `formation_heures_restantes` | Formula | `field('formation_heures_totales') - field('formation_heures_attribuees')` | Reste a attribuer | | 12 | `formation_created_at` | Created on | — | Audit | | 13 | `formation_updated_at` | Last modified time | — | Audit | **Vues** : - `Tous` (grid) - `Actives` (grid, filtre `formation_statut = actif`) - `Par filiere` (grid, group by `formation_filiere`) - `Calendrier` (calendar view, sur `formation_date_debut`) - `Capacite restante` (grid, sort `formation_heures_restantes ascending`) --- ## 4. Table `bloc` **Primary field** : `bloc_nom` | # | Field | Type Baserow | Parametres | Description | |---|-------|-------------|------------|-------------| | 1 | `bloc_nom` | Text | — | Nom du bloc (Primary) | | 2 | `bloc_description` | Long text | — | Description | | 3 | `bloc_formation` | Link to table | Lien vers `formation` (single) | Formation parente | | 4 | `bloc_heures_prevues` | Number | Decimal places: 2 | Heures du bloc | | 5 | `bloc_ordre` | Number | Decimal places: 0 | Ordre dans la formation | | 6 | `bloc_modules` | Link to table | Lien vers `module` (apres creation) | Modules du bloc | | 7 | `bloc_heures_attribuees` | Formula | `sum(lookup('bloc_modules', 'module_heures_prevues_active'))` | Rollup heures modules actifs | | 8 | `bloc_heures_restantes` | Formula | `field('bloc_heures_prevues') - field('bloc_heures_attribuees')` | Reste a decomposer | **Note** : la regle metier "un bloc a un nom unique par formation" se valide **cote bridge** ou via une vue filtree de duplication. **Vues** : - `Tous` (grid) - `Par formation` (grid, group by `bloc_formation`) --- ## 5. Table `module` **Primary field** : `module_nom` | # | Field | Type Baserow | Parametres | Description | |---|-------|-------------|------------|-------------| | 1 | `module_nom` | Text | — | Nom du module (Primary) | | 2 | `module_description` | Long text | — | Description | | 3 | `module_bloc` | Link to table | Lien vers `bloc` (single) | Bloc parent | | 4 | `module_heures_prevues` | Number | Decimal places: 2 | Heures du module | | 5 | `module_statut` | Single select | `a_attribuer` (default), `attribue`, `en_cours`, `realise`, `annule` | Cycle de vie | | 6 | `module_attributions` | Link to table | Lien vers `attribution` (apres creation) | Attributions du module | | 7 | `module_heures_prevues_active` | Formula | `if(field('module_statut') = 'annule', 0, field('module_heures_prevues'))` | Pour rollup bloc (exclut annule) | | 8 | `module_heures_attribuees` | Formula | `sum(lookup('module_attributions', 'attribution_heures_attribuees_active'))` | Rollup | | 9 | `module_heures_realisees` | Formula | `sum(lookup('module_attributions', 'attribution_heures_realisees'))` | Rollup | **Vues** : - `Tous` (grid) - **`A attribuer`** (kanban, group by `module_statut`) — vue principale pour l'admin - `Par bloc` (grid, group by `module_bloc`) - `Realises` (grid, filtre `module_statut = realise`) --- ## 6. Table `attribution` **Primary field** : `attribution_titre` (formula : nom_module + " → " + nom_personne) | # | Field | Type Baserow | Parametres | Description | |---|-------|-------------|------------|-------------| | 1 | `attribution_titre` | Formula | `concat(lookup('attribution_module', 'module_nom'), ' → ', lookup('attribution_personne', 'personne_prenom'), ' ', lookup('attribution_personne', 'personne_nom'))` | Titre auto (Primary) | | 2 | `attribution_module` | Link to table | Lien vers `module` (single) | Module attribue | | 3 | `attribution_personne` | Link to table | Lien vers `personne` (single) | Formateur (role formateur requis) | | 4 | `attribution_heures_attribuees` | Number | Decimal places: 2 | Heures planifiees | | 5 | `attribution_heures_realisees` | Number | Decimal places: 2, default 0 | Heures effectuees | | 6 | `attribution_date_debut` | Date | — | Debut periode | | 7 | `attribution_date_fin` | Date | — | Fin periode | | 8 | `attribution_statut` | Single select | `planifie` (default), `en_cours`, `realise`, `annule` | Statut | | 9 | `attribution_heures_attribuees_active` | Formula | `if(field('attribution_statut') = 'annule', 0, field('attribution_heures_attribuees'))` | Pour rollup module/personne | **Validation cote bridge** : - `attribution_personne.personne_roles` doit contenir `formateur` - `sum(attribution_heures_attribuees) for module <= module_heures_prevues` (RG-01) **Vues** : - `Tous` (grid) - **`Mes attributions`** (grid, filtre `attribution_personne = current user`) — vue formateur - `En cours` (grid, filtre `attribution_statut = en_cours`) - `Calendrier` (calendar view sur `attribution_date_debut` ou `attribution_date_fin`) - `Form public` (form view) — formateur saisit ses heures realisees rapide --- ## 7. Table `client` **Primary field** : `client_nom` | # | Field | Type Baserow | Parametres | Description | |---|-------|-------------|------------|-------------| | 1 | `client_nom` | Text | — | Nom (Primary) | | 2 | `client_contact_principal` | Text | — | Nom + role du contact | | 3 | `client_contact_email` | Email | — | Email contact | | 4 | `client_contact_telephone` | Phone number | — | Telephone | | 5 | `client_secteur` | Text | — | Secteur d'activite | | 6 | `client_notes` | Long text | — | Notes libres | | 7 | `client_statut` | Single select | `prospect` (default), `actif`, `inactif`, `archive` | Statut | | 8 | `client_projets` | Link to table | Lien vers `projet` | Projets du client | | 9 | `client_created_at` | Created on | — | Audit | **Vues** : - `Tous` (grid) - `Actifs` (grid, filtre `client_statut = actif`) - **`Pipeline`** (kanban, group by `client_statut`) — vue commerciale --- ## 8. Table `projet` **Primary field** : `projet_nom` | # | Field | Type Baserow | Parametres | Description | |---|-------|-------------|------------|-------------| | 1 | `projet_nom` | Text | — | Nom (Primary) | | 2 | `projet_description` | Long text | — | Description | | 3 | `projet_client` | Link to table | Lien vers `client` (single) | Client | | 4 | `projet_type` | Single select | `site_web`, `app_mobile`, `api`, `infra`, `audit`, `support`, `autre` | Type | | 5 | `projet_charge_heures` | Number | Decimal places: 2 | Charge estimee | | 6 | `projet_date_debut` | Date | — | Date debut | | 7 | `projet_date_fin_prevue` | Date | — | Date fin prevue | | 8 | `projet_date_livraison` | Date | — | Date livraison effective | | 9 | `projet_statut` | Single select | `devis` (default), `en_cours`, `livre`, `cloture`, `abandonne` | Statut | | 10 | `projet_formation_pedagogique` | Link to table | Lien vers `formation` (single, optionnel) | Lien pedagogique | | 11 | `projet_url` | URL | — | Site livraison | | 12 | `projet_repository` | URL | — | Repo Git | | 13 | `projet_taches` | Link to table | Lien vers `tache` | Taches du projet | | 14 | `projet_heures_attribuees` | Formula | `sum(lookup('projet_taches', 'tache_charge_heures'))` | Rollup taches | | 15 | `projet_heures_realisees` | Formula | `sum(lookup('projet_taches', 'tache_heures_realisees'))` | Rollup | | 16 | `projet_heures_restantes` | Formula | `field('projet_charge_heures') - field('projet_heures_realisees')` | Reste a faire | **Vues** : - `Tous` (grid) - **`Pipeline`** (kanban, group by `projet_statut`) — vue principale - `En cours` (grid, filtre `projet_statut = en_cours`) - `Timeline` (timeline view sur date_debut → date_fin_prevue) - `Par client` (grid, group by `projet_client`) --- ## 9. Table `tache` **Primary field** : `tache_titre` | # | Field | Type Baserow | Parametres | Description | |---|-------|-------------|------------|-------------| | 1 | `tache_titre` | Text | — | Titre (Primary) | | 2 | `tache_description` | Long text | — | Description | | 3 | `tache_projet` | Link to table | Lien vers `projet` (single) | Projet parent | | 4 | `tache_charge_heures` | Number | Decimal places: 2 | Charge estimee | | 5 | `tache_priorite` | Single select | `faible`, `normale`, `haute`, `critique` | Priorite | | 6 | `tache_statut` | Single select | `todo` (default), `in_progress`, `review`, `done`, `abandoned` | Statut | | 7 | `tache_date_debut` | Date | — | Debut prevu | | 8 | `tache_date_fin_prevue` | Date | — | Fin prevue | | 9 | `tache_assignee` | Link to table | Lien vers `personne` (single, optionnel) | Dev assignee informellement | | 10 | `tache_interventions` | Link to table | Lien vers `intervention` | Interventions sur cette tache | | 11 | `tache_heures_realisees` | Formula | `sum(lookup('tache_interventions', 'intervention_heures_active'))` | Rollup | **Vues** : - `Tous` (grid) - **`Kanban`** (kanban, group by `tache_statut`) — vue principale - `Par priorite` (grid, group by `tache_priorite`, sort par priorite) - `Mes taches` (grid, filtre `tache_assignee = current user`) - `Done recentes` (grid, filtre `tache_statut = done`, sort par date desc) --- ## 10. Table `intervention` **Primary field** : `intervention_titre` (formula auto) | # | Field | Type Baserow | Parametres | Description | |---|-------|-------------|------------|-------------| | 1 | `intervention_titre` | Formula | `concat(lookup('intervention_tache', 'tache_titre'), ' - ', lookup('intervention_personne', 'personne_prenom'), ' (', totext(field('intervention_date')), ')')` | Titre auto (Primary) | | 2 | `intervention_tache` | Link to table | Lien vers `tache` (single) | Tache concernee | | 3 | `intervention_personne` | Link to table | Lien vers `personne` (single) | Developpeur (role developpeur requis) | | 4 | `intervention_heures` | Number | Decimal places: 2 | Heures effectuees | | 5 | `intervention_date` | Date | default today | Date intervention | | 6 | `intervention_notes` | Long text | — | Notes / commit ref / lien PR | | 7 | `intervention_statut` | Single select | `planifie`, `realise` (default), `annule` | Statut | | 8 | `intervention_heures_active` | Formula | `if(field('intervention_statut') = 'annule', 0, field('intervention_heures'))` | Pour rollup tache/personne | **Validation cote bridge** : - `intervention_personne.personne_roles` doit contenir `developpeur` - `intervention_heures > 0` **Vues** : - `Tous` (grid, sort `intervention_date desc`) - **`Mes interventions`** (grid, filtre `intervention_personne = current user`) — vue dev - **`Form rapide`** (form public) — saisie heures rapide mobile - `Par projet` (grid, group by `intervention_tache.tache_projet`) - `Cette semaine` (grid, filtre `intervention_date >= start_of_week`) --- ## 11. Permissions et sharing ### 11.1 Roles Baserow | Role | Membres | Capacites | |------|---------|-----------| | Admin workspace | Corentin, Yan, Ludo | Plein controle | | Editor | Sophie + autres admins | Read/write toutes tables | | Builder | Formateurs / Devs | Read/write **leur ligne** via vues filtrees + form rapide | | Viewer | Stakeholders ponctuels | Read seul | **Limitation** : Baserow native permissions sont au niveau database/table, pas row-level. Pour limiter formateur/dev a leurs propres rows : - Soit **vues filtrees partagees publiquement** (form pour saisie + grid filtree pour lecture) - Soit **bridge service** qui filtre cote API selon `current_user_id` ### 11.2 Forms publics pour saisie rapide Plus simple que de gerer les permissions row-level : - Form public sur `attribution` — formateur saisit ses heures via lien sans compte Baserow - Form public sur `intervention` — dev saisit son intervention idem Le user qui saisit n'a pas besoin de voir le reste des donnees, juste son formulaire. --- ## 12. Webhooks (Phase 2 — bridge integration) Configurer dans Baserow → Database Settings → Webhooks : | Evenement | URL cible | Usage bridge | |-----------|-----------|--------------| | `row.created` sur `attribution` | `https://bridge.acadenice.fr/webhooks/attribution-created` | Notif formateur, recalcul cache mention | | `row.updated` sur `attribution` | `https://bridge.acadenice.fr/webhooks/attribution-updated` | Recalcul cache, notif si statut change | | `row.created` sur `intervention` | `https://bridge.acadenice.fr/webhooks/intervention-created` | Notif admin si depassement capacite | | `row.updated` sur `module` (si `module_statut` change) | `https://bridge.acadenice.fr/webhooks/module-status-changed` | Trigger cloturer formation auto | Authentification webhook : header `X-Bridge-Token` avec un secret partage (`.env`). --- ## 13. Seed data initial Apres creation des 9 tables, seed avec : - **personne** : equipe Acadenice (Yan, Corentin, Ludo, Sophie + formateurs intervenants) - **client** : 1-2 clients existants (Centralis Europe + autre) - **formation** : les 5 filieres en cours pour 2026-2027 - **bloc** : decoupage RNCP par filiere - **module** : programme detaille - **projet** : projets clients en cours - Pas d'attribution / intervention seed — saisies par l'usage Script de seed : `baserow/seed/seed.py` (a coder Phase 1). --- ## 14. Validation post-creation Checklist apres creation des 9 tables : - [ ] Les 9 tables existent dans la database `formation-hub` - [ ] Toutes les FK sont liees correctement (verifier en cliquant sur un lien dans une row) - [ ] Les rollups fonctionnent (creer une row test, verifier le calcul de `formation_heures_attribuees`) - [ ] Les formulas s'evaluent sans erreur (regarder les rows test) - [ ] Les Single Select / Multiple Select ont les bonnes options - [ ] Au moins une vue par table est creee (grid `Tous` minimum) - [ ] Les vues kanban (module, projet, tache, client) sont fonctionnelles - [ ] Le form public pour saisie heures (attribution, intervention) marche - [ ] L'API token est genere et fonctionne (test `curl`) ```bash # Test API token curl -H "Authorization: Token $BASEROW_API_TOKEN" \ "$BASEROW_URL/api/database/rows/table//?user_field_names=true" ``` --- ## 15. Notes d'implementation ### 15.1 Limitations Baserow connues - **Pas de FK `ON DELETE` configurable** : c'est `SET NULL` par defaut quand le lien est rompu. Pour forcer un comportement CASCADE/RESTRICT, le bridge service doit l'implementer (ou un workflow Baserow). - **Pas de CHECK constraint** : validation cote bridge ou cote UI. - **Pas d'index custom** : Baserow indexe automatiquement les Link to table et les Primary fields. - **Formules limitees** : pas de boucles ni de subqueries complexes. Pour calculs lourds, calcul cote bridge + ecriture en batch. ### 15.2 Alternatives si Baserow limite Si une formule devient trop complexe ou si on a besoin de validation forte : - Option A : **Bridge fait le calcul** et ecrit en Baserow via API - Option B : **Vue filtree dediee** + formula simple - Option C : **Migration vers Postgres direct** (futur — si on perd Baserow) ### 15.3 Migration data initiale Si donnees existent dans Excel/Trello/autre : 1. Exporter en CSV 2. Mapper les colonnes vers les fields Baserow 3. Importer via Baserow UI (`Import data`) 4. Verifier les liens FK manuellement (Baserow ne mappe pas auto les liens via CSV) --- ## 16. Resume — checklist d'implementation Phase 1 ``` [ ] 1. Setup workspace + database [ ] 2. Creer table 'personne' (sans liens encore) [ ] 3. Creer table 'client' (sans liens encore) [ ] 4. Creer table 'formation' (sans liens encore) [ ] 5. Creer table 'bloc' + lien vers formation [ ] 6. Creer table 'projet' + lien vers client (+ optionnel formation) [ ] 7. Creer table 'module' + lien vers bloc [ ] 8. Creer table 'tache' + lien vers projet (+ optionnel personne assignee) [ ] 9. Creer table 'attribution' + liens vers module + personne [ ] 10. Creer table 'intervention' + liens vers tache + personne [ ] 11. Ajouter formulas et lookups (apres tous les liens crees) [ ] 12. Creer vues recommandees par table [ ] 13. Configurer permissions roles + sharing [ ] 14. Seed data initial [ ] 15. Generer API token + verifier [ ] 16. Documenter exports JSON dans `baserow/schemas/` ``` Apres ca : la base structurelle est en place. La saisie metier peut commencer **immediat** — sans attendre Phase 2 / bridge.