Checkpoint: Ajout d'une colonne 'Mode d'envoi' dans l'historique des attestations pour distinguer les envois manuels des envois automatiques. Modifications: ajout du champ modeEnvoi dans la table historiqueAttestations, mise à jour du scheduler d'attestations, et ajout de la colonne dans l'interface utilisateur. Déployé sur le VPS de production.

This commit is contained in:
Manus
2026-01-30 06:01:30 -05:00
parent dd581594b5
commit 53609e901e
9 changed files with 4628 additions and 23 deletions

View File

@@ -0,0 +1,9 @@
{
"query": "ALTER TABLE `logsNotifications` MODIFY COLUMN `type` enum('remerciement','notification_formateur_inscription','notification_formateur_annulation','alerte_capacite','notification_liste_attente','attestation') NOT NULL;\nALTER TABLE `logsNotifications` ADD `modeEnvoi` enum('manuel','auto') DEFAULT 'manuel';",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE `logsNotifications` MODIFY COLUMN `type` enum('remerciement','notification_formateur_inscription','notification_formateur_annulation','alerte_capacite','notification_liste_attente','attestation') NOT NULL;\nALTER TABLE `logsNotifications` ADD `modeEnvoi` enum('manuel','auto') DEFAULT 'manuel';",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 586
}

View File

@@ -0,0 +1,9 @@
{
"query": "ALTER TABLE `historiqueAttestations` ADD `modeEnvoi` enum('manuel','auto') DEFAULT 'manuel';",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE `historiqueAttestations` ADD `modeEnvoi` enum('manuel','auto') DEFAULT 'manuel';",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 425
}

View File

@@ -59,6 +59,7 @@ export default function HistoriqueAttestations() {
<TableHead>Apprenant</TableHead> <TableHead>Apprenant</TableHead>
<TableHead>Email</TableHead> <TableHead>Email</TableHead>
<TableHead>Statut</TableHead> <TableHead>Statut</TableHead>
<TableHead>Mode d'envoi</TableHead>
<TableHead>Actions</TableHead> <TableHead>Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -91,6 +92,11 @@ export default function HistoriqueAttestations() {
</Badge> </Badge>
)} )}
</TableCell> </TableCell>
<TableCell>
<Badge variant={item.modeEnvoi === "auto" ? "secondary" : "outline"}>
{item.modeEnvoi === "auto" ? "Automatique" : "Manuel"}
</Badge>
</TableCell>
<TableCell> <TableCell>
{item.urlAttestation && ( {item.urlAttestation && (
<Button <Button

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -92,6 +92,20 @@
"when": 1769705546694, "when": 1769705546694,
"tag": "0012_puzzling_baron_zemo", "tag": "0012_puzzling_baron_zemo",
"breakpoints": true "breakpoints": true
},
{
"idx": 13,
"version": "5",
"when": 1769766383057,
"tag": "0013_glamorous_forge",
"breakpoints": true
},
{
"idx": 14,
"version": "5",
"when": 1769770887912,
"tag": "0014_overjoyed_overlord",
"breakpoints": true
} }
] ]
} }

View File

@@ -504,7 +504,8 @@ export const logsNotifications = mysqlTable("logsNotifications", {
"notification_formateur_inscription", "notification_formateur_inscription",
"notification_formateur_annulation", "notification_formateur_annulation",
"alerte_capacite", "alerte_capacite",
"notification_liste_attente" "notification_liste_attente",
"attestation"
]).notNull(), ]).notNull(),
/** Email du destinataire */ /** Email du destinataire */
emailDestinataire: varchar("emailDestinataire", { length: 320 }).notNull(), emailDestinataire: varchar("emailDestinataire", { length: 320 }).notNull(),
@@ -522,6 +523,8 @@ export const logsNotifications = mysqlTable("logsNotifications", {
messageErreur: text("messageErreur"), messageErreur: text("messageErreur"),
/** Métadonnées supplémentaires (JSON) */ /** Métadonnées supplémentaires (JSON) */
metadata: text("metadata"), metadata: text("metadata"),
/** Mode d'envoi (manuel ou automatique) */
modeEnvoi: mysqlEnum("modeEnvoi", ["manuel", "auto"]).default("manuel"),
dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(), dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(),
}); });
@@ -582,6 +585,8 @@ export const historiqueAttestations = mysqlTable("historiqueAttestations", {
statut: mysqlEnum("statut", ["envoye", "erreur"]).notNull(), statut: mysqlEnum("statut", ["envoye", "erreur"]).notNull(),
messageErreur: text("messageErreur"), messageErreur: text("messageErreur"),
urlAttestation: varchar("urlAttestation", { length: 500 }), urlAttestation: varchar("urlAttestation", { length: 500 }),
/** Mode d'envoi (manuel ou automatique) */
modeEnvoi: mysqlEnum("modeEnvoi", ["manuel", "auto"]).default("manuel"),
createdAt: timestamp("createdAt").defaultNow().notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(),
}); });

View File

@@ -1,8 +1,8 @@
import { getDb } from "./db"; import { getDb } from "./db";
import { sequences, inscriptions, apprenants, formations, datesFormation, attestations, logsNotifications } from "../drizzle/schema"; import { sequences, inscriptions, apprenants, formations, datesFormation, attestations, historiqueAttestations } from "../drizzle/schema";
import { eq, and, lte, sql, isNull } from "drizzle-orm"; import { eq, and, lte, sql, isNull } from "drizzle-orm";
import { sendAttestationEmail } from "./emailService"; import { sendAttestationEmail } from "./emailService";
import { logNotification } from "./notificationLogsDb"; import { enregistrerEnvoiAttestation } from "./attestationsDb";
/** /**
* Vérifie si une attestation a déjà été envoyée pour une inscription * Vérifie si une attestation a déjà été envoyée pour une inscription
@@ -13,21 +13,20 @@ async function attestationDejaEnvoyee(apprenantId: number, sequenceId: number):
try { try {
const [existing] = await db const [existing] = await db
.select({ id: logsNotifications.id }) .select({ id: historiqueAttestations.id })
.from(logsNotifications) .from(historiqueAttestations)
.where( .where(
and( and(
eq(logsNotifications.type, "attestation"), eq(historiqueAttestations.apprenantId, apprenantId),
eq(logsNotifications.apprenantId, apprenantId), eq(historiqueAttestations.sequenceId, sequenceId),
eq(logsNotifications.sequenceId, sequenceId), eq(historiqueAttestations.statut, "envoye")
eq(logsNotifications.statut, "success")
) )
) )
.limit(1); .limit(1);
return !!existing; return !!existing;
} catch (error) { } catch (error) {
console.error("[Attestations Auto] Erreur lors de la vérification du log:", error); console.error("[Attestations Auto] Erreur lors de la vérification de l'historique:", error);
return false; return false;
} }
} }
@@ -141,29 +140,26 @@ async function processAttestationSequence(
pdfUrl: attestation.documentUrl, pdfUrl: attestation.documentUrl,
}); });
// Logger la notification // Enregistrer dans l'historique
await logNotification({ await enregistrerEnvoiAttestation({
type: "attestation",
sequenceId: sequence.id, sequenceId: sequence.id,
apprenantId: apprenant.id, apprenantId: apprenant.id,
emailDestinataire: apprenant.email, statut: "envoye",
sujet: `Votre attestation de formation - ${formation.nom}`, urlAttestation: attestation.documentUrl,
statut: "success", modeEnvoi: "auto",
metadata: { attestationId: attestation.id },
}); });
envoyesCount++; envoyesCount++;
console.log(`[Attestations Auto] Email envoyé à ${apprenant.email}`); console.log(`[Attestations Auto] Email envoyé à ${apprenant.email}`);
} catch (error: any) { } catch (error: any) {
// Logger l'échec // Enregistrer l'échec dans l'historique
await logNotification({ await enregistrerEnvoiAttestation({
type: "attestation",
sequenceId: sequence.id, sequenceId: sequence.id,
apprenantId: apprenant.id, apprenantId: apprenant.id,
emailDestinataire: apprenant.email, statut: "erreur",
sujet: `Votre attestation de formation - ${formation.nom}`,
statut: "failed",
messageErreur: error.message, messageErreur: error.message,
urlAttestation: attestation.documentUrl,
modeEnvoi: "auto",
}); });
errorsCount++; errorsCount++;

View File

@@ -11,6 +11,7 @@ export async function enregistrerEnvoiAttestation(data: {
statut: "envoye" | "erreur"; statut: "envoye" | "erreur";
messageErreur?: string; messageErreur?: string;
urlAttestation?: string; urlAttestation?: string;
modeEnvoi?: "manuel" | "auto";
}) { }) {
const db = await getDb(); const db = await getDb();
if (!db) throw new Error("Database not available"); if (!db) throw new Error("Database not available");
@@ -21,6 +22,7 @@ export async function enregistrerEnvoiAttestation(data: {
statut: data.statut, statut: data.statut,
messageErreur: data.messageErreur || null, messageErreur: data.messageErreur || null,
urlAttestation: data.urlAttestation || null, urlAttestation: data.urlAttestation || null,
modeEnvoi: data.modeEnvoi || "manuel",
}); });
} }
@@ -39,6 +41,7 @@ export async function getHistoriqueAttestations() {
statut: historiqueAttestations.statut, statut: historiqueAttestations.statut,
messageErreur: historiqueAttestations.messageErreur, messageErreur: historiqueAttestations.messageErreur,
urlAttestation: historiqueAttestations.urlAttestation, urlAttestation: historiqueAttestations.urlAttestation,
modeEnvoi: historiqueAttestations.modeEnvoi,
sequence: { sequence: {
id: sequences.id, id: sequences.id,
nom: sequences.nom, nom: sequences.nom,
@@ -77,6 +80,7 @@ export async function getHistoriqueAttestationsBySequence(sequenceId: number) {
statut: historiqueAttestations.statut, statut: historiqueAttestations.statut,
messageErreur: historiqueAttestations.messageErreur, messageErreur: historiqueAttestations.messageErreur,
urlAttestation: historiqueAttestations.urlAttestation, urlAttestation: historiqueAttestations.urlAttestation,
modeEnvoi: historiqueAttestations.modeEnvoi,
apprenant: { apprenant: {
id: apprenants.id, id: apprenants.id,
nom: apprenants.nom, nom: apprenants.nom,