## Problème La procédure `emailTemplates.getByType` retournait `undefined` quand un template n'existait pas, ce qui causait une erreur React Query : "Query data cannot be undefined". Cette erreur se produisait notamment lors de la sélection des nouveaux templates (Rappel 3, 4, 5, 6) qui n'avaient pas encore été initialisés dans la base de données. ## Solution Modification de la fonction `getEmailTemplateByType` dans `server/db.ts` pour retourner `null` au lieu de `undefined` : - `if (!db) return null;` (au lieu de `return undefined;`) - `return results.length > 0 ? results[0] : null;` (au lieu de `undefined`) React Query accepte `null` comme valeur valide mais pas `undefined`. ## Résultat Les templates non initialisés peuvent maintenant être sélectionnés sans erreur. L'interface affiche correctement un état vide au lieu de crasher.
1017 lines
29 KiB
TypeScript
1017 lines
29 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
|
|
} 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 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 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 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 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(userId: number, token: string, expiresAt: Date) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const result = await db.insert(passwordResetTokens).values({
|
|
userId,
|
|
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({
|
|
name: template.name,
|
|
logoUrl: template.logoUrl,
|
|
primaryColor: template.primaryColor,
|
|
headerBgColor: template.headerBgColor,
|
|
headerTextColor: template.headerTextColor,
|
|
headerTitle: template.headerTitle,
|
|
bodyContent: template.bodyContent,
|
|
footerText: template.footerText,
|
|
active: template.active,
|
|
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) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
// Construire un objet d'insertion explicite sans derniereExecution
|
|
const insertData: Partial<InsertRappel> = {
|
|
nom: data.nom,
|
|
templateType: data.templateType,
|
|
joursAvant: data.joursAvant,
|
|
heureEnvoi: data.heureEnvoi,
|
|
actif: data.actif,
|
|
};
|
|
const result = await db.insert(rappels).values(insertData as InsertRappel);
|
|
return result;
|
|
}
|
|
|
|
export async function getRappels() {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
return await db.select().from(rappels).orderBy(rappels.joursAvant);
|
|
}
|
|
|
|
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>) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.update(rappels).set({
|
|
...data,
|
|
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");
|
|
|
|
await db.delete(rappels).where(eq(rappels.id, id));
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
|
|
// ==================== 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'));
|
|
}
|