Files
formation-manager-itinova/drizzle/schema.ts
Manus 61f28781f0 Checkpoint: Correction des erreurs de colonnes dans la table rappels :
- Modification du schéma rappels : remplacement de nbJours par joursAvant pour correspondre à la structure réelle de la base de données
- Correction des noms de colonnes pour utiliser exactement la même casse que la base de données (templateType, heureEnvoi, nomFichier, urlFichier, s3Key, typeFichier, tailleFichier, derniereExecution)
- La page Rappels fonctionne maintenant correctement et affiche tous les rappels configurés sans erreurs SQL
2026-01-12 11:56:15 -05:00

515 lines
21 KiB
TypeScript

import { boolean, int, mysqlEnum, mysqlTable, text, timestamp, varchar, datetime, decimal } 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", "formateur"]).default("user").notNull(),
/** Statut du compte (actif/inactif) */
isActive: boolean("isActive").default(true).notNull(),
/** ID du formateur associé (si role = formateur) */
formateurId: int("formateurId"),
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 tokens de réinitialisation de mot de passe
*/
export const passwordResetTokens = mysqlTable("passwordResetTokens", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
token: varchar("token", { length: 255 }).notNull().unique(),
expiresAt: timestamp("expiresAt").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
export type InsertPasswordResetToken = typeof passwordResetTokens.$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 séquences de formation (ex: Groupe A, Groupe B)
* Chaque séquence peut avoir 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: varchar("lieu", { length: 255 }).notNull(),
capaciteMax: int("capaciteMax").notNull(),
statut: mysqlEnum("statut", ["ouverte", "bloquee", "terminee", "fermee", "annulee"]).default("ouverte").notNull(),
/** Public cible de la séquence (Directeur, Chef de service, Autre, Tous) */
publicCible: mysqlEnum("publicCible", ["directeur", "chef_service", "autre", "tous"]).default("tous").notNull(),
/** ID du formateur assigné à cette séquence */
formateurId: int("formateurId"),
/** Token unique pour le QR code d'émargement */
qrCodeToken: varchar("qrCodeToken", { length: 100 }).unique(),
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(),
/** Ordre de la date (1, 2, 3 ou 4) */
ordre: int("ordre").notNull(),
dateDebut: datetime("dateDebut").notNull(),
dateFin: datetime("dateFin").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type DateFormation = typeof datesFormation.$inferSelect;
export type InsertDateFormation = typeof datesFormation.$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 }),
/** Fonction de l'apprenant (Directeur, Chef de service, Autre) */
fonction: varchar("fonction", { length: 100 }),
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 inscriptions
*/
export const inscriptions = mysqlTable("inscriptions", {
id: int("id").autoincrement().primaryKey(),
sequenceId: int("sequenceId").notNull(),
apprenantId: int("apprenantId").notNull(),
statut: mysqlEnum("statut", ["confirmee", "en_attente", "annulee"]).default("confirmee").notNull(),
dateInscription: timestamp("dateInscription").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Inscription = typeof inscriptions.$inferSelect;
export type InsertInscription = typeof inscriptions.$inferInsert;
/**
* Table de configuration des emails
*/
export const emailConfig = mysqlTable("emailConfig", {
id: int("id").autoincrement().primaryKey(),
/** Provider d'envoi d'emails (resend, smtp, simulation) */
provider: mysqlEnum("provider", ["resend", "smtp", "simulation"]).default("simulation").notNull(),
/** Clé API Resend */
resendApiKey: varchar("resendApiKey", { length: 255 }),
/** Hôte SMTP */
smtpHost: varchar("smtpHost", { length: 255 }),
/** Port SMTP */
smtpPort: int("smtpPort"),
/** Sécurité SMTP (TLS, SSL, None) */
smtpSecure: mysqlEnum("smtpSecure", ["tls", "ssl", "none"]),
/** Utilisateur SMTP */
smtpUser: varchar("smtpUser", { length: 255 }),
/** Mot de passe SMTP */
smtpPassword: varchar("smtpPassword", { length: 255 }),
/** Email expéditeur */
fromEmail: varchar("fromEmail", { length: 320 }).notNull(),
/** Nom de l'expéditeur */
fromName: varchar("fromName", { length: 255 }).notNull(),
/** Mode de fonctionnement (simulation ou production) */
mode: mysqlEnum("mode", ["simulation", "production"]).default("simulation").notNull(),
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 templates d'emails
*/
export const emailTemplates = mysqlTable("emailTemplates", {
id: int("id").autoincrement().primaryKey(),
/** Type de template (inscription, teaser, rappel, rappelJ1, rappel3, rappel4, rappel5, rappel6) */
type: mysqlEnum("type", ["inscription", "teaser", "rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull().unique(),
titre: varchar("titre", { length: 255 }).notNull(),
/** Corps du message avec variables dynamiques */
bodyContent: text("bodyContent").notNull(),
couleurPrincipale: varchar("couleurPrincipale", { length: 7 }).default("#283581").notNull(),
couleurSecondaire: varchar("couleurSecondaire", { length: 7 }).default("#0578BE").notNull(),
piedDePage: text("piedDePage"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type EmailTemplate = typeof emailTemplates.$inferSelect;
export type InsertEmailTemplate = typeof emailTemplates.$inferInsert;
/**
* Table des formateurs
*/
export const formateurs = mysqlTable("formateurs", {
id: int("id").autoincrement().primaryKey(),
nom: varchar("nom", { length: 255 }).notNull(),
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 rappels automatiques
*/
export const rappels = mysqlTable("rappels", {
id: int("id").autoincrement().primaryKey(),
nom: varchar("nom", { length: 255 }).notNull(),
/** Type de template d'email à utiliser (rappel, rappelJ1, rappel3, rappel4, rappel5, rappel6) */
templateType: mysqlEnum("templateType", ["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull(),
/** Nombre de jours avant la séquence */
joursAvant: int("joursAvant").notNull(),
/** Heure d'envoi (format HH:mm) */
heureEnvoi: varchar("heureEnvoi", { length: 5 }).default("09:00").notNull(),
/** Timing du rappel (pre_formation ou post_formation) */
timing: mysqlEnum("timing", ["pre_formation", "post_formation"]).default("pre_formation").notNull(),
/** Nom du fichier joint (optionnel) */
nomFichier: varchar("nomFichier", { length: 255 }),
/** URL du fichier sur S3 (optionnel) */
urlFichier: varchar("urlFichier", { length: 500 }),
/** Clé S3 du fichier (optionnel) */
s3Key: varchar("s3Key", { length: 500 }),
/** Type MIME du fichier (optionnel) */
typeFichier: varchar("typeFichier", { length: 100 }),
/** Taille du fichier en octets (optionnel) */
tailleFichier: int("tailleFichier"),
actif: boolean("actif").default(true).notNull(),
derniereExecution: datetime("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 de liaison entre rappels et dates de formation
* Si aucune date n'est associée, le rappel s'applique à toutes les dates
*/
export const rappelDates = mysqlTable("rappelDates", {
id: int("id").autoincrement().primaryKey(),
rappelId: int("rappelId").notNull(),
dateFormationId: int("dateFormationId").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type RappelDate = typeof rappelDates.$inferSelect;
export type InsertRappelDate = typeof rappelDates.$inferInsert;
/**
* Table des logs de rappels envoyés
*/
export const logsRappels = mysqlTable("logsRappels", {
id: int("id").autoincrement().primaryKey(),
rappelId: int("rappelId").notNull(),
sequenceId: int("sequenceId").notNull(),
apprenantId: int("apprenantId").notNull(),
email: varchar("email", { length: 320 }).notNull(),
/** Type de rappel (rappel, rappelJ1, rappel3, rappel4, rappel5, rappel6) */
type: mysqlEnum("type", ["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull(),
statut: mysqlEnum("statut", ["succes", "echec"]).notNull(),
messageErreur: text("messageErreur"),
/** Nombre de tentatives d'envoi */
nbTentatives: int("nbTentatives").default(1).notNull(),
/** Date du prochain essai en cas d'échec */
prochainEssai: datetime("prochainEssai"),
dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(),
});
export type LogRappel = typeof logsRappels.$inferSelect;
export type InsertLogRappel = typeof logsRappels.$inferInsert;
/**
* Table des questionnaires de satisfaction et d'évaluation
*/
export const questionnaires = mysqlTable("questionnaires", {
id: int("id").autoincrement().primaryKey(),
titre: varchar("titre", { length: 255 }).notNull(),
description: text("description"),
/** Type de questionnaire (satisfaction, evaluation_pre, evaluation_post) */
type: mysqlEnum("type", ["satisfaction", "evaluation_pre", "evaluation_post"]).notNull(),
actif: boolean("actif").default(true).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 d'un questionnaire
*/
export const questions = mysqlTable("questions", {
id: int("id").autoincrement().primaryKey(),
questionnaireId: int("questionnaireId").notNull(),
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 les questions à choix multiples (JSON) */
options: text("options"),
/** Valeur minimale pour les échelles */
valeurMin: int("valeurMin"),
/** Valeur maximale pour les échelles */
valeurMax: int("valeurMax"),
obligatoire: boolean("obligatoire").default(false).notNull(),
ordre: int("ordre").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type Question = typeof questions.$inferSelect;
export type InsertQuestion = typeof questions.$inferInsert;
/**
* Table des réponses aux questionnaires
*/
export const reponsesQuestionnaires = mysqlTable("reponsesQuestionnaires", {
id: int("id").autoincrement().primaryKey(),
questionnaireId: int("questionnaireId").notNull(),
sequenceId: int("sequenceId").notNull(),
apprenantId: int("apprenantId").notNull(),
dateReponse: timestamp("dateReponse").defaultNow().notNull(),
});
export type ReponseQuestionnaire = typeof reponsesQuestionnaires.$inferSelect;
export type InsertReponseQuestionnaire = typeof reponsesQuestionnaires.$inferInsert;
/**
* Table des réponses individuelles aux questions
*/
export const reponsesQuestions = mysqlTable("reponsesQuestions", {
id: int("id").autoincrement().primaryKey(),
reponseQuestionnaireId: int("reponseQuestionnaireId").notNull(),
questionId: int("questionId").notNull(),
/** Réponse textuelle */
reponseTexte: text("reponseTexte"),
/** Réponse numérique (pour échelles et oui/non) */
reponseNumerique: int("reponseNumerique"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type ReponseQuestion = typeof reponsesQuestions.$inferSelect;
export type InsertReponseQuestion = typeof reponsesQuestions.$inferInsert;
/**
* Table des envois de questionnaires
*/
export const envoisQuestionnaires = mysqlTable("envoisQuestionnaires", {
id: int("id").autoincrement().primaryKey(),
questionnaireId: int("questionnaireId").notNull(),
sequenceId: int("sequenceId").notNull(),
apprenantId: int("apprenantId").notNull(),
/** Token unique pour accéder au questionnaire */
token: varchar("token", { length: 100 }).notNull().unique(),
dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(),
dateReponse: timestamp("dateReponse"),
statut: mysqlEnum("statut", ["envoye", "repondu"]).default("envoye").notNull(),
});
export type EnvoiQuestionnaire = typeof envoisQuestionnaires.$inferSelect;
export type InsertEnvoiQuestionnaire = typeof envoisQuestionnaires.$inferInsert;
/**
* Table des supports de formation uploadés par les formateurs
*/
export const supportsFormation = mysqlTable("supportsFormation", {
id: int("id").autoincrement().primaryKey(),
sequenceId: int("sequenceId").notNull(),
formateurId: int("formateurId").notNull(),
nomFichier: varchar("nomFichier", { length: 255 }).notNull(),
typeFichier: varchar("typeFichier", { length: 100 }).notNull(),
tailleFichier: int("tailleFichier").notNull(),
urlFichier: varchar("urlFichier", { length: 500 }).notNull(),
s3Key: varchar("s3Key", { length: 500 }).notNull(),
description: text("description"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type SupportFormation = typeof supportsFormation.$inferSelect;
export type InsertSupportFormation = typeof supportsFormation.$inferInsert;
/**
* Table des présences (émargement)
*/
export const presences = mysqlTable("presences", {
id: int("id").autoincrement().primaryKey(),
inscriptionId: int("inscriptionId").notNull(),
dateFormationId: int("dateFormationId").notNull(),
heurePresence: timestamp("heurePresence").defaultNow().notNull(),
/** Mode de validation (qrcode, manuel) */
modeValidation: mysqlEnum("modeValidation", ["qrcode", "manuel"]).notNull(),
/** ID de l'utilisateur qui a validé (formateur ou admin) */
validateurId: int("validateurId"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type Presence = typeof presences.$inferSelect;
export type InsertPresence = typeof presences.$inferInsert;
/**
* Table des attestations de formation
*/
export const attestations = mysqlTable("attestations", {
id: int("id").autoincrement().primaryKey(),
apprenantId: int("apprenantId").notNull(),
sequenceId: int("sequenceId").notNull(),
urlPdf: varchar("urlPdf", { length: 500 }).notNull(),
s3Key: varchar("s3Key", { length: 500 }).notNull(),
dateGeneration: timestamp("dateGeneration").defaultNow().notNull(),
});
export type Attestation = typeof attestations.$inferSelect;
export type InsertAttestation = typeof attestations.$inferInsert;
/**
* Table de configuration des attestations
*/
export const configAttestation = mysqlTable("configAttestation", {
id: int("id").autoincrement().primaryKey(),
/** Texte personnalisable de l'attestation avec variables dynamiques */
texteAttestation: text("texteAttestation").notNull(),
/** Nom du signataire */
nomSignataire: varchar("nomSignataire", { length: 255 }).notNull(),
/** Fonction du signataire */
fonctionSignataire: varchar("fonctionSignataire", { length: 255 }).notNull(),
/** URL du logo sur S3 */
logoUrl: varchar("logoUrl", { length: 500 }),
/** Clé S3 du logo */
logoS3Key: varchar("logoS3Key", { length: 500 }),
/** URL de la signature sur S3 */
signatureUrl: varchar("signatureUrl", { length: 500 }),
/** Clé S3 de la signature */
signatureS3Key: varchar("signatureS3Key", { length: 500 }),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type ConfigAttestation = typeof configAttestation.$inferSelect;
export type InsertConfigAttestation = typeof configAttestation.$inferInsert;
/**
* Table des notifications envoyées
*/
export const logsNotifications = mysqlTable("logsNotifications", {
id: int("id").autoincrement().primaryKey(),
/** Type de notification */
type: mysqlEnum("type", [
"inscription",
"annulation",
"capacite_atteinte",
"place_disponible",
"remerciement",
"formateur_inscription",
"formateur_annulation"
]).notNull(),
/** Email du destinataire */
destinataire: varchar("destinataire", { length: 320 }).notNull(),
/** ID de la séquence concernée */
sequenceId: int("sequenceId"),
/** ID de l'apprenant concerné */
apprenantId: int("apprenantId"),
/** Statut de l'envoi */
statut: mysqlEnum("statut", ["succes", "echec"]).notNull(),
/** Message d'erreur en cas d'échec */
messageErreur: text("messageErreur"),
dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(),
});
export type LogNotification = typeof logsNotifications.$inferSelect;
export type InsertLogNotification = typeof logsNotifications.$inferInsert;
/**
* Table des paramètres de l'application
* Stocke les paramètres globaux de configuration
*/
export const parametres = mysqlTable("parametres", {
id: int("id").autoincrement().primaryKey(),
/** URL publique de l'application pour les QR codes d'émargement */
urlPublique: varchar("urlPublique", { length: 500 }).notNull().default("https://formations.itinova.org"),
/** Délai d'expiration des QR codes en minutes (par défaut 30 minutes) */
delaiExpirationQR: int("delaiExpirationQR").default(30).notNull(),
/** Durée de validité des tokens d'émargement en heures (par défaut 24 heures) */
dureeValiditeToken: int("dureeValiditeToken").default(24).notNull(),
/** Activer ou désactiver les notifications automatiques */
notificationsActives: boolean("notificationsActives").default(true).notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Parametre = typeof parametres.$inferSelect;
export type InsertParametre = typeof parametres.$inferInsert;
/**
* Table de l'historique des modifications des paramètres
* Permet de tracer qui a modifié quoi et quand
*/
export const historiqueParametres = mysqlTable("historiqueParametres", {
id: int("id").autoincrement().primaryKey(),
/** ID de l'utilisateur qui a effectué la modification */
userId: int("userId").notNull(),
/** Nom de l'utilisateur (pour historique même si user supprimé) */
userName: varchar("userName", { length: 255 }).notNull(),
/** Champ modifié */
champModifie: varchar("champModifie", { length: 100 }).notNull(),
/** Ancienne valeur (JSON) */
ancienneValeur: text("ancienneValeur"),
/** Nouvelle valeur (JSON) */
nouvelleValeur: text("nouvelleValeur"),
dateModification: timestamp("dateModification").defaultNow().notNull(),
});
export type HistoriqueParametre = typeof historiqueParametres.$inferSelect;
export type InsertHistoriqueParametre = typeof historiqueParametres.$inferInsert;