Files
formation-manager-itinova/server/routers.ts

440 lines
16 KiB
TypeScript

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,
apprenantFonction: inscriptionApprenant.fonction,
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,
nom: i.apprenant!.nom,
fonction: i.apprenant!.fonction,
}));
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;