- Ajout du champ typeEnvoi (enum 'automatique', 'test') dans le schéma Drizzle de la table logsRappels - Ajout de la fonction createLogRappel dans server/db.ts pour enregistrer les logs - Modification de la procédure testerRappel pour enregistrer chaque envoi de test avec typeEnvoi='test' - Enregistrement des succès et des échecs dans l'historique - Les tests n'interfèrent pas avec les rappels automatiques (typeEnvoi='automatique') - Migration SQL appliquée sur le VPS : ALTER TABLE logsRappels ADD COLUMN typeEnvoi - Déployé sur le VPS de production (https://formations.itinova.org) - Mise à jour du fichier todo.md Les envois de test apparaissent maintenant dans l'historique des rappels et sont distingués des envois automatiques programmés.
1328 lines
39 KiB
TypeScript
1328 lines
39 KiB
TypeScript
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<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 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));
|
|
}
|
|
|
|
// ==================== 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");
|
|
|
|
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<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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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
|
|
|
|
const result = await db.insert(passwordResetTokens).values({
|
|
email,
|
|
token,
|
|
expiresAt,
|
|
used: false,
|
|
});
|
|
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",
|
|
name: "Confirmation d'inscription",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "teaser",
|
|
name: "Email teaser",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "rappel",
|
|
name: "Rappel J-7",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
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>`,
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "rappelJ1",
|
|
name: "Rappel J-1",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
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>`,
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "rappel3",
|
|
name: "Rappel 3",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
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>`,
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "rappel4",
|
|
name: "Rappel 4",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
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>`,
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "rappel5",
|
|
name: "Rappel 5",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
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>`,
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "rappel6",
|
|
name: "Rappel 6",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
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>`,
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "reset_password",
|
|
name: "Réinitialisation de mot de passe",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
];
|
|
|
|
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 }) {
|
|
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 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;
|
|
}
|
|
|
|
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 }) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const updateData: any = { ...data };
|
|
|
|
// Gérer les champs de fichier
|
|
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
|
|
updateData.nomFichier = null;
|
|
updateData.urlFichier = null;
|
|
updateData.s3Key = null;
|
|
updateData.typeFichier = null;
|
|
updateData.tailleFichier = 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,
|
|
});
|
|
}
|