This commit is contained in:
Manus
2026-02-02 17:32:58 -05:00
parent 04001e7bcd
commit 70f9055073
94 changed files with 25859 additions and 5260 deletions

View File

@@ -1,164 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { getDb } from '../db';
import { validerPresenceFormateur, getPresencesFormateurBySequence } from '../presenceDb';
import { formateurs, sequences, datesFormation, presences } from '../../drizzle/schema';
import { eq, and, sql } from 'drizzle-orm';
describe('Émargement formateur par QR code', () => {
let testFormateurId: number;
let testSequenceId: number;
let testDateFormationId: number;
const timestamp = Date.now();
beforeAll(async () => {
const db = await getDb();
if (!db) throw new Error('Database not available');
// Créer un formateur de test
const formateurResult = await db.insert(formateurs).values({
nom: `Test Formateur Émargement ${timestamp}`,
email: `test.formateur.emargement.${timestamp}@test.com`,
});
testFormateurId = Number(formateurResult[0].insertId);
// Créer une séquence de test
const sequenceResult = await db.insert(sequences).values({
formationId: 1,
nom: 'Test Séquence Émargement',
lieu: 'Test Lieu',
capaciteMax: 10,
statut: 'ouverte',
publicCible: 'tous',
formateurId: testFormateurId,
});
testSequenceId = Number(sequenceResult[0].insertId);
// Créer une date de formation de test
const dateResult = await db.insert(datesFormation).values({
sequenceId: testSequenceId,
ordre: 1,
dateDebut: new Date(),
dateFin: new Date(Date.now() + 3600000),
});
testDateFormationId = Number(dateResult[0].insertId);
});
afterAll(async () => {
const db = await getDb();
if (!db) return;
// Nettoyer les données de test
await db.delete(presences).where(eq(presences.formateurId, testFormateurId));
await db.delete(datesFormation).where(eq(datesFormation.sequenceId, testSequenceId));
await db.delete(sequences).where(eq(sequences.id, testSequenceId));
await db.delete(formateurs).where(eq(formateurs.id, testFormateurId));
});
it('devrait valider la présence du formateur pour le matin', async () => {
const result = await validerPresenceFormateur({
formateurId: testFormateurId,
sequenceId: testSequenceId,
dateFormationId: testDateFormationId,
periode: 'matin',
modeValidation: 'qrcode',
});
expect(result.success).toBe(true);
// Vérifier que la présence a bien été créée
const db = await getDb();
if (!db) throw new Error('Database not available');
const presenceCreated = await db
.select()
.from(presences)
.where(
and(
eq(presences.formateurId, testFormateurId),
eq(presences.dateFormationId, testDateFormationId),
sql`${presences}.periode = 'matin'`,
sql`${presences}.typeUtilisateur = 'formateur'`
)
);
expect(presenceCreated.length).toBe(1);
expect(presenceCreated[0].typeUtilisateur).toBe('formateur');
expect(presenceCreated[0].modeValidation).toBe('qrcode');
});
it('devrait valider la présence du formateur pour l\'après-midi', async () => {
const result = await validerPresenceFormateur({
formateurId: testFormateurId,
sequenceId: testSequenceId,
dateFormationId: testDateFormationId,
periode: 'apres_midi',
modeValidation: 'qrcode',
});
expect(result.success).toBe(true);
});
it('devrait empêcher la double validation pour la même période', async () => {
await expect(
validerPresenceFormateur({
formateurId: testFormateurId,
sequenceId: testSequenceId,
dateFormationId: testDateFormationId,
periode: 'matin',
modeValidation: 'qrcode',
})
).rejects.toThrow('Présence déjà validée');
});
it('devrait récupérer les présences du formateur pour une séquence', async () => {
const presencesFormateur = await getPresencesFormateurBySequence(testSequenceId);
expect(presencesFormateur.length).toBeGreaterThanOrEqual(2); // Matin + Après-midi
expect(presencesFormateur.every(p => p.formateurId === testFormateurId)).toBe(true);
});
it('devrait stocker la signature tactile du formateur', async () => {
const db = await getDb();
if (!db) throw new Error('Database not available');
// Créer une nouvelle date pour ce test
const dateResult = await db.insert(datesFormation).values({
sequenceId: testSequenceId,
ordre: 2,
dateDebut: new Date(),
dateFin: new Date(Date.now() + 3600000),
});
const newDateId = Number(dateResult[0].insertId);
const signatureUrl = '/uploads/signatures/test-formateur-signature.png';
const result = await validerPresenceFormateur({
formateurId: testFormateurId,
sequenceId: testSequenceId,
dateFormationId: newDateId,
periode: 'matin',
modeValidation: 'qrcode',
signatureUrl,
signatureS3Key: 'signatures/test-formateur-signature.png',
});
expect(result.success).toBe(true);
// Vérifier que la signature a été stockée
const presenceCreated = await db
.select()
.from(presences)
.where(
and(
eq(presences.formateurId, testFormateurId),
eq(presences.dateFormationId, newDateId),
sql`${presences}.periode = 'matin'`
)
);
expect(presenceCreated.length).toBe(1);
expect(presenceCreated[0].signatureUrl).toBe(signatureUrl);
// Nettoyer
await db.delete(datesFormation).where(eq(datesFormation.id, newDateId));
});
});

View File

@@ -36,16 +36,9 @@ interface ApprenantPresence {
dateFormationId: number;
periode: 'matin' | 'apres_midi';
signatureUrl: string | null;
typeUtilisateur?: 'apprenant' | 'formateur';
}>;
}
interface FormateurPresence {
dateFormationId: number;
periode: 'matin' | 'apres_midi';
signatureUrl: string | null;
}
interface FeuillePresenceInfo {
formationNom: string;
sequenceNom: string;
@@ -54,7 +47,6 @@ interface FeuillePresenceInfo {
publicCible: string;
formateur?: string;
apprenants: ApprenantPresence[];
presencesFormateur?: FormateurPresence[];
}
/**
@@ -309,56 +301,14 @@ export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffe
doc.text('Signatures du formateur :', 14, signatureY);
doc.setFont('helvetica', 'normal');
// Trouver les signatures du formateur pour cette date
const presenceFormateurMatin = feuilleInfo.presencesFormateur?.find(
p => p.dateFormationId === dateInfo.id && p.periode === 'matin'
);
const presenceFormateurApresMidi = feuilleInfo.presencesFormateur?.find(
p => p.dateFormationId === dateInfo.id && p.periode === 'apres_midi'
);
// Signature Matin
doc.setFontSize(9);
doc.text('Matin :', 14, signatureY + 8);
if (presenceFormateurMatin?.signatureUrl) {
try {
const signaturePath = path.resolve(process.cwd(), presenceFormateurMatin.signatureUrl.substring(1));
if (fs.existsSync(signaturePath)) {
const signatureBuffer = fs.readFileSync(signaturePath);
const signatureBase64 = `data:image/png;base64,${signatureBuffer.toString('base64')}`;
doc.addImage(signatureBase64, 'PNG', 30, signatureY + 2, 55, 10);
} else {
doc.line(30, signatureY + 8, 90, signatureY + 8);
}
} catch (error) {
console.error('Erreur lors du chargement de la signature formateur matin:', error);
doc.line(30, signatureY + 8, 90, signatureY + 8);
}
} else {
doc.line(30, signatureY + 8, 90, signatureY + 8);
}
doc.line(30, signatureY + 8, 90, signatureY + 8);
// Signature Après-midi
doc.text('Après-midi :', 110, signatureY + 8);
if (presenceFormateurApresMidi?.signatureUrl) {
try {
const signaturePath = path.resolve(process.cwd(), presenceFormateurApresMidi.signatureUrl.substring(1));
if (fs.existsSync(signaturePath)) {
const signatureBuffer = fs.readFileSync(signaturePath);
const signatureBase64 = `data:image/png;base64,${signatureBuffer.toString('base64')}`;
doc.addImage(signatureBase64, 'PNG', 140, signatureY + 2, 55, 10);
} else {
doc.line(140, signatureY + 8, 200, signatureY + 8);
}
} catch (error) {
console.error('Erreur lors du chargement de la signature formateur après-midi:', error);
doc.line(140, signatureY + 8, 200, signatureY + 8);
}
} else {
doc.line(140, signatureY + 8, 200, signatureY + 8);
}
doc.line(140, signatureY + 8, 200, signatureY + 8);
};
// Générer une page pour chaque date

View File

@@ -1,72 +0,0 @@
// Correction pour validerPresenceFormateur - à intégrer dans presenceDb.ts ligne 189-256
export async function validerPresenceFormateur(params: {
formateurId: number;
sequenceId: number;
dateFormationId?: number;
periode: "matin" | "apres_midi";
modeValidation: "qrcode" | "manuel";
commentaire?: string;
signatureUrl?: string;
signatureS3Key?: string;
}) {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Si dateFormationId est fourni, vérifier la date
if (params.dateFormationId) {
const dateFormation = await db
.select()
.from(datesFormation)
.where(eq(datesFormation.id, params.dateFormationId))
.limit(1);
if (dateFormation.length === 0) {
throw new Error("Date de formation introuvable");
}
// Vérifier que la date actuelle est le jour de la formation ou après
const now = new Date();
const dateDebut = new Date(dateFormation[0].dateDebut);
// Réinitialiser les heures pour comparer uniquement les dates
now.setHours(0, 0, 0, 0);
dateDebut.setHours(0, 0, 0, 0);
if (now < dateDebut) {
throw new Error("Vous ne pouvez pas émarger avant la date de formation");
}
// Vérifier si la présence existe déjà pour cette période
const presenceExistante = await db
.select()
.from(presences)
.where(
and(
eq(presences.formateurId, params.formateurId),
eq(presences.dateFormationId, params.dateFormationId),
sql`${presences.periode} = ${params.periode}`,
sql`${presences.typeUtilisateur} = 'formateur'`
)
);
if (presenceExistante.length > 0) {
throw new Error(`Présence déjà validée pour ${params.periode === 'matin' ? 'le matin' : 'l\'après-midi'}`);
}
}
// Créer la présence formateur
await db.insert(presences).values({
typeUtilisateur: "formateur",
formateurId: params.formateurId,
dateFormationId: params.dateFormationId || null,
periode: params.periode,
heurePresence: new Date(),
modeValidation: params.modeValidation,
commentaire: params.commentaire,
signatureUrl: params.signatureUrl,
signatureS3Key: params.signatureS3Key,
});
return { success: true };
}

View File

@@ -68,6 +68,7 @@ export async function validerPresence(params: {
commentaire: params.commentaire,
signatureUrl: params.signatureUrl,
signatureS3Key: params.signatureS3Key,
dateSigned: params.signatureUrl ? new Date() : undefined,
});
return { success: true };
@@ -92,6 +93,7 @@ export async function getPresencesBySequence(sequenceId: number) {
commentaire: presences.commentaire,
signatureUrl: sql<string | null>`${presences}.signatureUrl`,
signatureS3Key: sql<string | null>`${presences}.signatureS3Key`,
dateSigned: sql<Date | null>`${presences}.dateSigned`,
apprenantId: apprenants.id,
apprenantNom: apprenants.nom,
apprenantPrenom: apprenants.prenom,
@@ -126,6 +128,7 @@ export async function getPresencesByInscription(inscriptionId: number) {
commentaire: presences.commentaire,
signatureUrl: sql<string | null>`${presences}.signatureUrl`,
signatureS3Key: sql<string | null>`${presences}.signatureS3Key`,
dateSigned: sql<Date | null>`${presences}.dateSigned`,
dateDebut: datesFormation.dateDebut,
dateFin: datesFormation.dateFin,
})
@@ -182,106 +185,3 @@ export async function supprimerPresence(presenceId: number) {
return { success: true };
}
/**
* Valider la présence d'un formateur (par QR code ou manuellement)
*/
export async function validerPresenceFormateur(params: {
formateurId: number;
sequenceId: number;
dateFormationId?: number;
periode: "matin" | "apres_midi";
modeValidation: "qrcode" | "manuel";
commentaire?: string;
signatureUrl?: string;
signatureS3Key?: string;
}) {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Récupérer la date de formation
const dateFormation = await db
.select()
.from(datesFormation)
.where(eq(datesFormation.id, params.dateFormationId))
.limit(1);
if (dateFormation.length === 0) {
throw new Error("Date de formation introuvable");
}
// Vérifier que la date actuelle est le jour de la formation ou après
const now = new Date();
const dateDebut = new Date(dateFormation[0].dateDebut);
// Réinitialiser les heures pour comparer uniquement les dates
now.setHours(0, 0, 0, 0);
dateDebut.setHours(0, 0, 0, 0);
if (now < dateDebut) {
throw new Error("Vous ne pouvez pas émarger avant la date de formation");
}
// Vérifier si la présence existe déjà pour cette période
const presenceExistante = await db
.select()
.from(presences)
.where(
and(
eq(presences.formateurId, params.formateurId),
eq(presences.dateFormationId, params.dateFormationId),
sql`${presences.periode} = ${params.periode}`,
sql`${presences.typeUtilisateur} = 'formateur'`
)
);
if (presenceExistante.length > 0) {
throw new Error(`Présence déjà validée pour ${params.periode === 'matin' ? 'le matin' : 'l\'après-midi'}`);
}
// Créer la présence formateur
await db.insert(presences).values({
typeUtilisateur: "formateur",
formateurId: params.formateurId,
dateFormationId: params.dateFormationId,
periode: params.periode,
heurePresence: new Date(),
modeValidation: params.modeValidation,
commentaire: params.commentaire,
signatureUrl: params.signatureUrl,
signatureS3Key: params.signatureS3Key,
});
return { success: true };
}
/**
* Récupérer les présences du formateur pour une séquence
*/
export async function getPresencesFormateurBySequence(sequenceId: number) {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db
.select({
presenceId: presences.id,
formateurId: presences.formateurId,
dateFormationId: presences.dateFormationId,
periode: presences.periode,
heurePresence: presences.heurePresence,
modeValidation: presences.modeValidation,
commentaire: presences.commentaire,
signatureUrl: sql<string | null>`${presences}.signatureUrl`,
signatureS3Key: sql<string | null>`${presences}.signatureS3Key`,
})
.from(presences)
.innerJoin(datesFormation, eq(presences.dateFormationId, datesFormation.id))
.where(
and(
eq(datesFormation.sequenceId, sequenceId),
sql`${presences}.typeUtilisateur = 'formateur'`
)
);
return result;
}

View File

@@ -1,287 +0,0 @@
import { eq, and, sql } from "drizzle-orm";
import { presences, inscriptions, datesFormation, sequences, apprenants } from "../drizzle/schema";
import { getDb } from "./db";
/**
* Valider la présence d'un apprenant (par QR code ou manuellement)
*/
export async function validerPresence(params: {
inscriptionId: number;
dateFormationId: number;
periode: "matin" | "apres_midi";
modeValidation: "qrcode" | "manuel";
validateurId?: number;
commentaire?: string;
signatureUrl?: string;
signatureS3Key?: string;
}) {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Récupérer la date de formation
const dateFormation = await db
.select()
.from(datesFormation)
.where(eq(datesFormation.id, params.dateFormationId))
.limit(1);
if (dateFormation.length === 0) {
throw new Error("Date de formation introuvable");
}
// Vérifier que la date actuelle est le jour de la formation ou après
const now = new Date();
const dateDebut = new Date(dateFormation[0].dateDebut);
// Réinitialiser les heures pour comparer uniquement les dates
now.setHours(0, 0, 0, 0);
dateDebut.setHours(0, 0, 0, 0);
if (now < dateDebut) {
throw new Error("Vous ne pouvez pas émarger avant la date de formation");
}
// Vérifier si la présence existe déjà pour cette période
const presenceExistante = await db
.select()
.from(presences)
.where(
and(
eq(presences.inscriptionId, params.inscriptionId),
eq(presences.dateFormationId, params.dateFormationId),
sql`${presences.periode} = ${params.periode}`
)
);
if (presenceExistante.length > 0) {
throw new Error(`Présence déjà validée pour ${params.periode === 'matin' ? 'le matin' : 'l\'après-midi'}`);
}
// Créer la présence
await db.insert(presences).values({
inscriptionId: params.inscriptionId,
dateFormationId: params.dateFormationId,
periode: params.periode,
heurePresence: new Date(),
modeValidation: params.modeValidation,
validateurId: params.validateurId,
commentaire: params.commentaire,
signatureUrl: params.signatureUrl,
signatureS3Key: params.signatureS3Key,
});
return { success: true };
}
/**
* Récupérer toutes les présences pour une séquence
*/
export async function getPresencesBySequence(sequenceId: number) {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db
.select({
presenceId: presences.id,
inscriptionId: presences.inscriptionId,
dateFormationId: presences.dateFormationId,
periode: presences.periode,
heurePresence: presences.heurePresence,
modeValidation: presences.modeValidation,
validateurId: presences.validateurId,
commentaire: presences.commentaire,
signatureUrl: sql<string | null>`${presences}.signatureUrl`,
signatureS3Key: sql<string | null>`${presences}.signatureS3Key`,
apprenantId: apprenants.id,
apprenantNom: apprenants.nom,
apprenantPrenom: apprenants.prenom,
apprenantEmail: apprenants.email,
dateDebut: datesFormation.dateDebut,
dateFin: datesFormation.dateFin,
})
.from(presences)
.innerJoin(inscriptions, eq(presences.inscriptionId, inscriptions.id))
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
.innerJoin(datesFormation, eq(presences.dateFormationId, datesFormation.id))
.where(eq(datesFormation.sequenceId, sequenceId));
return result;
}
/**
* Récupérer les présences pour une inscription spécifique
*/
export async function getPresencesByInscription(inscriptionId: number) {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db
.select({
presenceId: presences.id,
dateFormationId: presences.dateFormationId,
periode: presences.periode,
heurePresence: presences.heurePresence,
modeValidation: presences.modeValidation,
validateurId: presences.validateurId,
commentaire: presences.commentaire,
signatureUrl: sql<string | null>`${presences}.signatureUrl`,
signatureS3Key: sql<string | null>`${presences}.signatureS3Key`,
dateDebut: datesFormation.dateDebut,
dateFin: datesFormation.dateFin,
})
.from(presences)
.innerJoin(datesFormation, eq(presences.dateFormationId, datesFormation.id))
.where(eq(presences.inscriptionId, inscriptionId));
return result;
}
/**
* Vérifier si toutes les présences sont validées pour une inscription
*/
export async function checkAllPresencesValidated(inscriptionId: number): Promise<boolean> {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Récupérer l'inscription avec la séquence
const inscription = await db
.select({
sequenceId: inscriptions.sequenceId,
})
.from(inscriptions)
.where(eq(inscriptions.id, inscriptionId))
.limit(1);
if (inscription.length === 0) {
throw new Error("Inscription not found");
}
// Compter le nombre de dates de formation pour cette séquence
const datesCount = await db
.select({ count: datesFormation.id })
.from(datesFormation)
.where(eq(datesFormation.sequenceId, inscription[0].sequenceId));
// Compter le nombre de présences validées pour cette inscription
const presencesCount = await db
.select({ count: presences.id })
.from(presences)
.where(eq(presences.inscriptionId, inscriptionId));
return presencesCount.length === datesCount.length;
}
/**
* Supprimer une présence
*/
export async function supprimerPresence(presenceId: number) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.delete(presences).where(eq(presences.id, presenceId));
return { success: true };
}
/**
* Valider la présence d'un formateur (par QR code ou manuellement)
*/
export async function validerPresenceFormateur(params: {
formateurId: number;
sequenceId: number;
dateFormationId?: number;
periode: "matin" | "apres_midi";
modeValidation: "qrcode" | "manuel";
commentaire?: string;
signatureUrl?: string;
signatureS3Key?: string;
}) {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Récupérer la date de formation
const dateFormation = await db
.select()
.from(datesFormation)
.where(eq(datesFormation.id, params.dateFormationId))
.limit(1);
if (dateFormation.length === 0) {
throw new Error("Date de formation introuvable");
}
// Vérifier que la date actuelle est le jour de la formation ou après
const now = new Date();
const dateDebut = new Date(dateFormation[0].dateDebut);
// Réinitialiser les heures pour comparer uniquement les dates
now.setHours(0, 0, 0, 0);
dateDebut.setHours(0, 0, 0, 0);
if (now < dateDebut) {
throw new Error("Vous ne pouvez pas émarger avant la date de formation");
}
// Vérifier si la présence existe déjà pour cette période
const presenceExistante = await db
.select()
.from(presences)
.where(
and(
eq(presences.formateurId, params.formateurId),
eq(presences.dateFormationId, params.dateFormationId),
sql`${presences.periode} = ${params.periode}`,
sql`${presences.typeUtilisateur} = 'formateur'`
)
);
if (presenceExistante.length > 0) {
throw new Error(`Présence déjà validée pour ${params.periode === 'matin' ? 'le matin' : 'l\'après-midi'}`);
}
// Créer la présence formateur
await db.insert(presences).values({
typeUtilisateur: "formateur",
formateurId: params.formateurId,
dateFormationId: params.dateFormationId,
periode: params.periode,
heurePresence: new Date(),
modeValidation: params.modeValidation,
commentaire: params.commentaire,
signatureUrl: params.signatureUrl,
signatureS3Key: params.signatureS3Key,
});
return { success: true };
}
/**
* Récupérer les présences du formateur pour une séquence
*/
export async function getPresencesFormateurBySequence(sequenceId: number) {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db
.select({
presenceId: presences.id,
formateurId: presences.formateurId,
dateFormationId: presences.dateFormationId,
periode: presences.periode,
heurePresence: presences.heurePresence,
modeValidation: presences.modeValidation,
commentaire: presences.commentaire,
signatureUrl: sql<string | null>`${presences}.signatureUrl`,
signatureS3Key: sql<string | null>`${presences}.signatureS3Key`,
})
.from(presences)
.innerJoin(datesFormation, eq(presences.dateFormationId, datesFormation.id))
.where(
and(
eq(datesFormation.sequenceId, sequenceId),
sql`${presences}.typeUtilisateur = 'formateur'`
)
);
return result;
}

View File

@@ -38,35 +38,6 @@ export async function generateQRCodeDataURL(token: string): Promise<string> {
}
}
/**
* Générer un QR code formateur en base64 à partir d'un token
* @param token Le token unique de la séquence
* @returns Une image QR code en base64 (data URL) pour l'émargement formateur
*/
export async function generateQRCodeFormateurDataURL(token: string): Promise<string> {
// URL complète pour scanner le QR code formateur
const parametres = await getParametres();
const url = `${parametres.urlPublique}/emargement-formateur/${token}`;
try {
const qrCodeDataURL = await QRCode.toDataURL(url, {
errorCorrectionLevel: "H",
type: "image/png",
width: 400,
margin: 2,
color: {
dark: "#000000",
light: "#FFFFFF",
},
});
return qrCodeDataURL;
} catch (error) {
console.error("Erreur lors de la génération du QR code formateur:", error);
throw new Error("Impossible de générer le QR code formateur");
}
}
/**
* Générer un QR code en buffer PNG
* @param token Le token unique de la séquence

View File

@@ -1156,7 +1156,7 @@ export const appRouter = router({
}
// Récupérer toutes les présences avec signatures pour cette séquence
const { getPresencesBySequence, getPresencesFormateurBySequence } = await import("./presenceDb");
const { getPresencesBySequence } = await import("./presenceDb");
const presences = await getPresencesBySequence(input.sequenceId);
const data = inscriptions
@@ -1180,9 +1180,6 @@ export const appRouter = router({
};
});
// Récupérer les présences du formateur
const presencesFormateur = await getPresencesFormateurBySequence(input.sequenceId);
const buffer = await generateFeuillePresence({
formationNom: formation.nom,
sequenceNom: sequence.nom,
@@ -1196,11 +1193,6 @@ export const appRouter = router({
publicCible: sequence.publicCible,
formateur: formateurNom,
apprenants: data,
presencesFormateur: presencesFormateur.map(p => ({
dateFormationId: p.dateFormationId,
periode: p.periode as 'matin' | 'apres_midi',
signatureUrl: p.signatureUrl,
})),
});
return {
@@ -2517,239 +2509,70 @@ export const appRouter = router({
};
}),
// Générer un QR code formateur pour une séquence
generateQRCodeFormateur: protectedProcedure
.input(z.object({
sequenceId: z.number(),
}))
.mutation(async ({ input, ctx }) => {
const { generateQRToken, generateQRCodeFormateurDataURL } = await import("./qrCodeGenerator");
const { getDb } = await import("./db");
const { sequences } = await import("../drizzle/schema");
const { eq } = await import("drizzle-orm");
const db = await getDb();
if (!db) throw new Error("Database not available");
// Vérifier que la séquence existe
const sequence = await db.select().from(sequences).where(eq(sequences.id, input.sequenceId)).limit(1);
if (sequence.length === 0) {
throw new Error("Séquence introuvable");
}
// Générer un nouveau token si nécessaire
let token = sequence[0].qrCodeToken;
if (!token) {
token = generateQRToken();
await db.update(sequences).set({ qrCodeToken: token }).where(eq(sequences.id, input.sequenceId));
}
// Générer le QR code formateur
const qrCodeDataURL = await generateQRCodeFormateurDataURL(token);
return {
token,
qrCodeDataURL,
};
}),
// Récupérer les informations de la séquence par token (pour formateurs)
getSequenceByToken: publicProcedure
.input(z.object({
token: z.string(),
}))
.query(async ({ input }) => {
const { getDb, getDatesBySequence } = await import("./db");
const { sequences } = await import("../drizzle/schema");
const { eq } = await import("drizzle-orm");
const db = await getDb();
if (!db) throw new Error("Database not available");
// Récupérer la séquence par token
const sequence = await db.select().from(sequences).where(eq(sequences.qrCodeToken, input.token)).limit(1);
if (sequence.length === 0) {
throw new Error("QR code invalide");
}
// Récupérer les dates de la séquence
const dates = await getDatesBySequence(sequence[0].id);
return {
id: sequence[0].id,
nom: sequence[0].nom,
lieu: sequence[0].lieu,
dates,
};
}),
// Valider la présence d'un formateur (QR code dédié)
validerFormateur: publicProcedure
.input(z.object({
token: z.string(),
email: z.string().email(),
periode: z.enum(["matin", "apres_midi"]),
signatureDataUrl: z.string(),
}))
.mutation(async ({ input }) => {
const { getDb } = await import("./db");
const { formateurs, sequences, datesFormation } = await import("../drizzle/schema");
const { eq } = await import("drizzle-orm");
const { validerPresenceFormateur } = await import("./presenceDb");
const { storagePut } = await import("./storage");
const crypto = await import("crypto");
const path = await import("path");
const fs = await import("fs/promises");
const db = await getDb();
if (!db) throw new Error("Database not available");
// Vérifier que l'email correspond à un formateur
const formateurData = await db.select().from(formateurs).where(eq(formateurs.email, input.email)).limit(1);
if (formateurData.length === 0) {
throw new Error("Aucun formateur trouvé avec cet email");
}
// Vérifier le token et récupérer la séquence
const sequence = await db.select().from(sequences).where(eq(sequences.qrCodeToken, input.token)).limit(1);
if (sequence.length === 0) {
throw new Error("QR code invalide");
}
// Récupérer la première date de la séquence
const dates = await db.select().from(datesFormation).where(eq(datesFormation.sequenceId, sequence[0].id)).orderBy(datesFormation.dateDebut).limit(1);
if (dates.length === 0) {
throw new Error("Aucune date de formation trouvée pour cette séquence");
}
// Stocker la signature localement
const formationId = sequence[0].formationId;
const sequenceId = sequence[0].id;
const uploadsDir = path.join(process.cwd(), "uploads", "signatures", `${formationId}-${sequenceId}`);
await fs.mkdir(uploadsDir, { recursive: true });
const randomSuffix = crypto.randomBytes(8).toString("hex");
const fileName = `signature-formateur-${formateurData[0].id}-${dates[0].id}-${randomSuffix}.png`;
const filePath = path.join(uploadsDir, fileName);
const base64Data = input.signatureDataUrl.split(",")[1];
const buffer = Buffer.from(base64Data, "base64");
await fs.writeFile(filePath, buffer);
const signatureUrl = `/uploads/signatures/${formationId}-${sequenceId}/${fileName}`;
const signatureS3Key = `signatures/${formationId}-${sequenceId}/${fileName}`;
// Valider la présence formateur
await validerPresenceFormateur({
formateurId: formateurData[0].id,
sequenceId: sequence[0].id,
dateFormationId: dates[0].id,
periode: input.periode,
modeValidation: "qrcode",
signatureUrl,
signatureS3Key,
});
return { success: true };
}),
// Valider une présence (scan QR code ou manuel)
valider: publicProcedure
.input(z.object({
token: z.string().optional(),
email: z.string().email(),
inscriptionId: z.number().optional(),
dateFormationId: z.number().optional(),
dateId: z.number().optional(), // Alias pour dateFormationId
inscriptionId: z.number(),
dateFormationId: z.number(),
periode: z.enum(["matin", "apres_midi"]),
modeValidation: z.enum(["qrcode", "manuel"]).optional().default("qrcode"),
modeValidation: z.enum(["qrcode", "manuel"]),
validateurId: z.number().optional(),
commentaire: z.string().optional(),
signatureDataUrl: z.string().optional(),
}))
.mutation(async ({ input }) => {
const { validerPresence, validerPresenceFormateur } = await import("./presenceDb");
const { validerPresence } = await import("./presenceDb");
const { getDb } = await import("./db");
const { sequences, inscriptions, formateurs } = await import("../drizzle/schema");
const { sequences, inscriptions } = await import("../drizzle/schema");
const { eq } = await import("drizzle-orm");
const db = await getDb();
if (!db) throw new Error("Database not available");
// Détecter si l'utilisateur est un formateur
const formateurData = await db.select().from(formateurs).where(eq(formateurs.email, input.email)).limit(1);
const isFormateur = formateurData.length > 0;
// Si mode QR code, vérifier le token
let sequenceId: number | undefined;
let autoDateFormationId: number | undefined;
if (input.modeValidation === "qrcode" && input.token) {
// Vérifier que le token correspond à une séquence
const { datesFormation } = await import("../drizzle/schema");
const sequence = await db.select().from(sequences).where(eq(sequences.qrCodeToken, input.token)).limit(1);
if (sequence.length === 0) {
// Récupérer l'inscription pour vérifier la séquence
const inscription = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
if (inscription.length === 0) {
throw new Error("Inscription introuvable");
}
// Vérifier que le token correspond à la séquence
const sequence = await db.select().from(sequences).where(eq(sequences.id, inscription[0].sequenceId)).limit(1);
if (sequence.length === 0 || sequence[0].qrCodeToken !== input.token) {
throw new Error("QR code invalide");
}
sequenceId = sequence[0].id;
// Si formateur, récupérer automatiquement la première date de la séquence
if (isFormateur && !input.dateFormationId && !input.dateId) {
const dates = await db.select().from(datesFormation).where(eq(datesFormation.sequenceId, sequenceId)).orderBy(datesFormation.dateDebut).limit(1);
if (dates.length > 0) {
autoDateFormationId = dates[0].id;
}
}
// Si apprenant, vérifier l'inscription
if (!isFormateur && input.inscriptionId) {
const inscription = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
if (inscription.length === 0) {
throw new Error("Inscription introuvable");
}
if (inscription[0].sequenceId !== sequenceId) {
throw new Error("QR code invalide pour cette inscription");
}
}
}
// Stocker la signature localement si fournie
let signatureUrl: string | undefined;
let signatureS3Key: string | undefined;
let finalSequenceId = sequenceId;
if (input.signatureDataUrl) {
const fs = await import("fs");
const path = await import("path");
const crypto = await import("crypto");
// Déterminer la séquence
if (!finalSequenceId) {
if (input.inscriptionId) {
const inscriptionData = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
if (inscriptionData.length === 0) throw new Error("Inscription introuvable");
finalSequenceId = inscriptionData[0].sequenceId;
} else {
throw new Error("Impossible de déterminer la séquence");
}
}
// Récupérer l'inscription pour obtenir la séquence
const inscriptionData = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
if (inscriptionData.length === 0) throw new Error("Inscription introuvable");
const sequenceData = await db.select().from(sequences).where(eq(sequences.id, finalSequenceId)).limit(1);
const sequenceData = await db.select().from(sequences).where(eq(sequences.id, inscriptionData[0].sequenceId)).limit(1);
if (sequenceData.length === 0) throw new Error("Séquence introuvable");
const formationId = sequenceData[0].formationId;
const sequenceId = sequenceData[0].id;
// Créer la structure de dossiers
const uploadsDir = path.resolve(process.cwd(), "uploads", "signatures", `${formationId}-${finalSequenceId}`);
const uploadsDir = path.resolve(process.cwd(), "uploads", "signatures", `${formationId}-${sequenceId}`);
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// Générer un nom de fichier unique
const randomSuffix = crypto.randomBytes(8).toString("hex");
const userType = isFormateur ? "formateur" : "apprenant";
const userId = isFormateur ? formateurData[0].id : input.inscriptionId;
const dateRef = input.dateFormationId || input.dateId || "no-date";
const fileName = `signature-${userType}-${userId}-${dateRef}-${randomSuffix}.png`;
const fileName = `signature-${input.inscriptionId}-${input.dateFormationId}-${randomSuffix}.png`;
const filePath = path.join(uploadsDir, fileName);
// Convertir le data URL en buffer et sauvegarder
@@ -2758,77 +2581,57 @@ export const appRouter = router({
fs.writeFileSync(filePath, buffer);
// Générer l'URL publique
signatureUrl = `/uploads/signatures/${formationId}-${finalSequenceId}/${fileName}`;
signatureS3Key = `signatures/${formationId}-${finalSequenceId}/${fileName}`;
signatureUrl = `/uploads/signatures/${formationId}-${sequenceId}/${fileName}`;
signatureS3Key = `signatures/${formationId}-${sequenceId}/${fileName}`;
}
let result;
if (isFormateur) {
// Valider la présence du formateur
if (!finalSequenceId) throw new Error("Impossible de déterminer la séquence");
result = await validerPresenceFormateur({
formateurId: formateurData[0].id,
sequenceId: finalSequenceId,
dateFormationId: input.dateFormationId || input.dateId || undefined,
periode: input.periode,
modeValidation: input.modeValidation || "qrcode",
commentaire: input.commentaire,
signatureUrl,
signatureS3Key,
});
} else {
// Valider la présence de l'apprenant
if (!input.inscriptionId) throw new Error("Inscription requise pour un apprenant");
const dateFormId = input.dateFormationId || input.dateId;
if (!dateFormId) throw new Error("Date de formation requise pour un apprenant");
result = await validerPresence({
inscriptionId: input.inscriptionId,
dateFormationId: dateFormId,
periode: input.periode,
modeValidation: input.modeValidation || "qrcode",
validateurId: input.validateurId,
commentaire: input.commentaire,
signatureUrl,
signatureS3Key,
});
const result = await validerPresence({
inscriptionId: input.inscriptionId,
dateFormationId: input.dateFormationId,
periode: input.periode,
modeValidation: input.modeValidation,
validateurId: input.validateurId,
commentaire: input.commentaire,
signatureUrl,
signatureS3Key,
});
// Vérifier si toutes les présences sont validées
const { checkAllPresencesValidated } = await import("./presenceDb");
const allValidated = await checkAllPresencesValidated(input.inscriptionId);
// Vérifier si toutes les présences sont validées
const { checkAllPresencesValidated } = await import("./presenceDb");
const allValidated = await checkAllPresencesValidated(input.inscriptionId);
// Si toutes les présences sont validées, générer l'attestation automatiquement
if (allValidated) {
try {
const { genererAttestationPDF, enregistrerAttestation } = await import("./attestationService");
// Générer le PDF
const { s3Key, pdfUrl } = await genererAttestationPDF(input.inscriptionId);
// Récupérer les données de l'inscription
const db = await getDb();
if (db) {
const inscriptionData = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
if (inscriptionData.length > 0) {
// Enregistrer l'attestation en base
await enregistrerAttestation(
input.inscriptionId,
inscriptionData[0].apprenantId,
inscriptionData[0].sequenceId,
s3Key,
pdfUrl
);
}
// Si toutes les présences sont validées, générer l'attestation automatiquement
if (allValidated) {
try {
const { genererAttestationPDF, enregistrerAttestation } = await import("./attestationService");
// Générer le PDF
const { s3Key, pdfUrl } = await genererAttestationPDF(input.inscriptionId);
// Récupérer les données de l'inscription
const db = await getDb();
if (db) {
const inscriptionData = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
if (inscriptionData.length > 0) {
// Enregistrer l'attestation en base
await enregistrerAttestation(
input.inscriptionId,
inscriptionData[0].apprenantId,
inscriptionData[0].sequenceId,
s3Key,
pdfUrl
);
}
return {
...result,
attestationGenerated: true,
attestationUrl: pdfUrl,
};
} catch (error) {
console.error("Erreur lors de la génération de l'attestation:", error);
// Ne pas bloquer la validation de présence si l'attestation échoue
}
return {
...result,
attestationGenerated: true,
attestationUrl: pdfUrl,
};
} catch (error) {
console.error("Erreur lors de la génération de l'attestation:", error);
// Ne pas bloquer la validation de présence si l'attestation échoue
}
}