import { COOKIE_NAME } from "@shared/const"; import { getSessionCookieOptions } from "./_core/cookies"; import { systemRouter } from "./_core/systemRouter"; import { publicProcedure, protectedProcedure, router } from "./_core/trpc"; import { z } from "zod"; import * as db from "./db"; import { TRPCError } from "@trpc/server"; import { sendInscriptionConfirmation, sendGroupEmail } from "./emailService"; import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService"; // Procédure admin uniquement const adminProcedure = protectedProcedure.use(({ ctx, next }) => { if (ctx.user.role !== 'admin') { throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux administrateurs' }); } return next({ ctx }); }); export const appRouter = router({ system: systemRouter, auth: router({ me: publicProcedure.query(opts => opts.ctx.user), logout: publicProcedure.mutation(({ ctx }) => { const cookieOptions = getSessionCookieOptions(ctx.req); ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 }); return { success: true, } as const; }), }), // ===== FORMATIONS ===== formations: router({ list: adminProcedure.query(async () => { return db.getAllFormations(); }), getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { return db.getFormationById(input.id); }), getByLien: publicProcedure.input(z.object({ lien: z.string() })).query(async ({ input }) => { return db.getFormationByLien(input.lien); }), create: adminProcedure.input(z.object({ nom: z.string().min(1), description: z.string().optional(), lienUnique: z.string().min(1), actif: z.boolean().optional(), })).mutation(async ({ input }) => { await db.createFormation(input); return { success: true }; }), update: adminProcedure.input(z.object({ id: z.number(), nom: z.string().min(1).optional(), description: z.string().optional(), lienUnique: z.string().min(1).optional(), actif: z.boolean().optional(), })).mutation(async ({ input }) => { const { id, ...data } = input; await db.updateFormation(id, data); return { success: true }; }), delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { await db.deleteFormation(input.id); return { success: true }; }), }), // ===== APPRENANTS ===== apprenants: router({ list: adminProcedure.query(async () => { return db.getAllApprenants(); }), getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { return db.getApprenantById(input.id); }), getByEmail: publicProcedure.input(z.object({ email: z.string() })).query(async ({ input }) => { return db.getApprenantByEmail(input.email); }), create: adminProcedure.input(z.object({ nom: z.string().min(1), prenom: z.string().min(1), email: z.string().email(), codeEtablissement: z.string().min(1), fonction: z.enum(["directeur", "chef_service", "autre"]), })).mutation(async ({ input }) => { await db.createApprenant(input); return { success: true }; }), update: adminProcedure.input(z.object({ id: z.number(), nom: z.string().min(1).optional(), prenom: z.string().min(1).optional(), email: z.string().email().optional(), codeEtablissement: z.string().min(1).optional(), fonction: z.enum(["directeur", "chef_service", "autre"]).optional(), })).mutation(async ({ input }) => { const { id, ...data } = input; await db.updateApprenant(id, data); return { success: true }; }), delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { await db.deleteApprenant(input.id); return { success: true }; }), }), // ===== SESSIONS ===== sessions: router({ list: adminProcedure.query(async () => { return db.getAllSessions(); }), listByFormation: publicProcedure.input(z.object({ formationId: z.number() })).query(async ({ input }) => { return db.getSessionsByFormation(input.formationId); }), listAvecInscriptions: adminProcedure.input(z.object({ formationId: z.number() })).query(async ({ input }) => { return db.getSessionsAvecInscriptions(input.formationId); }), getById: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { return db.getSessionById(input.id); }), create: adminProcedure.input(z.object({ formationId: z.number(), nom: z.string().min(1), dateDebut: z.string(), dateFin: z.string(), lieu: z.string().min(1), capaciteMax: z.number().default(12), dateBlocage: z.string(), statut: z.enum(["ouverte", "bloquee", "terminee"]).optional(), })).mutation(async ({ input }) => { await db.createSession({ ...input, dateDebut: new Date(input.dateDebut), dateFin: new Date(input.dateFin), dateBlocage: new Date(input.dateBlocage), }); return { success: true }; }), update: adminProcedure.input(z.object({ id: z.number(), formationId: z.number().optional(), nom: z.string().min(1).optional(), dateDebut: z.string().optional(), dateFin: z.string().optional(), lieu: z.string().min(1).optional(), capaciteMax: z.number().optional(), dateBlocage: z.string().optional(), statut: z.enum(["ouverte", "bloquee", "terminee"]).optional(), })).mutation(async ({ input }) => { const { id, ...data } = input; const updateData: any = { ...data }; if (data.dateDebut) updateData.dateDebut = new Date(data.dateDebut); if (data.dateFin) updateData.dateFin = new Date(data.dateFin); if (data.dateBlocage) updateData.dateBlocage = new Date(data.dateBlocage); await db.updateSession(id, updateData); return { success: true }; }), delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { await db.deleteSession(input.id); return { success: true }; }), }), // ===== INSCRIPTIONS ===== inscriptions: router({ listBySession: adminProcedure.input(z.object({ sessionId: z.number() })).query(async ({ input }) => { return db.getInscriptionsBySession(input.sessionId); }), listByApprenant: publicProcedure.input(z.object({ apprenantId: z.number() })).query(async ({ input }) => { return db.getInscriptionsByApprenant(input.apprenantId); }), checkExisting: publicProcedure.input(z.object({ apprenantId: z.number(), sessionId: z.number(), })).query(async ({ input }) => { return db.getInscriptionByApprenantAndSession(input.apprenantId, input.sessionId); }), inscrire: publicProcedure.input(z.object({ apprenantId: z.number(), sessionId: z.number(), })).mutation(async ({ input }) => { // Vérifier si la session existe const session = await db.getSessionById(input.sessionId); if (!session) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Session introuvable' }); } // Vérifier si la session est bloquée const now = new Date(); if (session.statut === 'bloquee' || now >= new Date(session.dateBlocage)) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Les inscriptions sont fermées pour cette session (J-15 dépassé)' }); } // Vérifier si l'apprenant existe déjà une inscription const existing = await db.getInscriptionByApprenantAndSession(input.apprenantId, input.sessionId); if (existing) { throw new TRPCError({ code: 'CONFLICT', message: 'Vous êtes déjà inscrit à cette session' }); } // Vérifier la capacité const nbInscrits = await db.countInscriptionsConfirmees(input.sessionId); const statut = nbInscrits >= session.capaciteMax ? 'liste_attente' : 'confirmee'; await db.createInscription({ apprenantId: input.apprenantId, sessionId: input.sessionId, statut, }); // Envoyer l'email de confirmation avec invitation Outlook const inscriptionSession = await db.getSessionById(input.sessionId); const inscriptionApprenant = await db.getApprenantById(input.apprenantId); const inscriptionFormation = inscriptionSession ? await db.getFormationById(inscriptionSession.formationId) : null; if (inscriptionSession && inscriptionApprenant && inscriptionFormation) { await sendInscriptionConfirmation({ apprenantEmail: inscriptionApprenant.email, apprenantNom: inscriptionApprenant.nom, apprenantPrenom: inscriptionApprenant.prenom, formationNom: inscriptionFormation.nom, sessionNom: inscriptionSession.nom, dateDebut: new Date(inscriptionSession.dateDebut), dateFin: new Date(inscriptionSession.dateFin), lieu: inscriptionSession.lieu, statut, }); } return { success: true, statut }; }), desinscrire: publicProcedure.input(z.object({ apprenantId: z.number(), sessionId: z.number(), })).mutation(async ({ input }) => { // Vérifier si la session existe const session = await db.getSessionById(input.sessionId); if (!session) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Session introuvable' }); } // Vérifier si la session est bloquée const now = new Date(); if (session.statut === 'bloquee' || now >= new Date(session.dateBlocage)) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Les désinscriptions sont fermées pour cette session (J-15 dépassé)' }); } // Trouver l'inscription const inscription = await db.getInscriptionByApprenantAndSession(input.apprenantId, input.sessionId); if (!inscription) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Inscription introuvable' }); } await db.updateInscription(inscription.id, { statut: 'annulee' }); return { success: true }; }), updateStatut: adminProcedure.input(z.object({ id: z.number(), statut: z.enum(['confirmee', 'liste_attente', 'annulee']), })).mutation(async ({ input }) => { await db.updateInscription(input.id, { statut: input.statut }); return { success: true }; }), sendGroupEmails: adminProcedure.input(z.object({ sessionId: z.number(), type: z.enum(['teaser', 'rappel']), })).mutation(async ({ input }) => { const session = await db.getSessionById(input.sessionId); if (!session) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Session introuvable' }); } const formation = await db.getFormationById(session.formationId); if (!formation) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' }); } const inscriptions = await db.getInscriptionsBySession(input.sessionId); const recipients = inscriptions .filter(i => i.inscription.statut === 'confirmee' && i.apprenant) .map(i => ({ email: i.apprenant!.email, prenom: i.apprenant!.prenom, })); const result = await sendGroupEmail({ recipients, formationNom: formation.nom, sessionNom: session.nom, type: input.type, dateDebut: new Date(session.dateDebut), lieu: session.lieu, }); return result; }), exportExcel: adminProcedure.input(z.object({ sessionId: z.number() })).mutation(async ({ input }) => { const session = await db.getSessionById(input.sessionId); if (!session) throw new TRPCError({ code: 'NOT_FOUND', message: 'Session introuvable' }); const formation = await db.getFormationById(session.formationId); if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' }); const inscriptions = await db.getInscriptionsBySession(input.sessionId); const data = inscriptions .filter(i => i.apprenant) .map(i => ({ nom: i.apprenant!.nom, prenom: i.apprenant!.prenom, email: i.apprenant!.email, codeEtablissement: i.apprenant!.codeEtablissement, fonction: i.apprenant!.fonction, statut: i.inscription.statut, dateInscription: new Date(i.inscription.dateInscription), })); const buffer = generateExcelExport( { formationNom: formation.nom, sessionNom: session.nom, dateDebut: new Date(session.dateDebut), dateFin: new Date(session.dateFin), lieu: session.lieu, }, data ); return { data: buffer.toString('base64') }; }), exportPDF: adminProcedure.input(z.object({ sessionId: z.number() })).mutation(async ({ input }) => { const session = await db.getSessionById(input.sessionId); if (!session) throw new TRPCError({ code: 'NOT_FOUND', message: 'Session introuvable' }); const formation = await db.getFormationById(session.formationId); if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' }); const inscriptions = await db.getInscriptionsBySession(input.sessionId); const data = inscriptions .filter(i => i.apprenant) .map(i => ({ nom: i.apprenant!.nom, prenom: i.apprenant!.prenom, email: i.apprenant!.email, codeEtablissement: i.apprenant!.codeEtablissement, fonction: i.apprenant!.fonction, statut: i.inscription.statut, dateInscription: new Date(i.inscription.dateInscription), })); const buffer = generatePDFExport( { formationNom: formation.nom, sessionNom: session.nom, dateDebut: new Date(session.dateDebut), dateFin: new Date(session.dateFin), lieu: session.lieu, }, data ); return { data: buffer.toString('base64') }; }), exportFeuillePresence: adminProcedure.input(z.object({ sessionId: z.number() })).mutation(async ({ input }) => { const session = await db.getSessionById(input.sessionId); if (!session) throw new TRPCError({ code: 'NOT_FOUND', message: 'Session introuvable' }); const formation = await db.getFormationById(session.formationId); if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' }); const inscriptions = await db.getInscriptionsBySession(input.sessionId); const data = inscriptions .filter(i => i.apprenant) .map(i => ({ nom: i.apprenant!.nom, prenom: i.apprenant!.prenom, email: i.apprenant!.email, codeEtablissement: i.apprenant!.codeEtablissement, fonction: i.apprenant!.fonction, statut: i.inscription.statut, dateInscription: new Date(i.inscription.dateInscription), })); const buffer = generateFeuillePresence( { formationNom: formation.nom, sessionNom: session.nom, dateDebut: new Date(session.dateDebut), dateFin: new Date(session.dateFin), lieu: session.lieu, }, data ); return { data: buffer.toString('base64') }; }), }), }); export type AppRouter = typeof appRouter;