✅ Base de données : colonne "formateur" ajoutée à la table sequences (VARCHAR 255, nullable) ✅ Backend : procédures tRPC mises à jour - sequences.create : champ formateur ajouté (optionnel) - sequences.update : champ formateur ajouté (optionnel) ✅ Frontend : formulaires mis à jour - Formulaire de création : champ "Formateur" ajouté avec placeholder - Formulaire de modification : champ "Formateur" ajouté avec placeholder - Le champ est non obligatoire comme demandé Le champ formateur permet maintenant de spécifier le nom du formateur pour chaque séquence de formation lors de la création ou de la modification.
720 lines
25 KiB
TypeScript
720 lines
25 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 { parseLocalDateTime } from "./dateUtils";
|
|
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.getFormations();
|
|
}),
|
|
|
|
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.getApprenants();
|
|
}),
|
|
|
|
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 };
|
|
}),
|
|
}),
|
|
|
|
// ===== SÉQUENCES =====
|
|
sequences: router({
|
|
list: adminProcedure.query(async () => {
|
|
const seqs = await db.getSequences();
|
|
// Récupérer les dates et le nombre d'inscrits pour chaque séquence
|
|
const sequencesAvecDates = await Promise.all(
|
|
seqs.map(async (seq) => {
|
|
const dates = await db.getDatesBySequence(seq.id);
|
|
const nbInscrits = await db.countInscriptionsBySequence(seq.id, 'confirmee');
|
|
return { ...seq, dates, nbInscrits };
|
|
})
|
|
);
|
|
return sequencesAvecDates;
|
|
}),
|
|
|
|
listByFormation: publicProcedure.input(z.object({ formationId: z.number() })).query(async ({ input }) => {
|
|
const seqs = await db.getSequencesByFormation(input.formationId);
|
|
// Récupérer les dates et le nombre d'inscrits pour chaque séquence
|
|
const sequencesAvecDates = await Promise.all(
|
|
seqs.map(async (seq) => {
|
|
const dates = await db.getDatesBySequence(seq.id);
|
|
const nbInscrits = await db.countInscriptionsBySequence(seq.id, 'confirmee');
|
|
return { ...seq, dates, nbInscrits };
|
|
})
|
|
);
|
|
return sequencesAvecDates;
|
|
}),
|
|
|
|
getById: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
|
const seq = await db.getSequenceById(input.id);
|
|
if (!seq) return null;
|
|
const dates = await db.getDatesBySequence(input.id);
|
|
return { ...seq, dates };
|
|
}),
|
|
|
|
create: adminProcedure.input(z.object({
|
|
formationId: z.number(),
|
|
nom: z.string().min(1),
|
|
lieu: z.string().min(1),
|
|
publicCible: z.enum(["directeur", "chef_service", "tous", "autre"]),
|
|
formateur: z.string().optional(),
|
|
capaciteMax: z.number().default(12),
|
|
dateBlocage: z.string(),
|
|
statut: z.enum(["ouverte", "bloquee", "terminee"]).optional(),
|
|
dates: z.array(z.object({
|
|
dateDebut: z.string(),
|
|
dateFin: z.string(),
|
|
ordre: z.number(),
|
|
})).min(1).max(4),
|
|
})).mutation(async ({ input }) => {
|
|
const { dates, ...sequenceData } = input;
|
|
|
|
// Créer la séquence
|
|
const result = await db.createSequence({
|
|
...sequenceData,
|
|
dateBlocage: parseLocalDateTime(sequenceData.dateBlocage),
|
|
});
|
|
|
|
// Récupérer l'ID de la séquence créée
|
|
const sequenceId = Number(result[0].insertId);
|
|
|
|
// Créer les dates de formation
|
|
for (const date of dates) {
|
|
await db.createDateFormation({
|
|
sequenceId,
|
|
dateDebut: parseLocalDateTime(date.dateDebut),
|
|
dateFin: parseLocalDateTime(date.dateFin),
|
|
ordre: date.ordre,
|
|
});
|
|
}
|
|
|
|
return { success: true, sequenceId };
|
|
}),
|
|
|
|
update: adminProcedure.input(z.object({
|
|
id: z.number(),
|
|
formationId: z.number(),
|
|
nom: z.string().min(1),
|
|
lieu: z.string().min(1),
|
|
publicCible: z.enum(["directeur", "chef_service", "tous", "autre"]),
|
|
formateur: z.string().optional(),
|
|
capaciteMax: z.number(),
|
|
dateBlocage: z.string(),
|
|
statut: z.enum(["ouverte", "bloquee", "terminee"]),
|
|
dates: z.array(z.object({
|
|
dateDebut: z.string(),
|
|
dateFin: z.string(),
|
|
ordre: z.number(),
|
|
})).min(1).max(4),
|
|
})).mutation(async ({ input }) => {
|
|
const { id, dates, ...sequenceData } = input;
|
|
|
|
console.log('[UPDATE SEQUENCE] Input reçu:', JSON.stringify(input, null, 2));
|
|
|
|
// Mettre à jour la séquence
|
|
const updateData: any = {
|
|
...sequenceData,
|
|
dateBlocage: parseLocalDateTime(sequenceData.dateBlocage),
|
|
};
|
|
|
|
console.log('[UPDATE SEQUENCE] UpdateData:', JSON.stringify(updateData, null, 2));
|
|
await db.updateSequence(id, updateData);
|
|
console.log('[UPDATE SEQUENCE] Séquence mise à jour avec succès');
|
|
|
|
// Supprimer les anciennes dates
|
|
await db.deleteDatesBySequence(id);
|
|
console.log('[UPDATE SEQUENCE] Anciennes dates supprimées');
|
|
|
|
// Créer les nouvelles dates
|
|
for (const date of dates) {
|
|
console.log('[UPDATE SEQUENCE] Création date:', date);
|
|
await db.createDateFormation({
|
|
sequenceId: id,
|
|
dateDebut: parseLocalDateTime(date.dateDebut),
|
|
dateFin: parseLocalDateTime(date.dateFin),
|
|
ordre: date.ordre,
|
|
});
|
|
}
|
|
console.log('[UPDATE SEQUENCE] Toutes les dates ont été créées');
|
|
|
|
return { success: true };
|
|
}),
|
|
|
|
delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
|
// Supprimer d'abord les dates associées
|
|
await db.deleteDatesBySequence(input.id);
|
|
// Puis supprimer la séquence
|
|
await db.deleteSequence(input.id);
|
|
return { success: true };
|
|
}),
|
|
}),
|
|
|
|
// ===== INSCRIPTIONS =====
|
|
inscriptions: router({
|
|
listAll: adminProcedure.query(async () => {
|
|
return db.getAllInscriptions();
|
|
}),
|
|
|
|
listBySequence: adminProcedure.input(z.object({ sequenceId: z.number() })).query(async ({ input }) => {
|
|
return db.getInscriptionsBySequence(input.sequenceId);
|
|
}),
|
|
|
|
listByApprenant: publicProcedure.input(z.object({ apprenantId: z.number() })).query(async ({ input }) => {
|
|
return db.getInscriptionsByApprenant(input.apprenantId);
|
|
}),
|
|
|
|
checkExisting: publicProcedure.input(z.object({
|
|
apprenantId: z.number(),
|
|
sequenceId: z.number(),
|
|
})).query(async ({ input }) => {
|
|
return db.checkExistingInscription(input.apprenantId, input.sequenceId);
|
|
}),
|
|
|
|
inscrire: publicProcedure.input(z.object({
|
|
apprenantId: z.number(),
|
|
sequenceId: z.number(),
|
|
})).mutation(async ({ input }) => {
|
|
// Vérifier si la séquence existe
|
|
const sequence = await db.getSequenceById(input.sequenceId);
|
|
if (!sequence) {
|
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
}
|
|
|
|
// Vérifier si la séquence est bloquée
|
|
const now = new Date();
|
|
if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) {
|
|
throw new TRPCError({
|
|
code: 'FORBIDDEN',
|
|
message: 'Les inscriptions sont fermées pour cette séquence (J-15 dépassé)'
|
|
});
|
|
}
|
|
|
|
// Vérifier si l'apprenant a déjà une inscription
|
|
const existing = await db.checkExistingInscription(input.apprenantId, input.sequenceId);
|
|
if (existing) {
|
|
throw new TRPCError({
|
|
code: 'CONFLICT',
|
|
message: 'Vous êtes déjà inscrit à cette séquence'
|
|
});
|
|
}
|
|
|
|
// Vérifier la capacité
|
|
const nbInscrits = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
|
|
const statut = nbInscrits >= sequence.capaciteMax ? 'liste_attente' : 'confirmee';
|
|
|
|
await db.createInscription({
|
|
apprenantId: input.apprenantId,
|
|
sequenceId: input.sequenceId,
|
|
statut,
|
|
});
|
|
|
|
// Envoyer l'email de confirmation avec invitations Outlook pour toutes les dates
|
|
const inscriptionSequence = await db.getSequenceById(input.sequenceId);
|
|
const inscriptionApprenant = await db.getApprenantById(input.apprenantId);
|
|
const inscriptionFormation = inscriptionSequence ? await db.getFormationById(inscriptionSequence.formationId) : null;
|
|
const dates = inscriptionSequence ? await db.getDatesBySequence(inscriptionSequence.id) : [];
|
|
|
|
if (inscriptionSequence && inscriptionApprenant && inscriptionFormation && dates.length > 0) {
|
|
// Utiliser la première date pour l'email principal
|
|
const premiereDate = dates[0];
|
|
|
|
await sendInscriptionConfirmation({
|
|
apprenantEmail: inscriptionApprenant.email,
|
|
apprenantNom: inscriptionApprenant.nom,
|
|
apprenantPrenom: inscriptionApprenant.prenom,
|
|
apprenantFonction: inscriptionApprenant.fonction,
|
|
formationNom: inscriptionFormation.nom,
|
|
sequenceNom: inscriptionSequence.nom,
|
|
dates: dates.map(d => ({
|
|
dateDebut: new Date(d.dateDebut),
|
|
dateFin: new Date(d.dateFin),
|
|
ordre: d.ordre,
|
|
})),
|
|
lieu: inscriptionSequence.lieu,
|
|
statut,
|
|
});
|
|
}
|
|
|
|
return { success: true, statut };
|
|
}),
|
|
|
|
desinscrire: publicProcedure.input(z.object({
|
|
apprenantId: z.number(),
|
|
sequenceId: z.number(),
|
|
})).mutation(async ({ input }) => {
|
|
// Vérifier si la séquence existe
|
|
const sequence = await db.getSequenceById(input.sequenceId);
|
|
if (!sequence) {
|
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
}
|
|
|
|
// Vérifier si la séquence est bloquée
|
|
const now = new Date();
|
|
if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) {
|
|
throw new TRPCError({
|
|
code: 'FORBIDDEN',
|
|
message: 'Les désinscriptions sont fermées pour cette séquence (J-15 dépassé)'
|
|
});
|
|
}
|
|
|
|
// Trouver l'inscription
|
|
const inscription = await db.checkExistingInscription(input.apprenantId, input.sequenceId);
|
|
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 };
|
|
}),
|
|
|
|
sendGroupEmail: adminProcedure.input(z.object({
|
|
sequenceId: z.number(),
|
|
type: z.enum(['teaser', 'rappel']),
|
|
})).mutation(async ({ input }) => {
|
|
const sequence = await db.getSequenceById(input.sequenceId);
|
|
if (!sequence) {
|
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
}
|
|
|
|
const formation = await db.getFormationById(sequence.formationId);
|
|
if (!formation) {
|
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
|
}
|
|
|
|
const dates = await db.getDatesBySequence(sequence.id);
|
|
if (dates.length === 0) {
|
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Aucune date trouvée pour cette séquence' });
|
|
}
|
|
|
|
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
|
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,
|
|
sequenceNom: sequence.nom,
|
|
type: input.type,
|
|
dates: dates.map(d => ({
|
|
dateDebut: new Date(d.dateDebut),
|
|
dateFin: new Date(d.dateFin),
|
|
ordre: d.ordre,
|
|
})),
|
|
lieu: sequence.lieu,
|
|
});
|
|
|
|
return result;
|
|
}),
|
|
|
|
exportExcel: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => {
|
|
const sequence = await db.getSequenceById(input.sequenceId);
|
|
if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
|
|
const formation = await db.getFormationById(sequence.formationId);
|
|
if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
|
|
|
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
|
const dates = await db.getDatesBySequence(input.sequenceId);
|
|
|
|
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: i.inscription.dateInscription,
|
|
}));
|
|
|
|
const buffer = await generateExcelExport({
|
|
formationNom: formation.nom,
|
|
sequenceNom: sequence.nom,
|
|
dates: dates.map(d => ({
|
|
dateDebut: new Date(d.dateDebut),
|
|
dateFin: new Date(d.dateFin),
|
|
ordre: d.ordre,
|
|
})),
|
|
lieu: sequence.lieu,
|
|
publicCible: sequence.publicCible,
|
|
inscriptions: data,
|
|
});
|
|
|
|
return {
|
|
buffer: buffer.toString('base64'),
|
|
filename: `inscriptions_${sequence.nom.replace(/\s+/g, '_')}.xlsx`,
|
|
};
|
|
}),
|
|
|
|
exportPDF: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => {
|
|
const sequence = await db.getSequenceById(input.sequenceId);
|
|
if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
|
|
const formation = await db.getFormationById(sequence.formationId);
|
|
if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
|
|
|
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
|
const dates = await db.getDatesBySequence(input.sequenceId);
|
|
|
|
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: i.inscription.dateInscription,
|
|
}));
|
|
|
|
const buffer = await generatePDFExport({
|
|
formationNom: formation.nom,
|
|
sequenceNom: sequence.nom,
|
|
dates: dates.map(d => ({
|
|
dateDebut: new Date(d.dateDebut),
|
|
dateFin: new Date(d.dateFin),
|
|
ordre: d.ordre,
|
|
})),
|
|
lieu: sequence.lieu,
|
|
publicCible: sequence.publicCible,
|
|
inscriptions: data,
|
|
});
|
|
|
|
return {
|
|
buffer: buffer.toString('base64'),
|
|
filename: `inscriptions_${sequence.nom.replace(/\s+/g, '_')}.pdf`,
|
|
};
|
|
}),
|
|
|
|
exportFeuillePresence: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => {
|
|
const sequence = await db.getSequenceById(input.sequenceId);
|
|
if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
|
|
const formation = await db.getFormationById(sequence.formationId);
|
|
if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
|
|
|
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
|
const dates = await db.getDatesBySequence(input.sequenceId);
|
|
|
|
const data = inscriptions
|
|
.filter(i => i.inscription.statut === 'confirmee' && i.apprenant)
|
|
.map(i => ({
|
|
nom: i.apprenant!.nom,
|
|
prenom: i.apprenant!.prenom,
|
|
codeEtablissement: i.apprenant!.codeEtablissement,
|
|
fonction: i.apprenant!.fonction,
|
|
}));
|
|
|
|
const buffer = await generateFeuillePresence({
|
|
formationNom: formation.nom,
|
|
sequenceNom: sequence.nom,
|
|
dates: dates.map(d => ({
|
|
dateDebut: new Date(d.dateDebut),
|
|
dateFin: new Date(d.dateFin),
|
|
ordre: d.ordre,
|
|
})),
|
|
lieu: sequence.lieu,
|
|
publicCible: sequence.publicCible,
|
|
apprenants: data,
|
|
});
|
|
|
|
return {
|
|
buffer: buffer.toString('base64'),
|
|
filename: `feuille_presence_${sequence.nom.replace(/\s+/g, '_')}.pdf`,
|
|
};
|
|
}),
|
|
}),
|
|
|
|
// ===== GESTION DES UTILISATEURS =====
|
|
users: router({
|
|
list: adminProcedure.query(async () => {
|
|
return db.getAllUsers();
|
|
}),
|
|
|
|
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
|
return db.getUserById(input.id);
|
|
}),
|
|
|
|
create: adminProcedure.input(z.object({
|
|
openId: z.string().min(1),
|
|
name: z.string().optional(),
|
|
email: z.string().email().optional(),
|
|
role: z.enum(["user", "admin"]).default("user"),
|
|
isActive: z.boolean().default(true),
|
|
})).mutation(async ({ input }) => {
|
|
await db.createUser(input);
|
|
return { success: true };
|
|
}),
|
|
|
|
update: adminProcedure.input(z.object({
|
|
id: z.number(),
|
|
name: z.string().optional(),
|
|
email: z.string().email().optional(),
|
|
role: z.enum(["user", "admin"]).optional(),
|
|
isActive: z.boolean().optional(),
|
|
})).mutation(async ({ input }) => {
|
|
const { id, ...data } = input;
|
|
await db.updateUser(id, data);
|
|
return { success: true };
|
|
}),
|
|
|
|
toggleStatus: adminProcedure.input(z.object({
|
|
id: z.number(),
|
|
isActive: z.boolean(),
|
|
})).mutation(async ({ input }) => {
|
|
await db.toggleUserStatus(input.id, input.isActive);
|
|
return { success: true };
|
|
}),
|
|
|
|
delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
|
await db.deleteUser(input.id);
|
|
return { success: true };
|
|
}),
|
|
|
|
requestPasswordReset: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
|
const user = await db.getUserById(input.id);
|
|
if (!user) {
|
|
throw new Error("Utilisateur introuvable");
|
|
}
|
|
|
|
if (!user.email) {
|
|
throw new Error("Cet utilisateur n'a pas d'adresse email");
|
|
}
|
|
|
|
// Générer un token unique
|
|
const crypto = await import('crypto');
|
|
const token = crypto.randomBytes(32).toString('hex');
|
|
|
|
// Définir l'expiration à 24h
|
|
const expiresAt = new Date();
|
|
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
|
|
// Sauvegarder le token en base
|
|
await db.createPasswordResetToken(user.id, token, expiresAt);
|
|
|
|
// Générer le lien de réinitialisation
|
|
// TODO: Remplacer par l'URL réelle de votre application en production
|
|
const resetLink = `https://votre-domaine.com/reset-password?token=${token}`;
|
|
|
|
// Envoyer l'email
|
|
const emailService = await import('./emailService');
|
|
const emailSent = await emailService.sendPasswordResetEmail({
|
|
apprenantEmail: user.email,
|
|
apprenantNom: user.name || 'Utilisateur',
|
|
apprenantPrenom: '',
|
|
resetLink,
|
|
});
|
|
|
|
if (!emailSent) {
|
|
throw new Error("Échec de l'envoi de l'email");
|
|
}
|
|
|
|
return { success: true, message: "Email de réinitialisation envoyé" };
|
|
}),
|
|
}),
|
|
|
|
// ===== GESTION DES TEMPLATES D'EMAILS =====
|
|
emailTemplates: router({
|
|
list: adminProcedure.query(async () => {
|
|
return db.getAllEmailTemplates();
|
|
}),
|
|
|
|
getByType: adminProcedure
|
|
.input(z.object({ type: z.string() }))
|
|
.query(async ({ input }) => {
|
|
return db.getEmailTemplateByType(input.type);
|
|
}),
|
|
|
|
upsert: adminProcedure
|
|
.input(z.object({
|
|
type: z.string(),
|
|
name: z.string(),
|
|
logoUrl: z.string().nullable(),
|
|
primaryColor: z.string(),
|
|
headerBgColor: z.string(),
|
|
headerTextColor: z.string(),
|
|
headerTitle: z.string(),
|
|
footerText: z.string().nullable(),
|
|
active: z.boolean(),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
return db.upsertEmailTemplate(input);
|
|
}),
|
|
|
|
delete: adminProcedure
|
|
.input(z.object({ type: z.string() }))
|
|
.mutation(async ({ input }) => {
|
|
await db.deleteEmailTemplate(input.type);
|
|
return { success: true };
|
|
}),
|
|
|
|
initializeDefaults: adminProcedure.mutation(async () => {
|
|
await db.initializeDefaultEmailTemplates();
|
|
return { success: true };
|
|
}),
|
|
}),
|
|
|
|
emailConfig: router({
|
|
get: adminProcedure.query(async () => {
|
|
return db.getActiveEmailConfig();
|
|
}),
|
|
|
|
upsert: adminProcedure
|
|
.input(z.object({
|
|
provider: z.string(),
|
|
apiKey: z.string().nullable(),
|
|
fromEmail: z.string().email(),
|
|
fromName: z.string(),
|
|
mode: z.enum(["simulation", "production"]),
|
|
domainVerified: z.boolean(),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
await db.upsertEmailConfig(input);
|
|
return { success: true };
|
|
}),
|
|
|
|
testEmail: adminProcedure
|
|
.input(z.object({
|
|
toEmail: z.string().email(),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
// Importer le service d'envoi
|
|
const { sendEmail } = await import("./_core/emailSender");
|
|
|
|
try {
|
|
const success = await sendEmail({
|
|
to: input.toEmail,
|
|
subject: "Test d'envoi d'email - Formation Manager Itinova",
|
|
html: `
|
|
<h1>Test réussi !</h1>
|
|
<p>Cet email de test a été envoyé avec succès depuis votre configuration SMTP.</p>
|
|
<p>Votre configuration d'envoi d'emails fonctionne correctement.</p>
|
|
`,
|
|
});
|
|
|
|
return { success, message: success ? "Email envoyé avec succès" : "Échec de l'envoi" };
|
|
} catch (error: any) {
|
|
return { success: false, message: error.message };
|
|
}
|
|
}),
|
|
}),
|
|
});
|
|
|
|
export type AppRouter = typeof appRouter;
|