Rollback to eebcb648
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user