Checkpoint: Évolution du système de rappels pour permettre jusqu'à 2 pièces jointes au lieu d'une seule. Migration SQL appliquée en local et sur le VPS de production. Les rappels existants sont préservés.

This commit is contained in:
Manus
2026-01-28 10:13:32 -05:00
parent 8c6dda473a
commit 86d3d437bd
10 changed files with 2445 additions and 17 deletions

View File

@@ -1058,7 +1058,7 @@ export async function deleteFormateur(id: number) {
// ==================== RAPPELS ====================
export async function createRappel(data: InsertRappel & { fichier?: { nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null }) {
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");
@@ -1072,7 +1072,7 @@ export async function createRappel(data: InsertRappel & { fichier?: { nomFichier
actif: data.actif,
};
// Ajouter les champs de fichier si présents
// Ajouter les champs de fichier 1 si présents
if (data.fichier) {
insertData.nomFichier = data.fichier.nomFichier;
insertData.urlFichier = data.fichier.urlFichier;
@@ -1081,6 +1081,15 @@ export async function createRappel(data: InsertRappel & { fichier?: { nomFichier
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;
}
@@ -1151,13 +1160,13 @@ export async function getActiveRappels(): Promise<Rappel[]> {
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 }) {
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
// Gérer les champs de fichier 1
if ('fichier' in data) {
delete updateData.fichier;
if (data.fichier) {
@@ -1167,7 +1176,7 @@ export async function updateRappel(id: number, data: Partial<InsertRappel> & { f
updateData.typeFichier = data.fichier.typeFichier;
updateData.tailleFichier = data.fichier.tailleFichier;
} else {
// Supprimer le fichier
// Supprimer le fichier 1
updateData.nomFichier = null;
updateData.urlFichier = null;
updateData.s3Key = null;
@@ -1176,6 +1185,25 @@ export async function updateRappel(id: number, data: Partial<InsertRappel> & { f
}
}
// 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(),

View File

@@ -233,6 +233,9 @@ export async function sendRappelEmail(params: {
attachmentUrl?: string;
attachmentFilename?: string;
attachmentMimeType?: string;
attachmentUrl2?: string;
attachmentFilename2?: string;
attachmentMimeType2?: string;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
@@ -318,6 +321,37 @@ export async function sendRappelEmail(params: {
// Continuer l'envoi sans la pièce jointe
}
}
// Charger la deuxième pièce jointe si présente
if (params.attachmentUrl2 && params.attachmentFilename2 && params.attachmentMimeType2) {
try {
let base64: string;
// Vérifier si c'est une URL locale (commence par /uploads/)
if (params.attachmentUrl2.startsWith('/uploads/')) {
// Lire le fichier depuis le système de fichiers
const fs = await import('fs/promises');
const path = await import('path');
const filePath = path.join(process.cwd(), params.attachmentUrl2);
const fileBuffer = await fs.readFile(filePath);
base64 = fileBuffer.toString('base64');
} else {
// Télécharger le fichier depuis une URL externe (S3)
const response = await fetch(params.attachmentUrl2);
const arrayBuffer = await response.arrayBuffer();
base64 = Buffer.from(arrayBuffer).toString('base64');
}
attachments.push({
filename: params.attachmentFilename2,
content: base64,
contentType: params.attachmentMimeType2,
});
} catch (error) {
console.error('[Email] Erreur lors du chargement de la pièce jointe 2:', error);
// Continuer l'envoi sans la pièce jointe
}
}
return sendEmail({
to: params.apprenantEmail,

View File

@@ -216,6 +216,9 @@ async function processRappel(rappel: any) {
attachmentUrl: rappel.urlFichier || undefined,
attachmentFilename: rappel.nomFichier || undefined,
attachmentMimeType: rappel.typeFichier || undefined,
attachmentUrl2: rappel.urlFichier2 || undefined,
attachmentFilename2: rappel.nomFichier2 || undefined,
attachmentMimeType2: rappel.typeFichier2 || undefined,
});
console.log(`[Rappels] Email ${rappel.templateType} envoyé à ${apprenant.email} pour la séquence ${sequence.nom}`);

View File

@@ -1413,6 +1413,13 @@ export const appRouter = router({
typeFichier: z.string(),
tailleFichier: z.number(),
}).nullable().optional(),
fichier2: z.object({
nomFichier: z.string(),
urlFichier: z.string(),
s3Key: z.string(),
typeFichier: z.string(),
tailleFichier: z.number(),
}).nullable().optional(),
}))
.mutation(async ({ input }) => {
const { dateFormationIds, ...rappelData } = input;
@@ -1446,6 +1453,13 @@ export const appRouter = router({
typeFichier: z.string(),
tailleFichier: z.number(),
}).nullable().optional(),
fichier2: z.object({
nomFichier: z.string(),
urlFichier: z.string(),
s3Key: z.string(),
typeFichier: z.string(),
tailleFichier: z.number(),
}).nullable().optional(),
}))
.mutation(async ({ input }) => {
const { id, dateFormationIds, ...data } = input;