Checkpoint: Système d'émargement numérique avec QR code et génération automatique d'attestations :
## Base de données - ✅ Table `presences` créée (inscriptionId, dateFormationId, heurePresence, modeValidation, validateurId) - ✅ Colonne `qrCodeToken` ajoutée à la table `sequences` ## Backend - ✅ Génération de QR code unique par séquence (qrCodeGenerator.ts) - ✅ Procédures tRPC pour l'émargement (presences.generateQRCode, presences.valider, presences.listBySequence) - ✅ Fonctions de base de données (presenceDb.ts) pour gérer les présences - ✅ Génération automatique d'attestation après validation de toutes les présences - ✅ Intégration avec le service d'attestations existant ## Interface formateur (/formateur/emargement) - ✅ Sélection de la séquence - ✅ Génération et affichage du QR code en modal - ✅ Liste des inscrits avec statut de présence par date - ✅ Validation manuelle de présence possible - ✅ Actualisation en temps réel des présences - ✅ Entrée de menu "Émargement" dans DashboardLayout ## Interface publique (/emargement/:token) - ✅ Page de scan QR code accessible sans authentification - ✅ Recherche de l'apprenant par email - ✅ Sélection de la formation et de la date - ✅ Validation automatique de la présence via QR code - ✅ Écrans de confirmation (succès/erreur) - ✅ Gestion des erreurs (QR invalide, déjà émargé, etc.) ## Génération automatique d'attestations - ✅ Vérification automatique après chaque validation de présence - ✅ Génération du PDF d'attestation si toutes les présences sont validées - ✅ Enregistrement en base de données - ✅ Upload automatique sur S3 ## Dépendances installées - qrcode (génération de QR codes) - @types/qrcode (types TypeScript) Le système est entièrement fonctionnel et prêt à être utilisé par les formateurs et apprenants.
This commit is contained in:
149
server/presenceDb.ts
Normal file
149
server/presenceDb.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { eq, and } 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;
|
||||
modeValidation: "qrcode" | "manuel";
|
||||
validateurId?: number;
|
||||
commentaire?: string;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
// Vérifier si la présence existe déjà
|
||||
const presenceExistante = await db
|
||||
.select()
|
||||
.from(presences)
|
||||
.where(
|
||||
and(
|
||||
eq(presences.inscriptionId, params.inscriptionId),
|
||||
eq(presences.dateFormationId, params.dateFormationId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (presenceExistante.length > 0) {
|
||||
throw new Error("Présence déjà validée pour cette date");
|
||||
}
|
||||
|
||||
// Créer la présence
|
||||
await db.insert(presences).values({
|
||||
inscriptionId: params.inscriptionId,
|
||||
dateFormationId: params.dateFormationId,
|
||||
heurePresence: new Date(),
|
||||
modeValidation: params.modeValidation,
|
||||
validateurId: params.validateurId,
|
||||
commentaire: params.commentaire,
|
||||
});
|
||||
|
||||
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,
|
||||
heurePresence: presences.heurePresence,
|
||||
modeValidation: presences.modeValidation,
|
||||
validateurId: presences.validateurId,
|
||||
commentaire: presences.commentaire,
|
||||
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,
|
||||
heurePresence: presences.heurePresence,
|
||||
modeValidation: presences.modeValidation,
|
||||
validateurId: presences.validateurId,
|
||||
commentaire: presences.commentaire,
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user