✅ **1. Indicateur visuel de remplissage** - Composant CapacityProgressBar avec barre de progression colorée - Affichage "X / Y" avec pourcentage - Couleurs automatiques : vert (<70%), orange (70-90%), rouge (>90%) - Intégré dans la colonne capacité du tableau des séquences ✅ **2. Vue calendrier mensuel** - Nouvelle page AdminCalendrier accessible depuis le menu - Calendrier mensuel avec navigation (mois précédent/suivant, aujourd'hui) - Affichage des séquences sur leurs dates respectives - Dialog de détails au clic sur une séquence - Vue complète avec toutes les informations (lieu, formateur, capacité, dates) ✅ **3. Système de rappels automatiques personnalisables** - Tables rappels et rappelsEnvoyes créées - Procédures tRPC complètes (list, create, update, delete) - Page AdminRappels avec interface de gestion - Configuration de rappels multiples (J-15, J-7, J-1, etc.) - Templates d'emails personnalisables avec variables dynamiques - Activation/désactivation des rappels - Infrastructure prête pour l'envoi automatique (nécessite service de planification externe) Ces améliorations facilitent grandement la planification et le suivi des formations.
252 lines
10 KiB
TypeScript
252 lines
10 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 }),
|
|
name: text("name"),
|
|
email: varchar("email", { length: 320 }),
|
|
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 formateurs
|
|
*/
|
|
export const formateurs = mysqlTable("formateurs", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
nom: varchar("nom", { length: 255 }).notNull().unique(),
|
|
actif: boolean("actif").default(true).notNull(),
|
|
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 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(),
|
|
/** Nom du formateur (optionnel) */
|
|
formateur: varchar("formateur", { length: 255 }),
|
|
/** 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 rappels automatiques
|
|
* Configure les rappels à envoyer avant les formations
|
|
*/
|
|
export const rappels = mysqlTable("rappels", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
/** Nom du rappel (ex: "Rappel J-15") */
|
|
nom: varchar("nom", { length: 255 }).notNull(),
|
|
/** Nombre de jours avant la formation (ex: 15 pour J-15) */
|
|
joursAvant: int("joursAvant").notNull(),
|
|
/** Template d'email à utiliser */
|
|
emailTemplate: text("emailTemplate").notNull(),
|
|
/** Actif ou non */
|
|
actif: boolean("actif").default(true).notNull(),
|
|
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 envois de rappels
|
|
* Historique des rappels envoyés pour éviter les doublons
|
|
*/
|
|
export const rappelsEnvoyes = mysqlTable("rappelsEnvoyes", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
rappelId: int("rappelId").notNull(),
|
|
inscriptionId: int("inscriptionId").notNull(),
|
|
dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(),
|
|
});
|
|
|
|
export type RappelEnvoye = typeof rappelsEnvoyes.$inferSelect;
|
|
export type InsertRappelEnvoye = typeof rappelsEnvoyes.$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"),
|
|
/** 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(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type EmailConfig = typeof emailConfig.$inferSelect;
|
|
export type InsertEmailConfig = typeof emailConfig.$inferInsert;
|