Files
formation-manager-itinova/server/db.ts
manus-admin 2cac56b8c6
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 2m22s
fix: corriger le flux de réinitialisation par e-mail
2026-09-02 11:30:37 +02:00

1405 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
EmailTemplateType,
isEmailTemplateType,
InsertEmailTemplate,
formateurs,
InsertFormateur,
Formateur,
rappels,
InsertRappel,
Rappel,
rappelDates,
logsRappels
} from "../drizzle/schema";
import { ENV } from './_core/env';
let _db: ReturnType<typeof drizzle> | 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<void> {
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<string, unknown> = {};
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<InsertFormation>) {
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<Sequence | undefined> {
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<Sequence | undefined> {
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<InsertSequence>) {
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<DateFormation[]> {
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));
}
export async function updateDateFormation(id: number, data: Partial<InsertDateFormation>) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(datesFormation)
.set(data)
.where(eq(datesFormation.id, id));
}
export async function deleteDateFormation(id: number) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.delete(datesFormation).where(eq(datesFormation.id, id));
}
// ==================== 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<Apprenant | undefined> {
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<InsertApprenant>) {
const db = await getDb();
if (!db) throw new Error("Database not available");
try {
await db.update(apprenants).set(data).where(eq(apprenants.id, id));
} catch (error: any) {
if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
throw new Error("Cet email est déjà utilisé par un autre apprenant.");
}
throw error;
}
}
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<number>`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<InsertInscription>) {
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<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);
}
// 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(userId: number, token: string) {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Un Date est converti par Drizzle avec le fuseau du serveur sans manipulation de chaîne.
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
return db.insert(passwordResetTokens).values({ userId, token, expiresAt });
}
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;
// Les services de notification peuvent demander un template non persistant.
// Dans ce cas, le générateur retombe volontairement sur son HTML de secours.
if (!isEmailTemplateType(type)) 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");
if (!isEmailTemplateType(type)) {
throw new Error("Type de template email invalide");
}
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: "<p>Contenu par défaut</p>",
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: "<p>Contenu par défaut</p>",
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: `<h2>Rappel : Votre formation commence bientôt !</h2>
<p>Bonjour {{prenomApprenant}} {{nomApprenant}},</p>
<p>Nous vous rappelons que votre formation <strong>{{nomFormation}}</strong> commence dans une semaine.</p>
<div class="info-box">
<p><strong>Séquence :</strong> {{nomSequence}}</p>
<h4>Dates :</h4>
{{datesHTML}}
<p><strong>Lieu :</strong> {{lieu}}</p>
<p><strong>Formateur :</strong> {{formateur}}</p>
</div>
<p>N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.</p>
<p>À très bientôt !</p>`,
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: `<h2>Rappel : Votre formation commence demain !</h2>
<p>Bonjour {{prenomApprenant}} {{nomApprenant}},</p>
<p>Nous vous rappelons que votre formation <strong>{{nomFormation}}</strong> commence <strong>demain</strong>.</p>
<div class="info-box">
<p><strong>Séquence :</strong> {{nomSequence}}</p>
<h4>Dates :</h4>
{{datesHTML}}
<p><strong>Lieu :</strong> {{lieu}}</p>
<p><strong>Formateur :</strong> {{formateur}}</p>
</div>
<p><strong>Merci de vous présenter à l'heure indiquée.</strong></p>
<p>N'oubliez pas d'apporter le matériel nécessaire.</p>
<p>À demain !</p>`,
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: `<h2>Rappel : Votre formation commence bientôt !</h2>
<p>Bonjour {{prenomApprenant}} {{nomApprenant}},</p>
<p>Nous vous rappelons que votre formation <strong>{{nomFormation}}</strong> commence dans une semaine.</p>
<div class="info-box">
<p><strong>Séquence :</strong> {{nomSequence}}</p>
<h4>Dates :</h4>
{{datesHTML}}
<p><strong>Lieu :</strong> {{lieu}}</p>
<p><strong>Formateur :</strong> {{formateur}}</p>
</div>
<p>N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.</p>
<p>À très bientôt !</p>`,
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: `<h2>Rappel : Votre formation commence bientôt !</h2>
<p>Bonjour {{prenomApprenant}} {{nomApprenant}},</p>
<p>Nous vous rappelons que votre formation <strong>{{nomFormation}}</strong> commence dans une semaine.</p>
<div class="info-box">
<p><strong>Séquence :</strong> {{nomSequence}}</p>
<h4>Dates :</h4>
{{datesHTML}}
<p><strong>Lieu :</strong> {{lieu}}</p>
<p><strong>Formateur :</strong> {{formateur}}</p>
</div>
<p>N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.</p>
<p>À très bientôt !</p>`,
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: `<h2>Rappel : Votre formation commence bientôt !</h2>
<p>Bonjour {{prenomApprenant}} {{nomApprenant}},</p>
<p>Nous vous rappelons que votre formation <strong>{{nomFormation}}</strong> commence dans une semaine.</p>
<div class="info-box">
<p><strong>Séquence :</strong> {{nomSequence}}</p>
<h4>Dates :</h4>
{{datesHTML}}
<p><strong>Lieu :</strong> {{lieu}}</p>
<p><strong>Formateur :</strong> {{formateur}}</p>
</div>
<p>N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.</p>
<p>À très bientôt !</p>`,
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: `<h2>Rappel : Votre formation commence bientôt !</h2>
<p>Bonjour {{prenomApprenant}} {{nomApprenant}},</p>
<p>Nous vous rappelons que votre formation <strong>{{nomFormation}}</strong> commence dans une semaine.</p>
<div class="info-box">
<p><strong>Séquence :</strong> {{nomSequence}}</p>
<h4>Dates :</h4>
{{datesHTML}}
<p><strong>Lieu :</strong> {{lieu}}</p>
<p><strong>Formateur :</strong> {{formateur}}</p>
</div>
<p>N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.</p>
<p>À très bientôt !</p>`,
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",
// Le lien doit rester une variable : chaque demande génère un token différent.
bodyContent: `<p>Bonjour {{prenomApprenant}} {{nomApprenant}},</p>
<p>Vous avez demandé la réinitialisation de votre mot de passe pour votre compte Manager Itinova.</p>
<p>Cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe :</p>
<p style="text-align: center;"><a href="{{lienReinitialisation}}" class="button">Réinitialiser mon mot de passe</a></p>
<p>Si le bouton ne fonctionne pas, copiez ce lien dans votre navigateur :</p>
<p style="word-break: break-all;">{{lienReinitialisation}}</p>
<p>Ce lien est valide pendant 24 heures et ne peut être utilisé quune seule fois.</p>`,
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<InsertEmailConfig>) {
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<Formateur | undefined> {
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<InsertFormateur>) {
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<InsertRappel> = {
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<Rappel | undefined> {
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<Rappel[]> {
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<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");
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<number[]> {
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<void> {
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,
});
}