Files
formation-manager-itinova/drizzle/schema.ts
Manus Sandbox 5fcc98135c Checkpoint: Ajout de 3 améliorations pour le système de notifications :
1. Interface de gestion des emails formateurs (Configuration > Formateurs) - permet de gérer les adresses email des formateurs pour activer/désactiver les notifications
2. Tableau de bord des notifications (Configuration > Notifications) - historique complet des emails envoyés avec filtres et statistiques
3. Lien vers questionnaire de satisfaction automatiquement inclus dans l'email de remerciement post-formation
2025-12-15 15:27:30 -05:00

523 lines
22 KiB
TypeScript

import { boolean, datetime, int, mysqlEnum, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";
/**
* Core user table backing auth flow.
* Extend this file with additional tables as your product grows.
* Columns use camelCase to match both database fields and generated types.
*/
export const users = mysqlTable("users", {
/**
* Surrogate primary key. Auto-incremented numeric value managed by the database.
* Use this for relations between tables.
*/
id: int("id").autoincrement().primaryKey(),
/** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */
openId: varchar("openId", { length: 64 }).notNull().unique(),
name: text("name"),
email: varchar("email", { length: 320 }),
/** Identifiant de connexion unique pour l'utilisateur */
username: varchar("username", { length: 100 }).unique(),
/** Mot de passe hashé (bcrypt) */
password: varchar("password", { length: 255 }),
loginMethod: varchar("loginMethod", { length: 64 }),
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
/** Statut du compte (actif/inactif) */
isActive: boolean("isActive").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
});
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
/**
* Table des formations (ex: Manager Itinova)
*/
export const formations = mysqlTable("formations", {
id: int("id").autoincrement().primaryKey(),
nom: varchar("nom", { length: 255 }).notNull(),
description: text("description"),
/** Lien unique pour l'inscription à cette formation */
lienUnique: varchar("lienUnique", { length: 100 }).notNull().unique(),
actif: boolean("actif").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Formation = typeof formations.$inferSelect;
export type InsertFormation = typeof formations.$inferInsert;
/**
* Table des apprenants
*/
export const apprenants = mysqlTable("apprenants", {
id: int("id").autoincrement().primaryKey(),
nom: varchar("nom", { length: 255 }).notNull(),
prenom: varchar("prenom", { length: 255 }).notNull(),
email: varchar("email", { length: 320 }).notNull().unique(),
codeEtablissement: varchar("codeEtablissement", { length: 50 }).notNull(),
fonction: mysqlEnum("fonction", ["directeur", "chef_service", "autre"]).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Apprenant = typeof apprenants.$inferSelect;
export type InsertApprenant = typeof apprenants.$inferInsert;
/**
* Table des séquences de formation
* Une séquence peut contenir jusqu'à 4 dates de formation
*/
export const sequences = mysqlTable("sequences", {
id: int("id").autoincrement().primaryKey(),
formationId: int("formationId").notNull(),
nom: varchar("nom", { length: 255 }).notNull(),
/** Lieu de la formation */
lieu: varchar("lieu", { length: 500 }).notNull(),
/** Public cible de la séquence (Directeur, Chef de service, Tous, Autre) */
publicCible: mysqlEnum("publicCible", ["directeur", "chef_service", "tous", "autre"]).notNull(),
/** ID du formateur assigné à cette séquence */
formateurId: int("formateurId"),
/** Capacité maximale (12 par défaut) */
capaciteMax: int("capaciteMax").default(12).notNull(),
/** Date de blocage des inscriptions (J-15 avant la première date) */
dateBlocage: datetime("dateBlocage").notNull(),
/** Statut de la séquence */
statut: mysqlEnum("statut", ["ouverte", "bloquee", "terminee"]).default("ouverte").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Sequence = typeof sequences.$inferSelect;
export type InsertSequence = typeof sequences.$inferInsert;
/**
* Table des dates de formation pour chaque séquence
* Une séquence peut avoir jusqu'à 4 dates
*/
export const datesFormation = mysqlTable("datesFormation", {
id: int("id").autoincrement().primaryKey(),
sequenceId: int("sequenceId").notNull(),
/** Date de début de cette journée de formation */
dateDebut: datetime("dateDebut").notNull(),
/** Date de fin de cette journée de formation */
dateFin: datetime("dateFin").notNull(),
/** Ordre de la date dans la séquence (1 à 4) */
ordre: int("ordre").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type DateFormation = typeof datesFormation.$inferSelect;
export type InsertDateFormation = typeof datesFormation.$inferInsert;
/**
* Table des inscriptions
* L'inscription à une séquence inscrit automatiquement à toutes ses dates
*/
export const inscriptions = mysqlTable("inscriptions", {
id: int("id").autoincrement().primaryKey(),
apprenantId: int("apprenantId").notNull(),
sequenceId: int("sequenceId").notNull(),
/** Statut de l'inscription */
statut: mysqlEnum("statut", ["confirmee", "liste_attente", "annulee"]).notNull(),
dateInscription: timestamp("dateInscription").defaultNow().notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Inscription = typeof inscriptions.$inferSelect;
export type InsertInscription = typeof inscriptions.$inferInsert;
/**
* Table des tokens de réinitialisation de mot de passe
* Stocke les tokens temporaires générés pour la réinitialisation
*/
export const passwordResetTokens = mysqlTable("passwordResetTokens", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
/** Token unique généré pour la réinitialisation */
token: varchar("token", { length: 255 }).notNull().unique(),
/** Date d'expiration du token (24h après génération) */
expiresAt: timestamp("expiresAt").notNull(),
/** Indique si le token a été utilisé */
used: boolean("used").default(false).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
export type InsertPasswordResetToken = typeof passwordResetTokens.$inferInsert;
/**
* Table des templates d'emails personnalisables
*/
export const emailTemplates = mysqlTable("emailTemplates", {
id: int("id").autoincrement().primaryKey(),
/** Type de template (inscription, teaser, rappel, reset_password) */
type: varchar("type", { length: 50 }).notNull().unique(),
/** Nom affiché du template */
name: varchar("name", { length: 255 }).notNull(),
/** URL du logo (peut être vide pour utiliser le logo par défaut) */
logoUrl: text("logoUrl"),
/** Couleur principale (hex) */
primaryColor: varchar("primaryColor", { length: 7 }).default("#2563eb").notNull(),
/** Couleur de fond de l'en-tête (hex) */
headerBgColor: varchar("headerBgColor", { length: 7 }).default("#2563eb").notNull(),
/** Couleur du texte de l'en-tête (hex) */
headerTextColor: varchar("headerTextColor", { length: 7 }).default("#ffffff").notNull(),
/** Titre de l'en-tête */
headerTitle: varchar("headerTitle", { length: 255 }).default("Formation Manager Itinova").notNull(),
/** Texte du pied de page */
footerText: text("footerText"),
/** Contenu du corps de l'email (HTML ou texte) */
bodyContent: text("bodyContent"),
/** Actif ou non */
active: boolean("active").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type EmailTemplate = typeof emailTemplates.$inferSelect;
export type InsertEmailTemplate = typeof emailTemplates.$inferInsert;
/**
* Table de configuration SMTP/Email
* Stocke la configuration pour l'envoi d'emails (Resend, etc.)
*/
export const emailConfig = mysqlTable("emailConfig", {
id: int("id").autoincrement().primaryKey(),
/** Service d'envoi (resend, smtp) */
provider: varchar("provider", { length: 50 }).default("resend").notNull(),
/** Clé API Resend (chiffrée) */
apiKey: text("apiKey"),
/** Email expéditeur */
fromEmail: varchar("fromEmail", { length: 320 }).notNull(),
/** Nom de l'expéditeur */
fromName: varchar("fromName", { length: 255 }).default("Formation Manager Itinova").notNull(),
/** Mode: simulation ou production */
mode: mysqlEnum("mode", ["simulation", "production"]).default("simulation").notNull(),
/** Domaine vérifié (pour Resend) */
domainVerified: boolean("domainVerified").default(false).notNull(),
/** Actif ou non */
active: boolean("active").default(true).notNull(),
// Champs SMTP
/** Hôte SMTP (ex: smtp.gmail.com) */
smtpHost: varchar("smtpHost", { length: 255 }),
/** Port SMTP (ex: 587, 465, 25) */
smtpPort: int("smtpPort"),
/** Sécurité SMTP (none, tls, ssl) */
smtpSecure: mysqlEnum("smtpSecure", ["none", "tls", "ssl"]).default("tls"),
/** Nom d'utilisateur SMTP */
smtpUser: varchar("smtpUser", { length: 320 }),
/** Mot de passe SMTP (chiffré) */
smtpPassword: text("smtpPassword"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type EmailConfig = typeof emailConfig.$inferSelect;
export type InsertEmailConfig = typeof emailConfig.$inferInsert;
/**
* Table des formateurs
* Stocke la liste des formateurs disponibles pour animer les séquences
*/
export const formateurs = mysqlTable("formateurs", {
id: int("id").autoincrement().primaryKey(),
/** Nom complet du formateur */
nom: varchar("nom", { length: 255 }).notNull(),
/** Email du formateur pour les notifications */
email: varchar("email", { length: 320 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Formateur = typeof formateurs.$inferSelect;
export type InsertFormateur = typeof formateurs.$inferInsert;
/**
* Table des alertes automatiques
* Configure les alertes à envoyer selon certaines conditions
*/
export const alertes = mysqlTable("alertes", {
id: int("id").autoincrement().primaryKey(),
/** Nom de l'alerte */
nom: varchar("nom", { length: 255 }).notNull(),
/** Type d'alerte (taux_remplissage, seuil_inscriptions) */
type: mysqlEnum("type", ["taux_remplissage", "seuil_inscriptions"]).notNull(),
/** Condition de déclenchement (ex: ">=", ">", "<=", "<", "=") */
condition: varchar("condition", { length: 10 }).notNull(),
/** Valeur seuil pour déclencher l'alerte */
valeurSeuil: int("valeurSeuil").notNull(),
/** Code établissement concerné (null = tous) */
codeEtablissement: varchar("codeEtablissement", { length: 50 }),
/** Email(s) destinataire(s) séparés par des virgules */
emailsDestinataires: text("emailsDestinataires").notNull(),
/** Actif ou non */
actif: boolean("actif").default(true).notNull(),
/** Dernière vérification */
derniereVerification: timestamp("derniereVerification"),
/** Dernier envoi d'alerte */
dernierEnvoi: timestamp("dernierEnvoi"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Alerte = typeof alertes.$inferSelect;
export type InsertAlerte = typeof alertes.$inferInsert;
/**
* Table des rappels automatiques
* Stocke les règles d'envoi automatique de rappels avant les séquences
*/
export const rappels = mysqlTable("rappels", {
id: int("id").autoincrement().primaryKey(),
/** Nom du rappel */
nom: varchar("nom", { length: 255 }).notNull(),
/** Type de template à utiliser (rappel, rappelJ1) */
templateType: varchar("templateType", { length: 50 }).notNull(),
/** Nombre de jours avant la première date de la séquence */
joursAvant: int("joursAvant").notNull(),
/** Heure d'envoi (format HH:MM) */
heureEnvoi: varchar("heureEnvoi", { length: 5 }).default("09:00").notNull(),
/** Actif ou non */
actif: boolean("actif").default(true).notNull(),
/** Dernière exécution */
derniereExecution: timestamp("derniereExecution"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Rappel = typeof rappels.$inferSelect;
export type InsertRappel = typeof rappels.$inferInsert;
/**
* Table des questionnaires
* Stocke les questionnaires de satisfaction et d'évaluation des acquis
*/
export const questionnaires = mysqlTable("questionnaires", {
id: int("id").autoincrement().primaryKey(),
/** Nom du questionnaire */
nom: varchar("nom", { length: 255 }).notNull(),
/** Type de questionnaire (satisfaction, evaluation_pre, evaluation_post) */
type: mysqlEnum("type", ["satisfaction", "evaluation_pre", "evaluation_post"]).notNull(),
/** Description du questionnaire */
description: text("description"),
/** ID de la formation liée (null = tous les formations) */
formationId: int("formationId"),
/** Actif ou non */
actif: boolean("actif").default(true).notNull(),
/** Envoi automatique après la formation */
envoiAutomatique: boolean("envoiAutomatique").default(false).notNull(),
/** Délai en jours après la dernière date de la séquence (pour envoi automatique) */
delaiEnvoiJours: int("delaiEnvoiJours").default(1).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Questionnaire = typeof questionnaires.$inferSelect;
export type InsertQuestionnaire = typeof questionnaires.$inferInsert;
/**
* Table des questions
* Stocke les questions associées à chaque questionnaire
*/
export const questions = mysqlTable("questions", {
id: int("id").autoincrement().primaryKey(),
/** ID du questionnaire */
questionnaireId: int("questionnaireId").notNull(),
/** Ordre d'affichage */
ordre: int("ordre").notNull(),
/** Texte de la question */
texte: text("texte").notNull(),
/** Type de question (choix_multiple, echelle, texte_libre, oui_non) */
typeQuestion: mysqlEnum("typeQuestion", ["choix_multiple", "echelle", "texte_libre", "oui_non"]).notNull(),
/** Options pour choix multiple (JSON array) */
options: text("options"),
/** Valeur minimale pour échelle */
echelleMin: int("echelleMin"),
/** Valeur maximale pour échelle */
echelleMax: int("echelleMax"),
/** Label pour valeur minimale (ex: "Pas du tout satisfait") */
echelleLabelMin: varchar("echelleLabelMin", { length: 100 }),
/** Label pour valeur maximale (ex: "Très satisfait") */
echelleLabelMax: varchar("echelleLabelMax", { length: 100 }),
/** Question obligatoire */
obligatoire: boolean("obligatoire").default(false).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Question = typeof questions.$inferSelect;
export type InsertQuestion = typeof questions.$inferInsert;
/**
* Table des réponses aux questionnaires
* Stocke les réponses complètes d'un apprenant à un questionnaire
*/
export const reponsesQuestionnaires = mysqlTable("reponsesQuestionnaires", {
id: int("id").autoincrement().primaryKey(),
/** ID du questionnaire */
questionnaireId: int("questionnaireId").notNull(),
/** ID de l'apprenant */
apprenantId: int("apprenantId").notNull(),
/** ID de la séquence */
sequenceId: int("sequenceId").notNull(),
/** Date de réponse */
dateReponse: timestamp("dateReponse").defaultNow().notNull(),
/** Questionnaire complété */
complete: boolean("complete").default(false).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type ReponseQuestionnaire = typeof reponsesQuestionnaires.$inferSelect;
export type InsertReponseQuestionnaire = typeof reponsesQuestionnaires.$inferInsert;
/**
* Table des réponses aux questions
* Stocke les réponses individuelles à chaque question
*/
export const reponsesQuestions = mysqlTable("reponsesQuestions", {
id: int("id").autoincrement().primaryKey(),
/** ID de la réponse au questionnaire */
reponseQuestionnaireId: int("reponseQuestionnaireId").notNull(),
/** ID de la question */
questionId: int("questionId").notNull(),
/** Réponse numérique (pour échelle, oui_non) */
reponseNumerique: int("reponseNumerique"),
/** Réponse texte (pour texte_libre, choix_multiple) */
reponseTexte: text("reponseTexte"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type ReponseQuestion = typeof reponsesQuestions.$inferSelect;
export type InsertReponseQuestion = typeof reponsesQuestions.$inferInsert;
/**
* Table des envois de questionnaires
* Stocke l'historique des envois de questionnaires aux apprenants
*/
export const envoisQuestionnaires = mysqlTable("envoisQuestionnaires", {
id: int("id").autoincrement().primaryKey(),
/** ID du questionnaire */
questionnaireId: int("questionnaireId").notNull(),
/** ID de l'apprenant */
apprenantId: int("apprenantId").notNull(),
/** ID de la séquence */
sequenceId: int("sequenceId").notNull(),
/** Date d'envoi */
dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(),
/** Token unique pour accéder au questionnaire */
token: varchar("token", { length: 100 }).notNull().unique(),
/** Date de réponse (null si pas encore répondu) */
dateReponse: timestamp("dateReponse"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type EnvoiQuestionnaire = typeof envoisQuestionnaires.$inferSelect;
export type InsertEnvoiQuestionnaire = typeof envoisQuestionnaires.$inferInsert;
/**
* Table des supports de formation
* Stocke les documents uploadés par les formateurs pour chaque séquence
*/
export const supportsFormation = mysqlTable("supportsFormation", {
id: int("id").autoincrement().primaryKey(),
/** ID de la séquence */
sequenceId: int("sequenceId").notNull(),
/** ID du formateur qui a uploadé le support */
formateurId: int("formateurId").notNull(),
/** Nom du fichier */
nomFichier: varchar("nomFichier", { length: 255 }).notNull(),
/** Type de fichier (PDF, PPTX, DOCX, etc.) */
typeFichier: varchar("typeFichier", { length: 50 }).notNull(),
/** Taille du fichier en octets */
tailleFichier: int("tailleFichier").notNull(),
/** URL du fichier dans S3 */
urlFichier: varchar("urlFichier", { length: 500 }).notNull(),
/** Clé S3 du fichier */
s3Key: varchar("s3Key", { length: 500 }).notNull(),
/** Description optionnelle du support */
description: text("description"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type SupportFormation = typeof supportsFormation.$inferSelect;
export type InsertSupportFormation = typeof supportsFormation.$inferInsert;
/**
* Table des logs des rappels envoyés
* Stocke l'historique de tous les rappels envoyés pour éviter les doublons
*/
export const logsRappels = mysqlTable("logsRappels", {
id: int("id").autoincrement().primaryKey(),
/** ID du rappel (règle de rappel) */
rappelId: int("rappelId").notNull(),
/** ID de la séquence concernée */
sequenceId: int("sequenceId").notNull(),
/** ID de l'apprenant destinataire */
apprenantId: int("apprenantId").notNull(),
/** Email du destinataire */
emailDestinataire: varchar("emailDestinataire", { length: 320 }).notNull(),
/** Type de rappel (rappel, rappelJ1) */
typeRappel: varchar("typeRappel", { length: 50 }).notNull(),
/** Date d'envoi du rappel */
dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(),
/** Statut de l'envoi (success, failed) */
statut: mysqlEnum("statut", ["success", "failed"]).notNull(),
/** Message d'erreur en cas d'échec */
messageErreur: text("messageErreur"),
/** Date de la première date de la séquence (pour référence) */
dateSequence: timestamp("dateSequence").notNull(),
/** Nombre de tentatives d'envoi */
nbTentatives: int("nbTentatives").default(1).notNull(),
/** Date du prochain essai en cas d'échec */
prochainEssai: timestamp("prochainEssai"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type LogRappel = typeof logsRappels.$inferSelect;
export type InsertLogRappel = typeof logsRappels.$inferInsert;
/**
* Table des logs des notifications envoyées
* Stocke l'historique de toutes les notifications (remerciements, alertes formateurs, etc.)
*/
export const logsNotifications = mysqlTable("logsNotifications", {
id: int("id").autoincrement().primaryKey(),
/** Type de notification (remerciement, notification_formateur_inscription, notification_formateur_annulation, alerte_capacite, notification_liste_attente) */
type: mysqlEnum("type", [
"remerciement",
"notification_formateur_inscription",
"notification_formateur_annulation",
"alerte_capacite",
"notification_liste_attente"
]).notNull(),
/** ID de la séquence concernée (si applicable) */
sequenceId: int("sequenceId"),
/** ID de l'apprenant concerné (si applicable) */
apprenantId: int("apprenantId"),
/** ID du formateur concerné (si applicable) */
formateurId: int("formateurId"),
/** Email du destinataire */
emailDestinataire: varchar("emailDestinataire", { length: 320 }).notNull(),
/** Sujet de l'email */
sujet: varchar("sujet", { length: 500 }).notNull(),
/** Date d'envoi */
dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(),
/** Statut de l'envoi (success, failed) */
statut: mysqlEnum("statut", ["success", "failed"]).notNull(),
/** Message d'erreur en cas d'échec */
messageErreur: text("messageErreur"),
/** Informations supplémentaires (JSON) */
metadata: text("metadata"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type LogNotification = typeof logsNotifications.$inferSelect;
export type InsertLogNotification = typeof logsNotifications.$inferInsert;