Checkpoint: Ajout de la page de gestion des attestations de formation

Fonctionnalités :
- Configuration par formation (génération auto/manuel, envoi auto/manuel)
- Affichage de la liste des apprenants inscrits
- Upload de documents PDF par apprenant (mode manuel)
- Boutons de prévisualisation et d'envoi manuel
- Gestion complète des attestations

Corrections :
- Ajout des colonnes modeAttestation et modeEnvoi dans la table formations
- Ajout des colonnes documentUrl, documentS3Key, uploadedBy, uploadedAt, emailEnvoye, dateEnvoiEmail dans la table attestations
- Correction de l'erreur SQL en retirant la colonne urlPdf inexistante de la requête
This commit is contained in:
Manus
2026-01-12 15:13:44 -05:00
parent a7602e6740
commit 97aeacab29
12 changed files with 495 additions and 15 deletions

View File

@@ -0,0 +1,122 @@
{
"query": "DESCRIBE attestations;",
"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 DESCRIBE attestations;",
"rows": [
{
"Field": "id",
"Type": "int(11)",
"Null": "NO",
"Key": "PRI",
"Default": "NULL",
"Extra": "auto_increment"
},
{
"Field": "inscriptionId",
"Type": "int(11)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "apprenantId",
"Type": "int(11)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "sequenceId",
"Type": "int(11)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "s3Key",
"Type": "varchar(500)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "pdfUrl",
"Type": "varchar(1000)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "dateGeneration",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": ""
},
{
"Field": "emailEnvoye",
"Type": "tinyint(1)",
"Null": "NO",
"Key": "",
"Default": "0",
"Extra": ""
},
{
"Field": "dateEnvoiEmail",
"Type": "timestamp",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "createdAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": ""
},
{
"Field": "documentUrl",
"Type": "varchar(500)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "documentS3Key",
"Type": "varchar(500)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "uploadedBy",
"Type": "int(11)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "uploadedAt",
"Type": "timestamp",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
}
],
"messages": [],
"stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint(11)\tNO\tPRI\tNULL\tauto_increment\ninscriptionId\tint(11)\tNO\t\tNULL\t\napprenantId\tint(11)\tNO\t\tNULL\t\nsequenceId\tint(11)\tNO\t\tNULL\t\ns3Key\tvarchar(500)\tNO\t\tNULL\t\npdfUrl\tvarchar(1000)\tNO\t\tNULL\t\ndateGeneration\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\nemailEnvoye\ttinyint(1)\tNO\t\t0\t\ndateEnvoiEmail\ttimestamp\tYES\t\tNULL\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\ndocumentUrl\tvarchar(500)\tYES\t\tNULL\t\ndocumentS3Key\tvarchar(500)\tYES\t\tNULL\t\nuploadedBy\tint(11)\tYES\t\tNULL\t\nuploadedAt\ttimestamp\tYES\t\tNULL\t\n",
"stderr": "",
"execution_time_ms": 56
}

View File

@@ -0,0 +1,52 @@
{
"query": "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'attestations' AND TABLE_SCHEMA = DATABASE();",
"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 SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'attestations' AND TABLE_SCHEMA = DATABASE();",
"rows": [
{
"COLUMN_NAME": "id"
},
{
"COLUMN_NAME": "inscriptionId"
},
{
"COLUMN_NAME": "apprenantId"
},
{
"COLUMN_NAME": "sequenceId"
},
{
"COLUMN_NAME": "s3Key"
},
{
"COLUMN_NAME": "pdfUrl"
},
{
"COLUMN_NAME": "dateGeneration"
},
{
"COLUMN_NAME": "emailEnvoye"
},
{
"COLUMN_NAME": "dateEnvoiEmail"
},
{
"COLUMN_NAME": "createdAt"
},
{
"COLUMN_NAME": "documentUrl"
},
{
"COLUMN_NAME": "documentS3Key"
},
{
"COLUMN_NAME": "uploadedBy"
},
{
"COLUMN_NAME": "uploadedAt"
}
],
"messages": [],
"stdout": "COLUMN_NAME\nid\ninscriptionId\napprenantId\nsequenceId\ns3Key\npdfUrl\ndateGeneration\nemailEnvoye\ndateEnvoiEmail\ncreatedAt\ndocumentUrl\ndocumentS3Key\nuploadedBy\nuploadedAt\n",
"stderr": "",
"execution_time_ms": 62
}

View File

@@ -0,0 +1,9 @@
{
"query": "ALTER TABLE attestations\nADD COLUMN IF NOT EXISTS emailEnvoye BOOLEAN NOT NULL DEFAULT FALSE,\nADD COLUMN IF NOT EXISTS dateEnvoiEmail TIMESTAMP NULL;",
"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 attestations\nADD COLUMN IF NOT EXISTS emailEnvoye BOOLEAN NOT NULL DEFAULT FALSE,\nADD COLUMN IF NOT EXISTS dateEnvoiEmail TIMESTAMP NULL;",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 56
}

View File

@@ -0,0 +1,9 @@
{
"query": "SELECT id, apprenantId, sequenceId, inscriptionId FROM attestations LIMIT 5;",
"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 SELECT id, apprenantId, sequenceId, inscriptionId FROM attestations LIMIT 5;",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 63
}

View File

@@ -0,0 +1,70 @@
{
"query": "SELECT \n apprenants.id, apprenants.nom, apprenants.prenom, apprenants.email,\n inscriptions.id as inscriptionId, inscriptions.sequenceId,\n sequences.nom as sequenceNom,\n attestations.id as attestationId, attestations.documentUrl, attestations.emailEnvoye\nFROM inscriptions\nINNER JOIN apprenants ON inscriptions.apprenantId = apprenants.id\nINNER JOIN sequences ON inscriptions.sequenceId = sequences.id\nLEFT JOIN attestations ON attestations.inscriptionId = inscriptions.id\nWHERE sequences.formationId = 1 AND inscriptions.statut = 'confirmee'\nLIMIT 5;",
"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 SELECT \n apprenants.id, apprenants.nom, apprenants.prenom, apprenants.email,\n inscriptions.id as inscriptionId, inscriptions.sequenceId,\n sequences.nom as sequenceNom,\n attestations.id as attestationId, attestations.documentUrl, attestations.emailEnvoye\nFROM inscriptions\nINNER JOIN apprenants ON inscriptions.apprenantId = apprenants.id\nINNER JOIN sequences ON inscriptions.sequenceId = sequences.id\nLEFT JOIN attestations ON attestations.inscriptionId = inscriptions.id\nWHERE sequences.formationId = 1 AND inscriptions.statut = 'confirmee'\nLIMIT 5;",
"rows": [
{
"id": "1",
"nom": "JOHANSSON ",
"prenom": "Scarlett",
"email": "opareige@gmail.com",
"inscriptionId": "240001",
"sequenceId": "60001",
"sequenceNom": "Groupe C",
"attestationId": "NULL",
"documentUrl": "NULL",
"emailEnvoye": "NULL"
},
{
"id": "1",
"nom": "JOHANSSON ",
"prenom": "Scarlett",
"email": "opareige@gmail.com",
"inscriptionId": "60001",
"sequenceId": "30001",
"sequenceNom": "Groupe A",
"attestationId": "NULL",
"documentUrl": "NULL",
"emailEnvoye": "NULL"
},
{
"id": "60001",
"nom": "PITT",
"prenom": "Brad",
"email": "test2@gmail.com",
"inscriptionId": "270001",
"sequenceId": "60001",
"sequenceNom": "Groupe C",
"attestationId": "NULL",
"documentUrl": "NULL",
"emailEnvoye": "NULL"
},
{
"id": "90001",
"nom": "TEST",
"prenom": "Fred",
"email": "test.alerte@example.com",
"inscriptionId": "150001",
"sequenceId": "30001",
"sequenceNom": "Groupe A",
"attestationId": "NULL",
"documentUrl": "NULL",
"emailEnvoye": "NULL"
},
{
"id": "90002",
"nom": "AUBOISDORMANT",
"prenom": "Abel",
"email": "alerte2.test@example.com",
"inscriptionId": "150002",
"sequenceId": "30001",
"sequenceNom": "Groupe A",
"attestationId": "NULL",
"documentUrl": "NULL",
"emailEnvoye": "NULL"
}
],
"messages": [],
"stdout": "id\tnom\tprenom\temail\tinscriptionId\tsequenceId\tsequenceNom\tattestationId\tdocumentUrl\temailEnvoye\n1\tJOHANSSON \tScarlett\topareige@gmail.com\t240001\t60001\tGroupe C\tNULL\tNULL\tNULL\n1\tJOHANSSON \tScarlett\topareige@gmail.com\t60001\t30001\tGroupe A\tNULL\tNULL\tNULL\n60001\tPITT\tBrad\ttest2@gmail.com\t270001\t60001\tGroupe C\tNULL\tNULL\tNULL\n90001\tTEST\tFred\ttest.alerte@example.com\t150001\t30001\tGroupe A\tNULL\tNULL\tNULL\n90002\tAUBOISDORMANT\tAbel\talerte2.test@example.com\t150002\t30001\tGroupe A\tNULL\tNULL\tNULL\n",
"stderr": "",
"execution_time_ms": 62
}

View File

@@ -0,0 +1,122 @@
{
"query": "SHOW COLUMNS FROM attestations;",
"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 SHOW COLUMNS FROM attestations;",
"rows": [
{
"Field": "id",
"Type": "int(11)",
"Null": "NO",
"Key": "PRI",
"Default": "NULL",
"Extra": "auto_increment"
},
{
"Field": "inscriptionId",
"Type": "int(11)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "apprenantId",
"Type": "int(11)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "sequenceId",
"Type": "int(11)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "s3Key",
"Type": "varchar(500)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "pdfUrl",
"Type": "varchar(1000)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "dateGeneration",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": ""
},
{
"Field": "emailEnvoye",
"Type": "tinyint(1)",
"Null": "NO",
"Key": "",
"Default": "0",
"Extra": ""
},
{
"Field": "dateEnvoiEmail",
"Type": "timestamp",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "createdAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": ""
},
{
"Field": "documentUrl",
"Type": "varchar(500)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "documentS3Key",
"Type": "varchar(500)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "uploadedBy",
"Type": "int(11)",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "uploadedAt",
"Type": "timestamp",
"Null": "YES",
"Key": "",
"Default": "NULL",
"Extra": ""
}
],
"messages": [],
"stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint(11)\tNO\tPRI\tNULL\tauto_increment\ninscriptionId\tint(11)\tNO\t\tNULL\t\napprenantId\tint(11)\tNO\t\tNULL\t\nsequenceId\tint(11)\tNO\t\tNULL\t\ns3Key\tvarchar(500)\tNO\t\tNULL\t\npdfUrl\tvarchar(1000)\tNO\t\tNULL\t\ndateGeneration\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\nemailEnvoye\ttinyint(1)\tNO\t\t0\t\ndateEnvoiEmail\ttimestamp\tYES\t\tNULL\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\ndocumentUrl\tvarchar(500)\tYES\t\tNULL\t\ndocumentS3Key\tvarchar(500)\tYES\t\tNULL\t\nuploadedBy\tint(11)\tYES\t\tNULL\t\nuploadedAt\ttimestamp\tYES\t\tNULL\t\n",
"stderr": "",
"execution_time_ms": 67
}

View File

@@ -0,0 +1,13 @@
{
"query": "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'attestations' AND COLUMN_NAME LIKE '%pdf%';",
"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 SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'attestations' AND COLUMN_NAME LIKE '%pdf%';",
"rows": [
{
"COLUMN_NAME": "pdfUrl"
}
],
"messages": [],
"stdout": "COLUMN_NAME\npdfUrl\n",
"stderr": "",
"execution_time_ms": 59
}

View File

@@ -0,0 +1,8 @@
{
"query": "ALTER TABLE attestations\nADD COLUMN inscriptionId INT NOT NULL AFTER sequenceId,\nMODIFY COLUMN urlPdf VARCHAR(500),\nMODIFY COLUMN s3Key VARCHAR(500),\nADD COLUMN emailEnvoye BOOLEAN NOT NULL DEFAULT FALSE AFTER uploadedAt,\nADD COLUMN dateEnvoiEmail TIMESTAMP NULL AFTER emailEnvoye;",
"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 attestations\nADD COLUMN inscriptionId INT NOT NULL AFTER sequenceId,\nMODIFY COLUMN urlPdf VARCHAR(500),\nMODIFY COLUMN s3Key VARCHAR(500),\nADD COLUMN emailEnvoye BOOLEAN NOT NULL DEFAULT FALSE AFTER uploadedAt,\nADD COLUMN dateEnvoiEmail TIMESTAMP NULL AFTER emailEnvoye;",
"returncode": 1,
"logs": [
"ERROR 1060 (42S21) at line 1: Duplicate column name 'inscriptionId'"
]
}

View File

@@ -411,8 +411,21 @@ export const attestations = mysqlTable("attestations", {
id: int("id").autoincrement().primaryKey(), id: int("id").autoincrement().primaryKey(),
apprenantId: int("apprenantId").notNull(), apprenantId: int("apprenantId").notNull(),
sequenceId: int("sequenceId").notNull(), sequenceId: int("sequenceId").notNull(),
urlPdf: varchar("urlPdf", { length: 500 }).notNull(), inscriptionId: int("inscriptionId").notNull(),
s3Key: varchar("s3Key", { length: 500 }).notNull(), urlPdf: varchar("urlPdf", { length: 500 }),
s3Key: varchar("s3Key", { length: 500 }),
/** URL du document uploadé manuellement */
documentUrl: varchar("documentUrl", { length: 500 }),
/** Clé S3 du document uploadé manuellement */
documentS3Key: varchar("documentS3Key", { length: 500 }),
/** ID de l'utilisateur qui a uploadé le document */
uploadedBy: int("uploadedBy"),
/** Date d'upload du document */
uploadedAt: timestamp("uploadedAt"),
/** Email envoyé ou non */
emailEnvoye: boolean("emailEnvoye").default(false).notNull(),
/** Date d'envoi de l'email */
dateEnvoiEmail: timestamp("dateEnvoiEmail"),
dateGeneration: timestamp("dateGeneration").defaultNow().notNull(), dateGeneration: timestamp("dateGeneration").defaultNow().notNull(),
}); });

View File

@@ -145,10 +145,26 @@ export async function getApprenantsWithAttestationStatus(formationId: number) {
const results = await db const results = await db
.select({ .select({
apprenant: apprenants, apprenant: {
inscription: inscriptions, id: apprenants.id,
sequence: sequences, nom: apprenants.nom,
attestation: attestations, prenom: apprenants.prenom,
email: apprenants.email,
},
inscription: {
id: inscriptions.id,
sequenceId: inscriptions.sequenceId,
},
sequence: {
id: sequences.id,
nom: sequences.nom,
},
attestation: {
id: attestations.id,
documentUrl: attestations.documentUrl,
emailEnvoye: attestations.emailEnvoye,
dateEnvoiEmail: attestations.dateEnvoiEmail,
},
}) })
.from(inscriptions) .from(inscriptions)
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))

View File

@@ -2312,8 +2312,15 @@ export const appRouter = router({
getApprenantsWithStatus: adminProcedure getApprenantsWithStatus: adminProcedure
.input(z.object({ formationId: z.number() })) .input(z.object({ formationId: z.number() }))
.query(async ({ input }) => { .query(async ({ input }) => {
try {
const { getApprenantsWithAttestationStatus } = await import("./gestionAttestationsDb"); const { getApprenantsWithAttestationStatus } = await import("./gestionAttestationsDb");
return getApprenantsWithAttestationStatus(input.formationId); const results = await getApprenantsWithAttestationStatus(input.formationId);
console.log("[gestionAttestations] Found", results.length, "apprenants for formation", input.formationId);
return results;
} catch (error) {
console.error("[gestionAttestations] Error fetching apprenants:", error);
throw error;
}
}), }),
// Uploader un document d'attestation // Uploader un document d'attestation

View File

@@ -1,14 +1,53 @@
import { drizzle } from "drizzle-orm/mysql2"; import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise"; import { eq, and } from "drizzle-orm";
import * as schema from "./drizzle/schema.ts";
const connection = await mysql.createConnection(process.env.DATABASE_URL); const { apprenants, inscriptions, sequences, attestations } = schema;
const db = drizzle(connection);
const db = drizzle(process.env.DATABASE_URL);
try { try {
const result = await connection.query("SELECT YEAR(NOW()) as annee, MONTH(NOW()) as mois"); const results = await db
console.log("Test YEAR/MONTH réussi:", result[0]); .select({
apprenant: {
id: apprenants.id,
nom: apprenants.nom,
prenom: apprenants.prenom,
email: apprenants.email,
},
inscription: {
id: inscriptions.id,
sequenceId: inscriptions.sequenceId,
},
sequence: {
id: sequences.id,
nom: sequences.nom,
},
attestation: {
id: attestations.id,
documentUrl: attestations.documentUrl,
urlPdf: attestations.urlPdf,
emailEnvoye: attestations.emailEnvoye,
dateEnvoiEmail: attestations.dateEnvoiEmail,
},
})
.from(inscriptions)
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
.innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id))
.leftJoin(attestations, eq(attestations.inscriptionId, inscriptions.id))
.where(
and(
eq(sequences.formationId, 1),
eq(inscriptions.statut, "confirmee")
)
)
.orderBy(apprenants.nom, apprenants.prenom);
console.log("Success! Found", results.length, "results");
console.log(JSON.stringify(results, null, 2));
} catch (error) { } catch (error) {
console.error("Erreur:", error.message); console.error("Error:", error.message);
console.error("Full error:", error);
} }
await connection.end(); process.exit(0);