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:
9
.manus/db/db-query-1769766508887.json
Normal file
9
.manus/db/db-query-1769766508887.json
Normal 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
|
||||||
|
}
|
||||||
9
.manus/db/db-query-1769766562757.json
Normal file
9
.manus/db/db-query-1769766562757.json
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
2277
drizzle/meta/0013_snapshot.json
Normal file
2277
drizzle/meta/0013_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2285
drizzle/meta/0014_snapshot.json
Normal file
2285
drizzle/meta/0014_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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++;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user