Modifications principales : - Ajout des champs typeUtilisateur et formateurId dans la table presences pour distinguer formateurs et apprenants - Détection automatique du rôle (formateur/apprenant) via l'email lors du scan du QR code - Interface de signature tactile réutilisée pour les formateurs - Intégration des signatures tactiles formateur dans la génération PDF de la feuille de présence - QR code dans l'espace formateur accessible pour l'émargement - Tests unitaires validés avec succès Le système permet maintenant aux formateurs d'émarger (matin et après-midi) via le même QR code que les apprenants, avec détection automatique du rôle et intégration des signatures dans le PDF.
288 lines
8.8 KiB
TypeScript
288 lines
8.8 KiB
TypeScript
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;
|
|
}
|