Checkpoint: Correction du bug d'émargement formateur bloqué sur la saisie d'email

- Ajout de la détection automatique formateur/apprenant dans EmargementScan.tsx
- Nouvelle procédure tRPC getSequenceByToken pour récupérer les infos de séquence
- Interface adaptée selon le type d'utilisateur (apprenant ou formateur)
- Déploiement réussi sur le VPS de production

Le système détecte maintenant automatiquement si l'email correspond à un apprenant ou un formateur, et affiche l'interface appropriée pour chaque cas.
This commit is contained in:
Manus
2026-02-02 14:18:46 -05:00
parent e8410230a9
commit f9983adada
81 changed files with 3734 additions and 27745 deletions

View File

@@ -1,147 +0,0 @@
/**
* Requêtes analytiques pour le tableau de bord
*/
import { eq, sql, and, gte, lte, desc } from "drizzle-orm";
import { getDb } from "./db";
import { inscriptions, sequences, apprenants, datesFormation, formations } from "../drizzle/schema";
/**
* Récupère les statistiques d'inscriptions par mois
* @param startDate - Date de début de la période
* @param endDate - Date de fin de la période
*/
export async function getInscriptionsByMonth(startDate?: Date, endDate?: Date) {
const db = await getDb();
if (!db) return [];
const conditions = [];
if (startDate) {
conditions.push(gte(inscriptions.dateInscription, startDate));
}
if (endDate) {
conditions.push(lte(inscriptions.dateInscription, endDate));
}
const result = await db
.select({
mois: sql<string>`DATE_FORMAT(${inscriptions.dateInscription}, '%Y-%m')`,
total: sql<number>`COUNT(*)`,
confirmees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 ELSE 0 END)`,
listeAttente: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'liste_attente' THEN 1 ELSE 0 END)`,
annulees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'annulee' THEN 1 ELSE 0 END)`,
})
.from(inscriptions)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.groupBy(sql`DATE_FORMAT(${inscriptions.dateInscription}, '%Y-%m')`)
.orderBy(sql`DATE_FORMAT(${inscriptions.dateInscription}, '%Y-%m')`);
return result;
}
/**
* Récupère le taux de remplissage des séquences par mois
*/
export async function getTauxRemplissageByMonth() {
const db = await getDb();
if (!db) return [];
const result = await db
.select({
mois: sql<string>`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`,
capaciteTotale: sql<number>`SUM(${sequences.capaciteMax})`,
inscritsConfirmes: sql<number>`SUM(
(SELECT COUNT(*)
FROM ${inscriptions}
WHERE ${inscriptions.sequenceId} = ${sequences.id}
AND ${inscriptions.statut} = 'confirmee')
)`,
tauxRemplissage: sql<number>`ROUND(
(SUM(
(SELECT COUNT(*)
FROM ${inscriptions}
WHERE ${inscriptions.sequenceId} = ${sequences.id}
AND ${inscriptions.statut} = 'confirmee')
) / SUM(${sequences.capaciteMax})) * 100,
2
)`,
})
.from(sequences)
.groupBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`)
.orderBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`);
return result;
}
/**
* Récupère les statistiques de participation par établissement
*/
export async function getParticipationByEtablissement() {
const db = await getDb();
if (!db) return [];
const result = await db
.select({
codeEtablissement: apprenants.codeEtablissement,
totalApprenants: sql<number>`COUNT(DISTINCT ${apprenants.id})`,
totalInscriptions: sql<number>`COUNT(${inscriptions.id})`,
inscriptionsConfirmees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 ELSE 0 END)`,
inscriptionsListeAttente: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'liste_attente' THEN 1 ELSE 0 END)`,
inscriptionsAnnulees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'annulee' THEN 1 ELSE 0 END)`,
})
.from(apprenants)
.leftJoin(inscriptions, eq(apprenants.id, inscriptions.apprenantId))
.groupBy(apprenants.codeEtablissement)
.orderBy(desc(sql<number>`COUNT(${inscriptions.id})`));
return result;
}
/**
* Récupère les statistiques globales du tableau de bord
*/
export async function getGlobalStats() {
const db = await getDb();
if (!db) return null;
const [stats] = await db
.select({
totalFormations: sql<number>`(SELECT COUNT(*) FROM ${formations} WHERE ${formations.actif} = 1)`,
totalSequences: sql<number>`(SELECT COUNT(*) FROM ${sequences})`,
sequencesOuvertes: sql<number>`(SELECT COUNT(*) FROM ${sequences} WHERE ${sequences.statut} = 'ouverte')`,
totalApprenants: sql<number>`(SELECT COUNT(*) FROM ${apprenants})`,
totalInscriptions: sql<number>`(SELECT COUNT(*) FROM ${inscriptions})`,
inscriptionsConfirmees: sql<number>`(SELECT COUNT(*) FROM ${inscriptions} WHERE ${inscriptions.statut} = 'confirmee')`,
inscriptionsListeAttente: sql<number>`(SELECT COUNT(*) FROM ${inscriptions} WHERE ${inscriptions.statut} = 'liste_attente')`,
tauxRemplissageMoyen: sql<number>`ROUND(
(SELECT COUNT(*) FROM ${inscriptions} WHERE ${inscriptions.statut} = 'confirmee') /
(SELECT SUM(${sequences.capaciteMax}) FROM ${sequences}) * 100,
2
)`,
})
.from(sql`dual`);
return stats;
}
/**
* Récupère les statistiques de participation par fonction
*/
export async function getParticipationByFonction() {
const db = await getDb();
if (!db) return [];
const result = await db
.select({
fonction: apprenants.fonction,
totalApprenants: sql<number>`COUNT(DISTINCT ${apprenants.id})`,
totalInscriptions: sql<number>`COUNT(${inscriptions.id})`,
inscriptionsConfirmees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 ELSE 0 END)`,
})
.from(apprenants)
.leftJoin(inscriptions, eq(apprenants.id, inscriptions.apprenantId))
.groupBy(apprenants.fonction)
.orderBy(desc(sql<number>`COUNT(${inscriptions.id})`));
return result;
}