Checkpoint: Implémentation complète de la fonctionnalité "Gestion des plans de formations" :

- Schéma DB : 3 nouvelles tables (catalogueFormation, planFormation, planFormationItem)
- Backend tRPC : routers catalogue et planFormation (CRUD, import bulk CSV, suggestions, toggle validée Itinova)
- Frontend : page Plans de formation (liste par établissement/année, CRUD, soumission/validation)
- Frontend : page Détail plan (items, liaison catalogue, suggestions automatiques)
- Frontend : page Catalogue de formation (liste, filtres thème/Itinova/OPCO, import CSV, badges)
- Menu sidebar : nouvelle section "Plans de formations" avec sous-menus
- Tests unitaires : 13 tests (scoring suggestions, validation données, parsing CSV)
This commit is contained in:
Manus
2026-05-27 16:39:18 +00:00
parent 54159fd872
commit 6952ae6907
14 changed files with 5295 additions and 2 deletions

View File

@@ -593,3 +593,110 @@ export const historiqueAttestations = mysqlTable("historiqueAttestations", {
export type HistoriqueAttestation = typeof historiqueAttestations.$inferSelect;
export type InsertHistoriqueAttestation = typeof historiqueAttestations.$inferInsert;
/**
* Table du catalogue de formations partagé
* Géré par l'équipe Itinova, accessible en lecture à tous les établissements
*/
export const catalogueFormation = mysqlTable("catalogueFormation", {
id: int("id").autoincrement().primaryKey(),
/** Intitulé de la formation */
intitule: varchar("intitule", { length: 255 }).notNull(),
/** Thème principal de la formation */
theme: varchar("theme", { length: 255 }),
/** Mots-clés pour la recherche et la suggestion automatique (séparés par des virgules) */
motsCles: text("motsCles"),
/** Durée indicative (ex: "2 jours", "14h") */
duree: varchar("duree", { length: 100 }),
/** Organisme ou prestataire de formation */
prestataire: varchar("prestataire", { length: 255 }),
/** Description détaillée de la formation */
description: text("description"),
/** Objectifs pédagogiques */
objectifs: text("objectifs"),
/** Public concerné */
publicConcerne: varchar("publicConcerne", { length: 255 }),
/** Formation qualifiée "Validée Itinova" par l'équipe Itinova */
validateeItinova: boolean("validateeItinova").default(false).notNull(),
/** Formation éligible au financement OPCO */
opcoEligible: boolean("opcoEligible").default(false).notNull(),
/** Budget OPCO estimé (en euros) */
budgetOpco: decimal("budgetOpco", { precision: 10, scale: 2 }),
/** Référence du dossier OPCO */
referenceOpco: varchar("referenceOpco", { length: 255 }),
/** Source de la formation (import CSV, saisie manuelle, organisme externe) */
source: varchar("source", { length: 100 }).default("manuel").notNull(),
actif: boolean("actif").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type CatalogueFormation = typeof catalogueFormation.$inferSelect;
export type InsertCatalogueFormation = typeof catalogueFormation.$inferInsert;
/**
* Table des plans de formation par établissement et par année
*/
export const planFormation = mysqlTable("planFormation", {
id: int("id").autoincrement().primaryKey(),
/** Code de l'établissement concerné */
codeEtablissement: varchar("codeEtablissement", { length: 50 }).notNull(),
/** Nom de l'établissement (dénormalisé pour affichage) */
nomEtablissement: varchar("nomEtablissement", { length: 255 }),
/** Année du plan de formation */
annee: int("annee").notNull(),
/** Statut du plan (brouillon, soumis, valide, rejete) */
statut: mysqlEnum("statut", ["brouillon", "soumis", "valide", "rejete"]).default("brouillon").notNull(),
/** Notes générales sur le plan */
notes: text("notes"),
/** Notes de validation par l'équipe Itinova */
notesValidation: text("notesValidation"),
/** ID de l'utilisateur qui a créé le plan */
creePar: int("creePar"),
/** ID de l'utilisateur qui a validé le plan */
validePar: int("validePar"),
/** Date de soumission */
dateSoumission: timestamp("dateSoumission"),
/** Date de validation */
dateValidation: timestamp("dateValidation"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type PlanFormation = typeof planFormation.$inferSelect;
export type InsertPlanFormation = typeof planFormation.$inferInsert;
/**
* Table des items (lignes) d'un plan de formation
* Chaque item représente une formation souhaitée dans le plan
*/
export const planFormationItem = mysqlTable("planFormationItem", {
id: int("id").autoincrement().primaryKey(),
/** ID du plan de formation parent */
planId: int("planId").notNull(),
/** Intitulé de la formation souhaitée */
intitule: varchar("intitule", { length: 255 }).notNull(),
/** Durée souhaitée */
duree: varchar("duree", { length: 100 }),
/** Prestataire souhaité */
prestataire: varchar("prestataire", { length: 255 }),
/** Public concerné par cette formation */
publicConcerne: varchar("publicConcerne", { length: 255 }),
/** Nombre de personnes à former */
nbPersonnes: int("nbPersonnes"),
/** Description ou contexte de la demande */
description: text("description"),
/** Statut de l'item (en_attente, valide, refuse, suggere) */
statut: mysqlEnum("statut", ["en_attente", "valide", "refuse", "suggere"]).default("en_attente").notNull(),
/** ID de la formation du catalogue associée (suggestion ou validation) */
catalogueFormationId: int("catalogueFormationId"),
/** Budget estimé pour cet item */
budgetEstime: decimal("budgetEstime", { precision: 10, scale: 2 }),
/** Priorité (1 = haute, 2 = moyenne, 3 = basse) */
priorite: int("priorite").default(2),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type PlanFormationItem = typeof planFormationItem.$inferSelect;
export type InsertPlanFormationItem = typeof planFormationItem.$inferInsert;