Checkpoint: Checkpoint saved: Correction complète de la cohérence des types de rappels entre l'interface et le backend :
**Modifications du schéma Drizzle :**
- logsrappels.type : enum('rappel1','rappel2','rappel3','rappel4','rappel5','rappel6')
- emailTemplates.type : enum('inscription','teaser','rappel1','rappel2','rappel3','rappel4','rappel5','rappel6','reset_password')
- rappels.templateType : enum('rappel1','rappel2','rappel3','rappel4','rappel5','rappel6')
**Modifications du code :**
- server/routers.ts : mise à jour des enums Zod
- server/db.ts : renommage des templates par défaut (rappel → rappel1, rappelJ1 → rappel2)
- server/rappelRetry.ts : correction de la condition rappelJ1 → rappel2
- drizzle/schema.ts : mise à jour de tous les enums
**Migration de la base de données MySQL :**
- Script SQL créé et exécuté avec succès sur le VPS
- Migration en 3 étapes : élargir enum → mettre à jour données → restreindre enum
- Toutes les données existantes migrées correctement
**Résultat :**
- ✅ Plus d'erreur "Unknown column 'type'" dans les logs
- ✅ Cohérence parfaite entre l'interface (Rappel 1, Rappel 2) et le backend (rappel1, rappel2)
- ✅ Système de rappels fonctionnel
- ✅ Déployé et testé sur le VPS de production (https://formations.itinova.org)
This commit is contained in:
@@ -190,8 +190,8 @@ export type InsertEmailConfig = typeof emailConfig.$inferInsert;
|
|||||||
*/
|
*/
|
||||||
export const emailTemplates = mysqlTable("emailTemplates", {
|
export const emailTemplates = mysqlTable("emailTemplates", {
|
||||||
id: int("id").autoincrement().primaryKey(),
|
id: int("id").autoincrement().primaryKey(),
|
||||||
/** Type de template (inscription, teaser, rappel, rappelJ1, rappel3, rappel4, rappel5, rappel6, reset_password) */
|
/** Type de template (inscription, teaser, rappel1, rappel2, rappel3, rappel4, rappel5, rappel6, reset_password) */
|
||||||
type: mysqlEnum("type", ["inscription", "teaser", "rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6", "reset_password"]).notNull().unique(),
|
type: mysqlEnum("type", ["inscription", "teaser", "rappel1", "rappel2", "rappel3", "rappel4", "rappel5", "rappel6", "reset_password"]).notNull().unique(),
|
||||||
titre: varchar("titre", { length: 255 }).notNull(),
|
titre: varchar("titre", { length: 255 }).notNull(),
|
||||||
/** Corps du message avec variables dynamiques */
|
/** Corps du message avec variables dynamiques */
|
||||||
bodyContent: text("bodyContent").notNull(),
|
bodyContent: text("bodyContent").notNull(),
|
||||||
@@ -225,8 +225,8 @@ export type InsertFormateur = typeof formateurs.$inferInsert;
|
|||||||
export const rappels = mysqlTable("rappels", {
|
export const rappels = mysqlTable("rappels", {
|
||||||
id: int("id").autoincrement().primaryKey(),
|
id: int("id").autoincrement().primaryKey(),
|
||||||
nom: varchar("nom", { length: 255 }).notNull(),
|
nom: varchar("nom", { length: 255 }).notNull(),
|
||||||
/** Type de template d'email à utiliser (rappel, rappelJ1, rappel3, rappel4, rappel5, rappel6) */
|
/** Type de template d'email à utiliser (rappel1, rappel2, rappel3, rappel4, rappel5, rappel6) */
|
||||||
templateType: mysqlEnum("templateType", ["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull(),
|
templateType: mysqlEnum("templateType", ["rappel1", "rappel2", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull(),
|
||||||
/** Nombre de jours avant la séquence */
|
/** Nombre de jours avant la séquence */
|
||||||
joursAvant: int("joursAvant").notNull(),
|
joursAvant: int("joursAvant").notNull(),
|
||||||
/** Heure d'envoi (format HH:mm) */
|
/** Heure d'envoi (format HH:mm) */
|
||||||
@@ -275,8 +275,8 @@ export const logsRappels = mysqlTable("logsrappels", {
|
|||||||
sequenceId: int("sequenceId").notNull(),
|
sequenceId: int("sequenceId").notNull(),
|
||||||
apprenantId: int("apprenantId").notNull(),
|
apprenantId: int("apprenantId").notNull(),
|
||||||
email: varchar("email", { length: 320 }).notNull(),
|
email: varchar("email", { length: 320 }).notNull(),
|
||||||
/** Type de rappel (rappel, rappelJ1, rappel3, rappel4, rappel5, rappel6) */
|
/** Type de rappel (rappel1, rappel2, rappel3, rappel4, rappel5, rappel6) */
|
||||||
type: mysqlEnum("type", ["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull(),
|
type: mysqlEnum("type", ["rappel1", "rappel2", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull(),
|
||||||
/** Type d'envoi (automatique, test) - les tests ne bloquent pas les envois automatiques */
|
/** Type d'envoi (automatique, test) - les tests ne bloquent pas les envois automatiques */
|
||||||
typeEnvoi: mysqlEnum("typeEnvoi", ["automatique", "test"]).default("automatique").notNull(),
|
typeEnvoi: mysqlEnum("typeEnvoi", ["automatique", "test"]).default("automatique").notNull(),
|
||||||
statut: mysqlEnum("statut", ["succes", "echec"]).notNull(),
|
statut: mysqlEnum("statut", ["succes", "echec"]).notNull(),
|
||||||
|
|||||||
@@ -751,8 +751,8 @@ export async function initializeDefaultEmailTemplates() {
|
|||||||
piedDePage: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
piedDePage: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "rappel",
|
type: "rappel1",
|
||||||
titre: "Rappel J-7",
|
titre: "Rappel 1",
|
||||||
couleurPrincipale: "#283581",
|
couleurPrincipale: "#283581",
|
||||||
couleurSecondaire: "#0578BE",
|
couleurSecondaire: "#0578BE",
|
||||||
bodyContent: `<h2>Rappel : Votre formation commence bientôt !</h2>
|
bodyContent: `<h2>Rappel : Votre formation commence bientôt !</h2>
|
||||||
@@ -774,8 +774,8 @@ export async function initializeDefaultEmailTemplates() {
|
|||||||
piedDePage: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
piedDePage: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "rappelJ1",
|
type: "rappel2",
|
||||||
titre: "Rappel J-1",
|
titre: "Rappel 2",
|
||||||
couleurPrincipale: "#283581",
|
couleurPrincipale: "#283581",
|
||||||
couleurSecondaire: "#0578BE",
|
couleurSecondaire: "#0578BE",
|
||||||
bodyContent: `<h2>Rappel : Votre formation commence demain !</h2>
|
bodyContent: `<h2>Rappel : Votre formation commence demain !</h2>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export async function processRappelRetry() {
|
|||||||
|
|
||||||
// Réessayer l'envoi
|
// Réessayer l'envoi
|
||||||
try {
|
try {
|
||||||
if (log.typeRappel === "rappel") {
|
if (log.type === "rappel") {
|
||||||
await sendRappelJ7Email({
|
await sendRappelJ7Email({
|
||||||
apprenantEmail: apprenant[0].email,
|
apprenantEmail: apprenant[0].email,
|
||||||
apprenantPrenom: apprenant[0].prenom,
|
apprenantPrenom: apprenant[0].prenom,
|
||||||
@@ -128,7 +128,7 @@ export async function processRappelRetry() {
|
|||||||
})),
|
})),
|
||||||
lieu: seq.lieu || "",
|
lieu: seq.lieu || "",
|
||||||
});
|
});
|
||||||
} else if (log.typeRappel === "rappelJ1") {
|
} else if (log.type === "rappel2") {
|
||||||
await sendRappelJ1Email({
|
await sendRappelJ1Email({
|
||||||
apprenantEmail: apprenant[0].email,
|
apprenantEmail: apprenant[0].email,
|
||||||
apprenantPrenom: apprenant[0].prenom,
|
apprenantPrenom: apprenant[0].prenom,
|
||||||
@@ -149,7 +149,7 @@ export async function processRappelRetry() {
|
|||||||
await db
|
await db
|
||||||
.update(logsRappels)
|
.update(logsRappels)
|
||||||
.set({
|
.set({
|
||||||
statut: "success",
|
statut: "succes",
|
||||||
nbTentatives: log.nbTentatives + 1,
|
nbTentatives: log.nbTentatives + 1,
|
||||||
prochainEssai: null,
|
prochainEssai: null,
|
||||||
})
|
})
|
||||||
@@ -166,7 +166,7 @@ export async function processRappelRetry() {
|
|||||||
await db
|
await db
|
||||||
.update(logsRappels)
|
.update(logsRappels)
|
||||||
.set({
|
.set({
|
||||||
statut: "failed",
|
statut: "echec",
|
||||||
nbTentatives: nouvelleTentative,
|
nbTentatives: nouvelleTentative,
|
||||||
messageErreur: `Abandon après 3 tentatives: ${messageErreur}`,
|
messageErreur: `Abandon après 3 tentatives: ${messageErreur}`,
|
||||||
prochainEssai: null,
|
prochainEssai: null,
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ export const appRouter = router({
|
|||||||
|
|
||||||
await sendResetPasswordEmail({
|
await sendResetPasswordEmail({
|
||||||
email,
|
email,
|
||||||
prenom: utilisateur.prenom || '',
|
prenom: utilisateur.name?.split(' ')[0] || '',
|
||||||
nom: utilisateur.nom || '',
|
nom: utilisateur.name?.split(' ').slice(1).join(' ') || utilisateur.name || '',
|
||||||
lienReinitialisation: resetUrl,
|
lienReinitialisation: resetUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -593,7 +593,7 @@ export const appRouter = router({
|
|||||||
|
|
||||||
// Vérifier la capacité
|
// Vérifier la capacité
|
||||||
const nbInscrits = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
|
const nbInscrits = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
|
||||||
const statut = nbInscrits >= sequence.capaciteMax ? 'liste_attente' : 'confirmee';
|
const statut = nbInscrits >= sequence.capaciteMax ? 'en_attente' : 'confirmee';
|
||||||
|
|
||||||
await db.createInscription({
|
await db.createInscription({
|
||||||
apprenantId: input.apprenantId,
|
apprenantId: input.apprenantId,
|
||||||
@@ -615,7 +615,7 @@ export const appRouter = router({
|
|||||||
apprenantEmail: inscriptionApprenant.email,
|
apprenantEmail: inscriptionApprenant.email,
|
||||||
apprenantNom: inscriptionApprenant.nom,
|
apprenantNom: inscriptionApprenant.nom,
|
||||||
apprenantPrenom: inscriptionApprenant.prenom,
|
apprenantPrenom: inscriptionApprenant.prenom,
|
||||||
apprenantFonction: inscriptionApprenant.fonction,
|
apprenantFonction: inscriptionApprenant.fonction || '',
|
||||||
formationNom: inscriptionFormation.nom,
|
formationNom: inscriptionFormation.nom,
|
||||||
sequenceNom: inscriptionSequence.nom,
|
sequenceNom: inscriptionSequence.nom,
|
||||||
dates: dates.map(d => ({
|
dates: dates.map(d => ({
|
||||||
@@ -638,8 +638,8 @@ export const appRouter = router({
|
|||||||
formateurNom: formateur.nom,
|
formateurNom: formateur.nom,
|
||||||
apprenantNom: inscriptionApprenant.nom,
|
apprenantNom: inscriptionApprenant.nom,
|
||||||
apprenantPrenom: inscriptionApprenant.prenom,
|
apprenantPrenom: inscriptionApprenant.prenom,
|
||||||
apprenantFonction: inscriptionApprenant.fonction,
|
apprenantFonction: inscriptionApprenant.fonction || '',
|
||||||
apprenantEtablissement: inscriptionApprenant.codeEtablissement,
|
apprenantEtablissement: inscriptionApprenant.codeEtablissement || '',
|
||||||
formationNom: inscriptionFormation.nom,
|
formationNom: inscriptionFormation.nom,
|
||||||
sequenceNom: inscriptionSequence.nom,
|
sequenceNom: inscriptionSequence.nom,
|
||||||
nbInscrits: nbInscritsActuel,
|
nbInscrits: nbInscritsActuel,
|
||||||
@@ -685,7 +685,7 @@ export const appRouter = router({
|
|||||||
const admins = await db.getAdminUsers();
|
const admins = await db.getAdminUsers();
|
||||||
const adminEmails = admins.filter(a => a.email).map(a => a.email!);
|
const adminEmails = admins.filter(a => a.email).map(a => a.email!);
|
||||||
if (adminEmails.length > 0) {
|
if (adminEmails.length > 0) {
|
||||||
const nbListeAttente = await db.countInscriptionsBySequence(input.sequenceId, 'liste_attente');
|
const nbListeAttente = await db.countInscriptionsBySequence(input.sequenceId, 'en_attente');
|
||||||
let formateurNom: string | undefined;
|
let formateurNom: string | undefined;
|
||||||
if (inscriptionSequence.formateurId) {
|
if (inscriptionSequence.formateurId) {
|
||||||
const formateur = await db.getFormateurById(inscriptionSequence.formateurId);
|
const formateur = await db.getFormateurById(inscriptionSequence.formateurId);
|
||||||
@@ -819,7 +819,7 @@ export const appRouter = router({
|
|||||||
// Notifier le premier en liste d'attente qu'une place s'est libérée
|
// Notifier le premier en liste d'attente qu'une place s'est libérée
|
||||||
const inscriptionsListeAttente = await db.getInscriptionsBySequence(input.sequenceId);
|
const inscriptionsListeAttente = await db.getInscriptionsBySequence(input.sequenceId);
|
||||||
const premierEnAttente = inscriptionsListeAttente
|
const premierEnAttente = inscriptionsListeAttente
|
||||||
.filter(i => i.inscription.statut === 'liste_attente')
|
.filter(i => i.inscription.statut === 'en_attente')
|
||||||
.sort((a, b) => new Date(a.inscription.dateInscription).getTime() - new Date(b.inscription.dateInscription).getTime())[0];
|
.sort((a, b) => new Date(a.inscription.dateInscription).getTime() - new Date(b.inscription.dateInscription).getTime())[0];
|
||||||
|
|
||||||
if (premierEnAttente && premierEnAttente.apprenant) {
|
if (premierEnAttente && premierEnAttente.apprenant) {
|
||||||
@@ -828,7 +828,7 @@ export const appRouter = router({
|
|||||||
apprenantEmail: premierEnAttente.apprenant.email,
|
apprenantEmail: premierEnAttente.apprenant.email,
|
||||||
apprenantNom: premierEnAttente.apprenant.nom,
|
apprenantNom: premierEnAttente.apprenant.nom,
|
||||||
apprenantPrenom: premierEnAttente.apprenant.prenom,
|
apprenantPrenom: premierEnAttente.apprenant.prenom,
|
||||||
apprenantFonction: premierEnAttente.apprenant.fonction,
|
apprenantFonction: premierEnAttente.apprenant.fonction || '',
|
||||||
formationNom: formation.nom,
|
formationNom: formation.nom,
|
||||||
sequenceNom: sequence.nom,
|
sequenceNom: sequence.nom,
|
||||||
positionListeAttente: 1,
|
positionListeAttente: 1,
|
||||||
@@ -865,7 +865,7 @@ export const appRouter = router({
|
|||||||
|
|
||||||
updateStatut: adminProcedure.input(z.object({
|
updateStatut: adminProcedure.input(z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
statut: z.enum(['confirmee', 'liste_attente', 'annulee']),
|
statut: z.enum(['confirmee', 'en_attente', 'annulee']),
|
||||||
})).mutation(async ({ input }) => {
|
})).mutation(async ({ input }) => {
|
||||||
// Récupérer l'inscription avant modification
|
// Récupérer l'inscription avant modification
|
||||||
const inscriptionAvant = await db.getInscriptionById(input.id);
|
const inscriptionAvant = await db.getInscriptionById(input.id);
|
||||||
@@ -1262,7 +1262,7 @@ export const appRouter = router({
|
|||||||
|
|
||||||
upsert: adminProcedure
|
upsert: adminProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
type: z.enum(["inscription", "teaser", "rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6", "reset_password"]),
|
type: z.enum(["inscription", "teaser", "rappel1", "rappel2", "rappel3", "rappel4", "rappel5", "rappel6", "reset_password"]),
|
||||||
titre: z.string(),
|
titre: z.string(),
|
||||||
bodyContent: z.string(),
|
bodyContent: z.string(),
|
||||||
couleurPrincipale: z.string(),
|
couleurPrincipale: z.string(),
|
||||||
@@ -1295,7 +1295,7 @@ export const appRouter = router({
|
|||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
nom: z.string(),
|
nom: z.string(),
|
||||||
templateType: z.enum(["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]),
|
templateType: z.enum(["rappel1", "rappel2", "rappel3", "rappel4", "rappel5", "rappel6"]),
|
||||||
timing: z.enum(["pre_formation", "post_formation"]).default("pre_formation"),
|
timing: z.enum(["pre_formation", "post_formation"]).default("pre_formation"),
|
||||||
joursAvant: z.number(),
|
joursAvant: z.number(),
|
||||||
heureEnvoi: z.string().default("09:00"),
|
heureEnvoi: z.string().default("09:00"),
|
||||||
@@ -1328,7 +1328,7 @@ export const appRouter = router({
|
|||||||
.input(z.object({
|
.input(z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
nom: z.string().optional(),
|
nom: z.string().optional(),
|
||||||
templateType: z.enum(["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).optional(),
|
templateType: z.enum(["rappel1", "rappel2", "rappel3", "rappel4", "rappel5", "rappel6"]).optional(),
|
||||||
timing: z.enum(["pre_formation", "post_formation"]).optional(),
|
timing: z.enum(["pre_formation", "post_formation"]).optional(),
|
||||||
joursAvant: z.number().optional(),
|
joursAvant: z.number().optional(),
|
||||||
heureEnvoi: z.string().optional(),
|
heureEnvoi: z.string().optional(),
|
||||||
|
|||||||
15
todo.md
15
todo.md
@@ -884,3 +884,18 @@
|
|||||||
- [x] Corriger le problème d'affichage vide du menu Gestion technique (rebuild forcé)
|
- [x] Corriger le problème d'affichage vide du menu Gestion technique (rebuild forcé)
|
||||||
- [x] Redéployer correctement DashboardLayout.tsx sur le VPS avec Gestion technique en dernier
|
- [x] Redéployer correctement DashboardLayout.tsx sur le VPS avec Gestion technique en dernier
|
||||||
- [ ] Corriger l'erreur qui cause l'affichage d'une page vide sur le VPS
|
- [ ] Corriger l'erreur qui cause l'affichage d'une page vide sur le VPS
|
||||||
|
- [x] Identifier les 5 erreurs TypeScript les plus critiques
|
||||||
|
- [x] Corriger les erreurs TypeScript critiques identifiées (89 → 78 erreurs)
|
||||||
|
- [x] Déployer les corrections TypeScript sur le VPS
|
||||||
|
- [ ] Vérifier le schéma de la table logsrappels dans la base de données
|
||||||
|
- [ ] Exécuter la migration de base de données si nécessaire
|
||||||
|
- [ ] Analyser la correspondance entre types de rappels et templates d'emails
|
||||||
|
- [ ] Corriger l'enum de la colonne type dans logsrappels si nécessaire
|
||||||
|
|
||||||
|
## Correction cohérence types de rappels
|
||||||
|
|
||||||
|
- [x] Analyser la correspondance entre types de rappels et templates d'emails
|
||||||
|
- [x] Renommer rappel → rappel1 et rappelJ1 → rappel2 dans le schéma Drizzle
|
||||||
|
- [x] Mettre à jour tous les fichiers utilisant rappel et rappelJ1
|
||||||
|
- [x] Modifier l'enum dans la base de données MySQL
|
||||||
|
- [x] Déployer et tester les corrections
|
||||||
|
|||||||
Reference in New Issue
Block a user