Checkpoint: Espace Formateur - Fonctionnalités complètes
✅ **Nouvelles fonctionnalités** : **Base de données** : - Nouvelle table `supportsFormation` pour stocker les documents uploadés par les formateurs - Champs : sequenceId, formateurId, nomFichier, typeFichier, tailleFichier, urlFichier, s3Key, description - Migration effectuée avec succès **Backend (server/formateurDb.ts)** : - `getCalendrierFormateur()` : Récupère les interventions à venir avec dates, lieux et nombre d'inscrits - `getApprenantsSequence()` : Liste des apprenants inscrits à une séquence - `getSupportsSequence()` : Liste des supports de formation d'une séquence - `ajouterSupport()` : Upload d'un nouveau support - `supprimerSupport()` : Suppression d'un support avec vérification de propriété - `validerPresence()` : Validation de la présence d'un apprenant - `getHistoriqueFormateur()` : Historique des formations passées avec statistiques - `getDetailSequence()` : Détails complets d'une séquence avec dates **API tRPC (server/routers.ts)** : - `formateur.calendrier` : Calendrier des interventions avec filtres de dates - `formateur.apprenants` : Liste des apprenants d'une séquence - `formateur.supports` : Gestion des supports (liste, ajout, suppression) - `formateur.validerPresence` : Validation des présences en ligne - `formateur.historique` : Historique complet des formations - `formateur.detailSequence` : Détails d'une séquence **Pages frontend** : - `/formateur` : **FormateurDashboard** - Calendrier des interventions à venir groupées par date - `/formateur/sequence/:id` : **FormateurSequence** - Détails séquence avec onglets Apprenants et Supports - `/formateur/historique` : **FormateurHistorique** - Historique des formations avec taux de présence **Fonctionnalités implémentées** : - 📅 Calendrier des interventions avec dates, heures, lieux et nombre d'inscrits - 👥 Liste des apprenants avec informations complètes (nom, email, fonction, établissement) - ✅ Validation des présences en ligne avec checkboxes interactives - 📎 Gestion des supports de formation (liste, téléchargement) - 📊 Historique des formations avec statistiques (inscrits, présents, taux) - 🎨 Interface moderne avec cartes, tableaux et onglets **Notes** : - L'ID du formateur est actuellement fixé à 1 (TODO: lier à l'utilisateur connecté) - La fonctionnalité d'upload de supports est préparée mais nécessite l'implémentation du composant d'upload - Les présences sont gérées via le statut de l'inscription (confirmée/annulée)
This commit is contained in:
269
server/formateurDb.ts
Normal file
269
server/formateurDb.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { eq, sql, and, gte, lte, desc } from "drizzle-orm";
|
||||
import { getDb } from "./db";
|
||||
import {
|
||||
sequences,
|
||||
formations,
|
||||
formateurs,
|
||||
datesFormation,
|
||||
inscriptions,
|
||||
apprenants,
|
||||
supportsFormation
|
||||
} from "../drizzle/schema";
|
||||
|
||||
/**
|
||||
* Récupère le calendrier des interventions d'un formateur
|
||||
* @param formateurId - ID du formateur
|
||||
* @param dateDebut - Date de début optionnelle
|
||||
* @param dateFin - Date de fin optionnelle
|
||||
*/
|
||||
export async function getCalendrierFormateur(formateurId: number, dateDebut?: Date, dateFin?: Date) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const conditions = [eq(sequences.formateurId, formateurId)];
|
||||
|
||||
if (dateDebut) {
|
||||
conditions.push(gte(datesFormation.dateDebut, dateDebut));
|
||||
}
|
||||
if (dateFin) {
|
||||
conditions.push(lte(datesFormation.dateFin, dateFin));
|
||||
}
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
sequenceId: sequences.id,
|
||||
sequenceNom: sequences.nom,
|
||||
formationId: formations.id,
|
||||
formationNom: formations.nom,
|
||||
dateId: datesFormation.id,
|
||||
dateDebut: datesFormation.dateDebut,
|
||||
dateFin: datesFormation.dateFin,
|
||||
ordre: datesFormation.ordre,
|
||||
lieu: sequences.lieu,
|
||||
nbInscrits: sql<number>`(
|
||||
SELECT COUNT(*)
|
||||
FROM inscriptions
|
||||
WHERE inscriptions.sequenceId = ${sequences.id}
|
||||
AND inscriptions.statut IN ('confirmee', 'liste_attente')
|
||||
)`,
|
||||
})
|
||||
.from(sequences)
|
||||
.innerJoin(formations, eq(sequences.formationId, formations.id))
|
||||
.innerJoin(datesFormation, eq(datesFormation.sequenceId, sequences.id))
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(datesFormation.dateDebut);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la liste des apprenants inscrits à une séquence
|
||||
* @param sequenceId - ID de la séquence
|
||||
*/
|
||||
export async function getApprenantsSequence(sequenceId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
inscriptionId: inscriptions.id,
|
||||
apprenantId: apprenants.id,
|
||||
nom: apprenants.nom,
|
||||
prenom: apprenants.prenom,
|
||||
email: apprenants.email,
|
||||
fonction: apprenants.fonction,
|
||||
codeEtablissement: apprenants.codeEtablissement,
|
||||
statut: inscriptions.statut,
|
||||
dateInscription: inscriptions.dateInscription,
|
||||
// presenceValidee n'existe pas dans le schéma actuel
|
||||
})
|
||||
.from(inscriptions)
|
||||
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
|
||||
.where(eq(inscriptions.sequenceId, sequenceId))
|
||||
.orderBy(apprenants.nom, apprenants.prenom);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les supports de formation d'une séquence
|
||||
* @param sequenceId - ID de la séquence
|
||||
*/
|
||||
export async function getSupportsSequence(sequenceId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
id: supportsFormation.id,
|
||||
nomFichier: supportsFormation.nomFichier,
|
||||
typeFichier: supportsFormation.typeFichier,
|
||||
tailleFichier: supportsFormation.tailleFichier,
|
||||
urlFichier: supportsFormation.urlFichier,
|
||||
description: supportsFormation.description,
|
||||
formateurNom: formateurs.nom,
|
||||
createdAt: supportsFormation.createdAt,
|
||||
})
|
||||
.from(supportsFormation)
|
||||
.innerJoin(formateurs, eq(supportsFormation.formateurId, formateurs.id))
|
||||
.where(eq(supportsFormation.sequenceId, sequenceId))
|
||||
.orderBy(desc(supportsFormation.createdAt));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute un support de formation
|
||||
* @param support - Données du support à ajouter
|
||||
*/
|
||||
export async function ajouterSupport(support: {
|
||||
sequenceId: number;
|
||||
formateurId: number;
|
||||
nomFichier: string;
|
||||
typeFichier: string;
|
||||
tailleFichier: number;
|
||||
urlFichier: string;
|
||||
s3Key: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
const [result] = await db.insert(supportsFormation).values(support);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un support de formation
|
||||
* @param supportId - ID du support à supprimer
|
||||
* @param formateurId - ID du formateur (pour vérification)
|
||||
*/
|
||||
export async function supprimerSupport(supportId: number, formateurId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
// Vérifier que le support appartient bien au formateur
|
||||
const [support] = await db
|
||||
.select()
|
||||
.from(supportsFormation)
|
||||
.where(
|
||||
and(
|
||||
eq(supportsFormation.id, supportId),
|
||||
eq(supportsFormation.formateurId, formateurId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!support) {
|
||||
throw new Error("Support non trouvé ou non autorisé");
|
||||
}
|
||||
|
||||
await db.delete(supportsFormation).where(eq(supportsFormation.id, supportId));
|
||||
|
||||
return support; // Retourner le support pour récupérer la clé S3
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide la présence d'un apprenant
|
||||
* @param inscriptionId - ID de l'inscription
|
||||
* @param present - true si présent, false sinon
|
||||
*/
|
||||
export async function validerPresence(inscriptionId: number, present: boolean) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
await db
|
||||
.update(inscriptions)
|
||||
.set({ statut: present ? 'confirmee' : 'annulee' })
|
||||
.where(eq(inscriptions.id, inscriptionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère l'historique des formations d'un formateur
|
||||
* @param formateurId - ID du formateur
|
||||
* @param limit - Nombre de résultats à retourner (par défaut 50)
|
||||
*/
|
||||
export async function getHistoriqueFormateur(formateurId: number, limit: number = 50) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
sequenceId: sequences.id,
|
||||
sequenceNom: sequences.nom,
|
||||
formationNom: formations.nom,
|
||||
dateDebut: sql<Date>`MIN(${datesFormation.dateDebut})`,
|
||||
dateFin: sql<Date>`MAX(${datesFormation.dateFin})`,
|
||||
lieu: sequences.lieu,
|
||||
nbInscrits: sql<number>`(
|
||||
SELECT COUNT(*)
|
||||
FROM inscriptions
|
||||
WHERE inscriptions.sequenceId = ${sequences.id}
|
||||
AND inscriptions.statut IN ('confirmee', 'liste_attente')
|
||||
)`,
|
||||
nbPresents: sql<number>`(
|
||||
SELECT COUNT(*)
|
||||
FROM inscriptions
|
||||
WHERE inscriptions.sequenceId = ${sequences.id}
|
||||
AND inscriptions.statut = 'confirmee'
|
||||
)`,
|
||||
})
|
||||
.from(sequences)
|
||||
.innerJoin(formations, eq(sequences.formationId, formations.id))
|
||||
.innerJoin(datesFormation, eq(datesFormation.sequenceId, sequences.id))
|
||||
.where(eq(sequences.formateurId, formateurId))
|
||||
.groupBy(sequences.id, sequences.nom, formations.nom, sequences.lieu)
|
||||
.orderBy(desc(sql`MIN(${datesFormation.dateDebut})`))
|
||||
.limit(limit);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les détails d'une séquence pour un formateur
|
||||
* @param sequenceId - ID de la séquence
|
||||
* @param formateurId - ID du formateur (pour vérification)
|
||||
*/
|
||||
export async function getDetailSequence(sequenceId: number, formateurId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
|
||||
const [result] = await db
|
||||
.select({
|
||||
id: sequences.id,
|
||||
nom: sequences.nom,
|
||||
formationNom: formations.nom,
|
||||
lieu: sequences.lieu,
|
||||
capaciteMax: sequences.capaciteMax,
|
||||
formateurNom: formateurs.nom,
|
||||
})
|
||||
.from(sequences)
|
||||
.innerJoin(formations, eq(sequences.formationId, formations.id))
|
||||
.innerJoin(formateurs, eq(sequences.formateurId, formateurs.id))
|
||||
.where(
|
||||
and(
|
||||
eq(sequences.id, sequenceId),
|
||||
eq(sequences.formateurId, formateurId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
// Récupérer les dates de formation
|
||||
const dates = await db
|
||||
.select({
|
||||
id: datesFormation.id,
|
||||
dateDebut: datesFormation.dateDebut,
|
||||
dateFin: datesFormation.dateFin,
|
||||
ordre: datesFormation.ordre,
|
||||
})
|
||||
.from(datesFormation)
|
||||
.where(eq(datesFormation.sequenceId, sequenceId))
|
||||
.orderBy(datesFormation.dateDebut);
|
||||
|
||||
return {
|
||||
...result,
|
||||
dates,
|
||||
};
|
||||
}
|
||||
@@ -1266,6 +1266,96 @@ export const appRouter = router({
|
||||
}),
|
||||
}),
|
||||
|
||||
// ===== ESPACE FORMATEUR =====
|
||||
formateur: router({
|
||||
calendrier: protectedProcedure
|
||||
.input(z.object({
|
||||
formateurId: z.number(),
|
||||
dateDebut: z.date().optional(),
|
||||
dateFin: z.date().optional(),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const formateurDb = await import("./formateurDb");
|
||||
return formateurDb.getCalendrierFormateur(input.formateurId, input.dateDebut, input.dateFin);
|
||||
}),
|
||||
|
||||
apprenants: protectedProcedure
|
||||
.input(z.object({ sequenceId: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const formateurDb = await import("./formateurDb");
|
||||
return formateurDb.getApprenantsSequence(input.sequenceId);
|
||||
}),
|
||||
|
||||
supports: protectedProcedure
|
||||
.input(z.object({ sequenceId: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const formateurDb = await import("./formateurDb");
|
||||
return formateurDb.getSupportsSequence(input.sequenceId);
|
||||
}),
|
||||
|
||||
ajouterSupport: protectedProcedure
|
||||
.input(z.object({
|
||||
sequenceId: z.number(),
|
||||
formateurId: z.number(),
|
||||
nomFichier: z.string(),
|
||||
typeFichier: z.string(),
|
||||
tailleFichier: z.number(),
|
||||
urlFichier: z.string(),
|
||||
s3Key: z.string(),
|
||||
description: z.string().optional(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const formateurDb = await import("./formateurDb");
|
||||
return formateurDb.ajouterSupport(input);
|
||||
}),
|
||||
|
||||
supprimerSupport: protectedProcedure
|
||||
.input(z.object({
|
||||
supportId: z.number(),
|
||||
formateurId: z.number(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const formateurDb = await import("./formateurDb");
|
||||
const support = await formateurDb.supprimerSupport(input.supportId, input.formateurId);
|
||||
|
||||
// Note: La suppression du fichier S3 devrait être gérée par un processus de nettoyage séparé
|
||||
// car il n'y a pas de fonction storageDelete dans l'API actuelle
|
||||
|
||||
return { success: true, support };
|
||||
}),
|
||||
|
||||
validerPresence: protectedProcedure
|
||||
.input(z.object({
|
||||
inscriptionId: z.number(),
|
||||
present: z.boolean(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const formateurDb = await import("./formateurDb");
|
||||
await formateurDb.validerPresence(input.inscriptionId, input.present);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
historique: protectedProcedure
|
||||
.input(z.object({
|
||||
formateurId: z.number(),
|
||||
limit: z.number().optional(),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const formateurDb = await import("./formateurDb");
|
||||
return formateurDb.getHistoriqueFormateur(input.formateurId, input.limit);
|
||||
}),
|
||||
|
||||
detailSequence: protectedProcedure
|
||||
.input(z.object({
|
||||
sequenceId: z.number(),
|
||||
formateurId: z.number(),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const formateurDb = await import("./formateurDb");
|
||||
return formateurDb.getDetailSequence(input.sequenceId, input.formateurId);
|
||||
}),
|
||||
}),
|
||||
|
||||
// ===== ÉTABLISSEMENTS =====
|
||||
etablissements: router({
|
||||
list: adminProcedure.query(async () => {
|
||||
|
||||
Reference in New Issue
Block a user