import { eq, and, ne, sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/mysql2"; import { InsertUser, users, formations, apprenants, sequences, datesFormation, inscriptions, passwordResetTokens, InsertPasswordResetToken, emailTemplates, emailConfig, InsertEmailConfig, InsertSequence, InsertDateFormation, InsertInscription, InsertFormation, InsertApprenant, Sequence, DateFormation, Apprenant, Formation, EmailTemplate, InsertEmailTemplate, formateurs, InsertFormateur, Formateur, rappels, InsertRappel, Rappel, rappelDates, logsRappels } from "../drizzle/schema"; import { ENV } from './_core/env'; let _db: ReturnType | null = null; // Lazily create the drizzle instance so local tooling can run without a DB. export async function getDb() { if (!_db && process.env.DATABASE_URL) { try { _db = drizzle(process.env.DATABASE_URL); } catch (error) { console.warn("[Database] Failed to connect:", error); _db = null; } } return _db; } export async function upsertUser(user: InsertUser): Promise { if (!user.openId) { throw new Error("User openId is required for upsert"); } const db = await getDb(); if (!db) { console.warn("[Database] Cannot upsert user: database not available"); return; } try { const values: InsertUser = { openId: user.openId, }; const updateSet: Record = {}; const textFields = ["name", "email", "loginMethod"] as const; type TextField = (typeof textFields)[number]; const assignNullable = (field: TextField) => { const value = user[field]; if (value === undefined) return; const normalized = value ?? null; values[field] = normalized; updateSet[field] = normalized; }; textFields.forEach(assignNullable); if (user.lastSignedIn !== undefined) { values.lastSignedIn = user.lastSignedIn; updateSet.lastSignedIn = user.lastSignedIn; } if (user.role !== undefined) { values.role = user.role; updateSet.role = user.role; } else if (user.openId === ENV.ownerOpenId) { values.role = 'admin'; updateSet.role = 'admin'; } if (!values.lastSignedIn) { values.lastSignedIn = new Date(); } if (Object.keys(updateSet).length === 0) { updateSet.lastSignedIn = new Date(); } await db.insert(users).values(values).onDuplicateKeyUpdate({ set: updateSet, }); } catch (error) { console.error("[Database] Failed to upsert user:", error); throw error; } } export async function getUserByOpenId(openId: string) { const db = await getDb(); if (!db) { console.warn("[Database] Cannot get user: database not available"); return undefined; } const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1); return result.length > 0 ? result[0] : undefined; } // ==================== FORMATIONS ==================== export async function createFormation(data: InsertFormation) { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(formations).values(data); return result; } export async function getFormations() { const db = await getDb(); if (!db) return []; return await db.select().from(formations); } export async function getFormationsByFormateur(formateurId: number) { const db = await getDb(); if (!db) return []; // Récupérer les IDs de formations uniques pour lesquelles le formateur a des séquences const seqs = await db.select({ formationId: sequences.formationId }) .from(sequences) .where(eq(sequences.formateurId, formateurId)); const formationIds = Array.from(new Set(seqs.map(s => s.formationId))); if (formationIds.length === 0) return []; // Récupérer les formations correspondantes const result = await db.select().from(formations).where( sql`${formations.id} IN (${sql.join(formationIds.map(id => sql`${id}`), sql`, `)})` ); return result; } export async function getFormationById(id: number) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(formations).where(eq(formations.id, id)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function getFormationByLien(lien: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(formations).where(eq(formations.lienUnique, lien)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function updateFormation(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(formations).set(data).where(eq(formations.id, id)); } export async function deleteFormation(id: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(formations).where(eq(formations.id, id)); } // ==================== SÉQUENCES ==================== export async function createSequence(data: InsertSequence) { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(sequences).values(data); return result; } export async function getSequences() { const db = await getDb(); if (!db) return []; return await db.select().from(sequences); } export async function getSequencesWithFormation() { const db = await getDb(); if (!db) return []; const seqs = await db.select().from(sequences); // Récupérer les informations de formation et de formateur pour chaque séquence return await Promise.all( seqs.map(async (seq) => { const formation = await db.select().from(formations).where(eq(formations.id, seq.formationId)).limit(1); let formateur = null; if (seq.formateurId) { const formateurResult = await db.select().from(formateurs).where(eq(formateurs.id, seq.formateurId)).limit(1); if (formateurResult.length > 0) { formateur = { id: formateurResult[0].id, nom: formateurResult[0].nom }; } } return { ...seq, formation: formation.length > 0 ? { id: formation[0].id, nom: formation[0].nom } : null, formateur, }; }) ); } export async function getSequencesByFormateur(formateurId: number) { const db = await getDb(); if (!db) return []; // Récupérer uniquement les séquences de ce formateur const seqs = await db.select().from(sequences).where(eq(sequences.formateurId, formateurId)); // Récupérer les informations de formation et de formateur pour chaque séquence return await Promise.all( seqs.map(async (seq) => { const formation = await db.select().from(formations).where(eq(formations.id, seq.formationId)).limit(1); let formateur = null; if (seq.formateurId) { const formateurResult = await db.select().from(formateurs).where(eq(formateurs.id, seq.formateurId)).limit(1); if (formateurResult.length > 0) { formateur = { id: formateurResult[0].id, nom: formateurResult[0].nom }; } } return { ...seq, formation: formation.length > 0 ? { id: formation[0].id, nom: formation[0].nom } : null, formateur, }; }) ); } export async function getSequenceById(id: number): Promise { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(sequences).where(eq(sequences.id, id)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function getSequenceByFormateurToken(token: string): Promise { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(sequences).where(eq(sequences.qrCodeFormateurToken, token)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function getSequencesByFormation(formationId: number) { const db = await getDb(); if (!db) return []; return await db.select().from(sequences).where(eq(sequences.formationId, formationId)); } export async function updateSequence(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(sequences).set(data).where(eq(sequences.id, id)); } export async function deleteSequence(id: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(sequences).where(eq(sequences.id, id)); } // ==================== DATES DE FORMATION ==================== export async function createDateFormation(data: InsertDateFormation) { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(datesFormation).values(data); return result; } export async function getDatesBySequence(sequenceId: number): Promise { const db = await getDb(); if (!db) return []; const results = await db.select() .from(datesFormation) .where(eq(datesFormation.sequenceId, sequenceId)) .orderBy(datesFormation.ordre); return results; } export async function deleteDatesBySequence(sequenceId: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(datesFormation).where(eq(datesFormation.sequenceId, sequenceId)); } // ==================== APPRENANTS ==================== export async function createApprenant(data: InsertApprenant) { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(apprenants).values(data); return result; } export async function getApprenants() { const db = await getDb(); if (!db) return []; const apprenantsData = await db.select().from(apprenants); // Pour chaque apprenant, récupérer ses inscriptions const apprenantsWithInscriptions = await Promise.all( apprenantsData.map(async (apprenant) => { const inscriptionsData = await db .select() .from(inscriptions) .where(eq(inscriptions.apprenantId, apprenant.id)); return { ...apprenant, inscriptions: inscriptionsData, }; }) ); return apprenantsWithInscriptions; } export async function getApprenantsByFormateur(formateurId: number) { const db = await getDb(); if (!db) return []; // Récupérer les IDs de séquences du formateur const seqs = await db.select({ id: sequences.id }) .from(sequences) .where(eq(sequences.formateurId, formateurId)); const sequenceIds = seqs.map(s => s.id); if (sequenceIds.length === 0) return []; // Récupérer les IDs d'apprenants inscrits à ces séquences const inscriptionsData = await db.select({ apprenantId: inscriptions.apprenantId }) .from(inscriptions) .where( sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})` ); const apprenantIds = Array.from(new Set(inscriptionsData.map(i => i.apprenantId))); if (apprenantIds.length === 0) return []; // Récupérer les apprenants correspondants const apprenantsData = await db.select().from(apprenants).where( sql`${apprenants.id} IN (${sql.join(apprenantIds.map(id => sql`${id}`), sql`, `)})` ); // Pour chaque apprenant, récupérer ses inscriptions (filtrées par les séquences du formateur) const apprenantsWithInscriptions = await Promise.all( apprenantsData.map(async (apprenant) => { const inscriptionsFiltered = await db .select() .from(inscriptions) .where( and( eq(inscriptions.apprenantId, apprenant.id), sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})` ) ); return { ...apprenant, inscriptions: inscriptionsFiltered, }; }) ); return apprenantsWithInscriptions; } export async function getApprenantById(id: number): Promise { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(apprenants).where(eq(apprenants.id, id)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function getApprenantByEmail(email: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(apprenants).where(eq(apprenants.email, email)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function updateApprenant(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(apprenants).set(data).where(eq(apprenants.id, id)); } export async function deleteApprenant(id: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(apprenants).where(eq(apprenants.id, id)); } // ==================== INSCRIPTIONS ==================== export async function createInscription(data: InsertInscription) { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(inscriptions).values(data); return result; } export async function getAllInscriptions() { const db = await getDb(); if (!db) return []; const results = await db.select({ inscription: inscriptions, apprenant: apprenants, }) .from(inscriptions) .leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)); return results; } export async function getInscriptionsBySequence(sequenceId: number) { const db = await getDb(); if (!db) return []; const results = await db.select({ inscription: inscriptions, apprenant: apprenants, }) .from(inscriptions) .leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) .where(and( eq(inscriptions.sequenceId, sequenceId), ne(inscriptions.statut, 'annulee') )); return results; } export async function getInscriptionsByApprenant(apprenantId: number) { const db = await getDb(); if (!db) return []; const results = await db.select({ inscription: inscriptions, sequence: sequences, }) .from(inscriptions) .leftJoin(sequences, eq(inscriptions.sequenceId, sequences.id)) .where(and( eq(inscriptions.apprenantId, apprenantId), ne(inscriptions.statut, 'annulee') )); return results; } export async function checkExistingInscription(apprenantId: number, sequenceId: number) { const db = await getDb(); if (!db) return undefined; const result = await db.select() .from(inscriptions) .where(and( eq(inscriptions.apprenantId, apprenantId), eq(inscriptions.sequenceId, sequenceId), ne(inscriptions.statut, 'annulee') )) .limit(1); return result.length > 0 ? result[0] : undefined; } export async function countInscriptionsBySequence(sequenceId: number, statut?: string) { const db = await getDb(); if (!db) return 0; const conditions = [eq(inscriptions.sequenceId, sequenceId)]; if (statut) { conditions.push(eq(inscriptions.statut, statut as any)); } const result = await db.select({ count: sql`count(*)` }) .from(inscriptions) .where(and(...conditions)); return result[0]?.count || 0; } export async function getInscriptionById(id: number) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(inscriptions).where(eq(inscriptions.id, id)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function updateInscription(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(inscriptions).set(data).where(eq(inscriptions.id, id)); } export async function deleteInscription(id: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(inscriptions).where(eq(inscriptions.id, id)); } // ==================== GESTION DES UTILISATEURS ==================== export async function createUser(data: InsertUser) { const db = await getDb(); if (!db) throw new Error("Database not available"); // Hasher le mot de passe si fourni if (data.password) { const bcrypt = await import('bcryptjs'); data.password = await bcrypt.hash(data.password, 10); } // Si le rôle est formateur et qu'il n'y a pas de formateurId, créer une entrée dans la table formateurs if (data.role === 'formateur' && !data.formateurId && data.name && data.email) { const formateurResult = await db.insert(formateurs).values({ nom: data.name, email: data.email, }); // Récupérer l'ID du formateur créé data.formateurId = Number(formateurResult[0].insertId); } const result = await db.insert(users).values(data); return result; } export async function getAllUsers() { const db = await getDb(); if (!db) return []; return await db.select().from(users); } export async function getUserById(id: number) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(users).where(eq(users.id, id)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function updateUser(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("Database not available"); // Hasher le mot de passe si fourni if (data.password) { const bcrypt = await import('bcryptjs'); data.password = await bcrypt.hash(data.password, 10); } // Récupérer l'utilisateur actuel const currentUser = await getUserById(id); if (!currentUser) throw new Error("User not found"); // Si le rôle change vers formateur et qu'il n'y a pas de formateurId if (data.role === 'formateur' && !currentUser.formateurId && !data.formateurId) { const name = data.name || currentUser.name; const email = data.email || currentUser.email; if (name && email) { const formateurResult = await db.insert(formateurs).values({ nom: name, email: email, }); data.formateurId = Number(formateurResult[0].insertId); } } // Si l'utilisateur est formateur et a un formateurId, synchroniser les modifications if (currentUser.formateurId && (data.name || data.email)) { const formateurUpdate: any = {}; if (data.name) formateurUpdate.nom = data.name; if (data.email) formateurUpdate.email = data.email; if (Object.keys(formateurUpdate).length > 0) { await db.update(formateurs) .set(formateurUpdate) .where(eq(formateurs.id, currentUser.formateurId)); } } await db.update(users).set(data).where(eq(users.id, id)); } export async function toggleUserStatus(id: number, isActive: boolean) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(users).set({ isActive }).where(eq(users.id, id)); } export async function deleteUser(id: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(users).where(eq(users.id, id)); } // ==================== GESTION DES TOKENS DE RÉINITIALISATION ==================== export async function createPasswordResetToken(email: string, token: string) { const db = await getDb(); if (!db) throw new Error("Database not available"); const expiresAt = new Date(); expiresAt.setHours(expiresAt.getHours() + 24); // Token valide 24h // Convertir la date au format MySQL DATETIME (YYYY-MM-DD HH:MM:SS) const mysqlDate = expiresAt.toISOString().slice(0, 19).replace('T', ' '); // Utiliser une requête SQL brute pour éviter les problèmes avec Drizzle const result = await db.execute(sql` INSERT INTO passwordResetTokens (email, token, expiresAt) VALUES (${email}, ${token}, ${mysqlDate}) `); return result; } export async function getPasswordResetToken(token: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select() .from(passwordResetTokens) .where(eq(passwordResetTokens.token, token)) .limit(1); return result.length > 0 ? result[0] : undefined; } export async function markTokenAsUsed(tokenId: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(passwordResetTokens) .set({ used: true }) .where(eq(passwordResetTokens.id, tokenId)); } export async function deleteExpiredTokens() { const db = await getDb(); if (!db) throw new Error("Database not available"); const now = new Date(); await db.delete(passwordResetTokens) .where(sql`${passwordResetTokens.expiresAt} < ${now}`); } // ============================================ // Email Templates Management // ============================================ /** * Récupère tous les templates d'emails */ export async function getAllEmailTemplates() { const db = await getDb(); if (!db) return []; return await db.select().from(emailTemplates); } /** * Récupère un template par son type */ export async function getEmailTemplateByType(type: string) { const db = await getDb(); if (!db) return null; const results = await db.select().from(emailTemplates).where(eq(emailTemplates.type, type)).limit(1); return results.length > 0 ? results[0] : null; } /** * Crée ou met à jour un template d'email */ export async function upsertEmailTemplate(template: InsertEmailTemplate) { const db = await getDb(); if (!db) throw new Error("Database not available"); const existing = await getEmailTemplateByType(template.type); if (existing) { // Mise à jour await db.update(emailTemplates) .set({ titre: template.titre, bodyContent: template.bodyContent, couleurPrincipale: template.couleurPrincipale, couleurSecondaire: template.couleurSecondaire, piedDePage: template.piedDePage, updatedAt: new Date(), }) .where(eq(emailTemplates.type, template.type)); return await getEmailTemplateByType(template.type); } else { // Création await db.insert(emailTemplates).values(template); return await getEmailTemplateByType(template.type); } } /** * Supprime un template d'email */ export async function deleteEmailTemplate(type: string) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(emailTemplates).where(eq(emailTemplates.type, type)); } /** * Initialise les templates par défaut si la table est vide */ export async function initializeDefaultEmailTemplates() { const db = await getDb(); if (!db) return; const existing = await getAllEmailTemplates(); if (existing.length > 0) return; // Déjà initialisé const defaultTemplates: InsertEmailTemplate[] = [ { type: "inscription", titre: "Confirmation d'inscription", bodyContent: "

Contenu par défaut

", couleurPrincipale: "#283581", couleurSecondaire: "#0578BE", 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: "teaser", titre: "Email teaser", bodyContent: "

Contenu par défaut

", couleurPrincipale: "#283581", couleurSecondaire: "#0578BE", 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: "rappel1", titre: "Rappel 1", couleurPrincipale: "#283581", couleurSecondaire: "#0578BE", bodyContent: `

Rappel : Votre formation commence bientôt !

Bonjour {{prenomApprenant}} {{nomApprenant}},

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

Séquence : {{nomSequence}}

Dates :

{{datesHTML}}

Lieu : {{lieu}}

Formateur : {{formateur}}

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

À très bientôt !

`, 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: "rappel2", titre: "Rappel 2", couleurPrincipale: "#283581", couleurSecondaire: "#0578BE", bodyContent: `

Rappel : Votre formation commence demain !

Bonjour {{prenomApprenant}} {{nomApprenant}},

Nous vous rappelons que votre formation {{nomFormation}} commence demain.

Séquence : {{nomSequence}}

Dates :

{{datesHTML}}

Lieu : {{lieu}}

Formateur : {{formateur}}

Merci de vous présenter à l'heure indiquée.

N'oubliez pas d'apporter le matériel nécessaire.

À demain !

`, 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: "rappel3", titre: "Rappel 3", couleurPrincipale: "#283581", couleurSecondaire: "#0578BE", bodyContent: `

Rappel : Votre formation commence bientôt !

Bonjour {{prenomApprenant}} {{nomApprenant}},

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

Séquence : {{nomSequence}}

Dates :

{{datesHTML}}

Lieu : {{lieu}}

Formateur : {{formateur}}

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

À très bientôt !

`, 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: "rappel4", titre: "Rappel 4", couleurPrincipale: "#283581", couleurSecondaire: "#0578BE", bodyContent: `

Rappel : Votre formation commence bientôt !

Bonjour {{prenomApprenant}} {{nomApprenant}},

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

Séquence : {{nomSequence}}

Dates :

{{datesHTML}}

Lieu : {{lieu}}

Formateur : {{formateur}}

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

À très bientôt !

`, 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: "rappel5", titre: "Rappel 5", couleurPrincipale: "#283581", couleurSecondaire: "#0578BE", bodyContent: `

Rappel : Votre formation commence bientôt !

Bonjour {{prenomApprenant}} {{nomApprenant}},

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

Séquence : {{nomSequence}}

Dates :

{{datesHTML}}

Lieu : {{lieu}}

Formateur : {{formateur}}

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

À très bientôt !

`, 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: "rappel6", titre: "Rappel 6", couleurPrincipale: "#283581", couleurSecondaire: "#0578BE", bodyContent: `

Rappel : Votre formation commence bientôt !

Bonjour {{prenomApprenant}} {{nomApprenant}},

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

Séquence : {{nomSequence}}

Dates :

{{datesHTML}}

Lieu : {{lieu}}

Formateur : {{formateur}}

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

À très bientôt !

`, 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: "reset_password", titre: "Réinitialisation de mot de passe", bodyContent: "

Contenu par défaut

", couleurPrincipale: "#283581", couleurSecondaire: "#0578BE", piedDePage: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", }, ]; for (const template of defaultTemplates) { await db.insert(emailTemplates).values(template); } console.log("[DB] Templates d'emails par défaut initialisés"); } // ==================== Email Config ==================== /** * Récupère la configuration email active */ export async function getActiveEmailConfig() { const db = await getDb(); if (!db) return undefined; const result = await db .select() .from(emailConfig) .where(eq(emailConfig.active, true)) .limit(1); return result.length > 0 ? result[0] : undefined; } /** * Crée ou met à jour la configuration email */ export async function upsertEmailConfig(data: InsertEmailConfig) { const db = await getDb(); if (!db) return; // Vérifier s'il existe déjà une configuration active const existing = await db .select() .from(emailConfig) .where(eq(emailConfig.active, true)) .limit(1); if (existing.length > 0) { // Mettre à jour la configuration existante await db .update(emailConfig) .set({ ...data, updatedAt: new Date(), }) .where(eq(emailConfig.id, existing[0].id)); } else { // Créer une nouvelle configuration await db.insert(emailConfig).values({ ...data, active: true, }); } } /** * Met à jour la configuration email */ export async function updateEmailConfig(id: number, data: Partial) { const db = await getDb(); if (!db) return; await db .update(emailConfig) .set({ ...data, updatedAt: new Date(), }) .where(eq(emailConfig.id, id)); } // ==================== FORMATEURS ==================== export async function createFormateur(data: InsertFormateur) { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(formateurs).values(data); return result; } export async function getFormateurs() { const db = await getDb(); if (!db) return []; return await db.select().from(formateurs); } export async function getFormateurById(id: number): Promise { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(formateurs).where(eq(formateurs.id, id)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function updateFormateur(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(formateurs).set(data).where(eq(formateurs.id, id)); } export async function deleteFormateur(id: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(formateurs).where(eq(formateurs.id, id)); } // ==================== RAPPELS ==================== export async function createRappel(data: InsertRappel & { fichier?: { nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null; fichier2?: { nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null }) { const db = await getDb(); if (!db) throw new Error("Database not available"); // Construire un objet d'insertion explicite const insertData: Partial = { nom: data.nom, templateType: data.templateType, timing: data.timing, joursAvant: data.joursAvant, heureEnvoi: data.heureEnvoi, actif: data.actif, }; // Ajouter les champs de fichier 1 si présents if (data.fichier) { insertData.nomFichier = data.fichier.nomFichier; insertData.urlFichier = data.fichier.urlFichier; insertData.s3Key = data.fichier.s3Key; insertData.typeFichier = data.fichier.typeFichier; insertData.tailleFichier = data.fichier.tailleFichier; } // Ajouter les champs de fichier 2 si présents if (data.fichier2) { insertData.nomFichier2 = data.fichier2.nomFichier; insertData.urlFichier2 = data.fichier2.urlFichier; insertData.s3Key2 = data.fichier2.s3Key; insertData.typeFichier2 = data.fichier2.typeFichier; insertData.tailleFichier2 = data.fichier2.tailleFichier; } const result = await db.insert(rappels).values(insertData as InsertRappel); return result; } export async function getRappels() { const db = await getDb(); if (!db) return []; const allRappels = await db.select().from(rappels).orderBy(rappels.joursAvant); // Pour chaque rappel, récupérer les dates de formation associées avec leurs infos const rappelsWithDates = await Promise.all( allRappels.map(async (rappel) => { const dateFormationIds = await getRappelDates(rappel.id); // Récupérer les détails des dates de formation const datesDetails = await Promise.all( dateFormationIds.map(async (dateId) => { const dateFormationResult = await db.select().from(datesFormation).where(eq(datesFormation.id, dateId)).limit(1); if (dateFormationResult.length === 0) return null; const dateFormation = dateFormationResult[0]; const sequenceResult = await db.select().from(sequences).where(eq(sequences.id, dateFormation.sequenceId)).limit(1); if (sequenceResult.length === 0) return null; const sequence = sequenceResult[0]; const formationResult = await db.select().from(formations).where(eq(formations.id, sequence.formationId)).limit(1); if (formationResult.length === 0) return null; const formation = formationResult[0]; return { dateFormationId: dateFormation.id, dateDebut: dateFormation.dateDebut, dateFin: dateFormation.dateFin, ordre: dateFormation.ordre, sequenceId: sequence.id, sequenceNom: sequence.nom, formationId: formation.id, formationNom: formation.nom, }; }) ); return { ...rappel, dateFormationIds, datesDetails: datesDetails.filter(d => d !== null), }; }) ); return rappelsWithDates; } export async function getRappelById(id: number): Promise { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(rappels).where(eq(rappels.id, id)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function getActiveRappels(): Promise { const db = await getDb(); if (!db) return []; return await db.select().from(rappels).where(eq(rappels.actif, true)); } export async function updateRappel(id: number, data: Partial & { fichier?: { nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null; fichier2?: { nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null }) { const db = await getDb(); if (!db) throw new Error("Database not available"); const updateData: any = { ...data }; // Gérer les champs de fichier 1 if ('fichier' in data) { delete updateData.fichier; if (data.fichier) { updateData.nomFichier = data.fichier.nomFichier; updateData.urlFichier = data.fichier.urlFichier; updateData.s3Key = data.fichier.s3Key; updateData.typeFichier = data.fichier.typeFichier; updateData.tailleFichier = data.fichier.tailleFichier; } else { // Supprimer le fichier 1 updateData.nomFichier = null; updateData.urlFichier = null; updateData.s3Key = null; updateData.typeFichier = null; updateData.tailleFichier = null; } } // Gérer les champs de fichier 2 if ('fichier2' in data) { delete updateData.fichier2; if (data.fichier2) { updateData.nomFichier2 = data.fichier2.nomFichier; updateData.urlFichier2 = data.fichier2.urlFichier; updateData.s3Key2 = data.fichier2.s3Key; updateData.typeFichier2 = data.fichier2.typeFichier; updateData.tailleFichier2 = data.fichier2.tailleFichier; } else { // Supprimer le fichier 2 updateData.nomFichier2 = null; updateData.urlFichier2 = null; updateData.s3Key2 = null; updateData.typeFichier2 = null; updateData.tailleFichier2 = null; } } await db.update(rappels).set({ ...updateData, updatedAt: new Date(), }).where(eq(rappels.id, id)); } export async function deleteRappel(id: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); // Supprimer d'abord les associations await db.delete(rappelDates).where(eq(rappelDates.rappelId, id)); // Puis le rappel await db.delete(rappels).where(eq(rappels.id, id)); } // ==================== RAPPEL DATES ==================== export async function createRappelDates(rappelId: number, dateFormationIds: number[]) { const db = await getDb(); if (!db) throw new Error("Database not available"); if (dateFormationIds.length === 0) return; const values = dateFormationIds.map(dateFormationId => ({ rappelId, dateFormationId, })); await db.insert(rappelDates).values(values); } export async function deleteRappelDates(rappelId: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(rappelDates).where(eq(rappelDates.rappelId, rappelId)); } export async function getRappelDates(rappelId: number): Promise { const db = await getDb(); if (!db) return []; const results = await db.select().from(rappelDates).where(eq(rappelDates.rappelId, rappelId)); return results.map(r => r.dateFormationId); } export async function updateRappelExecution(id: number) { const db = await getDb(); if (!db) return; await db.update(rappels).set({ derniereExecution: new Date(), }).where(eq(rappels.id, id)); } export async function getRappelsByDateFormation(dateFormationId: number) { const db = await getDb(); if (!db) return []; // Récupérer les IDs des rappels associés à cette date const rappelDatesResults = await db.select().from(rappelDates).where(eq(rappelDates.dateFormationId, dateFormationId)); const rappelIds = rappelDatesResults.map(rd => rd.rappelId); if (rappelIds.length === 0) { // Retourner tous les rappels actifs qui n'ont pas de dates spécifiques (rappels globaux) const allRappels = await db.select().from(rappels).where(eq(rappels.actif, true)); const globalRappels = []; for (const rappel of allRappels) { const dates = await getRappelDates(rappel.id); if (dates.length === 0) { globalRappels.push(rappel); } } return globalRappels; } // Récupérer les détails des rappels const rappelsResults = await Promise.all( rappelIds.map(async (id) => { const rappelResult = await db.select().from(rappels).where(eq(rappels.id, id)).limit(1); return rappelResult.length > 0 ? rappelResult[0] : null; }) ); // Filtrer les null et retourner return rappelsResults.filter((r): r is Rappel => r !== null); } // ==================== UTILISATEURS ADMIN ==================== /** * Récupère tous les utilisateurs avec le rôle admin */ export async function getAdminUsers() { const db = await getDb(); if (!db) return []; return await db.select().from(users).where(eq(users.role, 'admin')); } export async function getUtilisateurByEmail(email: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select() .from(users) .where(eq(users.email, email)) .limit(1); return result.length > 0 ? result[0] : undefined; } export async function updateUtilisateurPassword(userId: number, hashedPassword: string): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(users) .set({ password: hashedPassword }) .where(eq(users.id, userId)); } // ==================== LOGS RAPPELS ==================== /** * Crée un log de rappel dans l'historique */ export async function createLogRappel(logData: { rappelId: number; sequenceId: number; apprenantId: number; email: string; type: string; typeEnvoi: 'automatique' | 'test'; statut: 'succes' | 'echec'; messageErreur?: string; dateEnvoi: Date; }) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.insert(logsRappels).values({ rappelId: logData.rappelId, sequenceId: logData.sequenceId, apprenantId: logData.apprenantId, email: logData.email, type: logData.type as any, typeEnvoi: logData.typeEnvoi, statut: logData.statut, messageErreur: logData.messageErreur || null, dateEnvoi: logData.dateEnvoi, }); }