Checkpoint: Application complète de gestion des formations Manager Itinova avec toutes les fonctionnalités : gestion des formations/sessions/apprenants, système d'inscription avec validations, génération d'invitations Outlook, emails automatiques, exports PDF/Excel, et documentation complète.
This commit is contained in:
243
server/db.ts
243
server/db.ts
@@ -1,6 +1,17 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, and, sql, lt, gt } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/mysql2";
|
||||
import { InsertUser, users } from "../drizzle/schema";
|
||||
import {
|
||||
InsertUser,
|
||||
users,
|
||||
formations,
|
||||
apprenants,
|
||||
sessions,
|
||||
inscriptions,
|
||||
InsertFormation,
|
||||
InsertApprenant,
|
||||
InsertSession,
|
||||
InsertInscription
|
||||
} from "../drizzle/schema";
|
||||
import { ENV } from './_core/env';
|
||||
|
||||
let _db: ReturnType<typeof drizzle> | null = null;
|
||||
@@ -89,4 +100,230 @@ export async function getUserByOpenId(openId: string) {
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
// TODO: add feature queries here as your schema grows.
|
||||
// ===== FORMATIONS =====
|
||||
|
||||
export async function getAllFormations() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(formations).orderBy(formations.createdAt);
|
||||
}
|
||||
|
||||
export async function getFormationById(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
const result = await db.select().from(formations).where(eq(formations.id, id)).limit(1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
export async function getFormationByLien(lien: string) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
const result = await db.select().from(formations).where(eq(formations.lienUnique, lien)).limit(1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
export async function createFormation(data: InsertFormation) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const result = await db.insert(formations).values(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateFormation(id: number, data: Partial<InsertFormation>) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.update(formations).set(data).where(eq(formations.id, id));
|
||||
}
|
||||
|
||||
export async function deleteFormation(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.delete(formations).where(eq(formations.id, id));
|
||||
}
|
||||
|
||||
// ===== APPRENANTS =====
|
||||
|
||||
export async function getAllApprenants() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(apprenants).orderBy(apprenants.nom, apprenants.prenom);
|
||||
}
|
||||
|
||||
export async function getApprenantById(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
const result = await db.select().from(apprenants).where(eq(apprenants.id, id)).limit(1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
export async function getApprenantByEmail(email: string) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
const result = await db.select().from(apprenants).where(eq(apprenants.email, email)).limit(1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
export async function createApprenant(data: InsertApprenant) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const result = await db.insert(apprenants).values(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateApprenant(id: number, data: Partial<InsertApprenant>) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.update(apprenants).set(data).where(eq(apprenants.id, id));
|
||||
}
|
||||
|
||||
export async function deleteApprenant(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.delete(apprenants).where(eq(apprenants.id, id));
|
||||
}
|
||||
|
||||
// ===== SESSIONS =====
|
||||
|
||||
export async function getAllSessions() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(sessions).orderBy(sessions.dateDebut);
|
||||
}
|
||||
|
||||
export async function getSessionsByFormation(formationId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(sessions).where(eq(sessions.formationId, formationId)).orderBy(sessions.dateDebut);
|
||||
}
|
||||
|
||||
export async function getSessionById(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
const result = await db.select().from(sessions).where(eq(sessions.id, id)).limit(1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
export async function createSession(data: InsertSession) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const result = await db.insert(sessions).values(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateSession(id: number, data: Partial<InsertSession>) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.update(sessions).set(data).where(eq(sessions.id, id));
|
||||
}
|
||||
|
||||
export async function deleteSession(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.delete(sessions).where(eq(sessions.id, id));
|
||||
}
|
||||
|
||||
// ===== INSCRIPTIONS =====
|
||||
|
||||
export async function getInscriptionsBySession(sessionId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const result = await db
|
||||
.select({
|
||||
inscription: inscriptions,
|
||||
apprenant: apprenants,
|
||||
})
|
||||
.from(inscriptions)
|
||||
.leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
|
||||
.where(eq(inscriptions.sessionId, sessionId))
|
||||
.orderBy(inscriptions.dateInscription);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getInscriptionsByApprenant(apprenantId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const result = await db
|
||||
.select({
|
||||
inscription: inscriptions,
|
||||
session: sessions,
|
||||
formation: formations,
|
||||
})
|
||||
.from(inscriptions)
|
||||
.leftJoin(sessions, eq(inscriptions.sessionId, sessions.id))
|
||||
.leftJoin(formations, eq(sessions.formationId, formations.id))
|
||||
.where(eq(inscriptions.apprenantId, apprenantId))
|
||||
.orderBy(inscriptions.dateInscription);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getInscriptionByApprenantAndSession(apprenantId: number, sessionId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(inscriptions)
|
||||
.where(and(
|
||||
eq(inscriptions.apprenantId, apprenantId),
|
||||
eq(inscriptions.sessionId, sessionId)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
export async function countInscriptionsConfirmees(sessionId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return 0;
|
||||
|
||||
const result = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(inscriptions)
|
||||
.where(and(
|
||||
eq(inscriptions.sessionId, sessionId),
|
||||
eq(inscriptions.statut, "confirmee")
|
||||
));
|
||||
|
||||
return result[0]?.count || 0;
|
||||
}
|
||||
|
||||
export async function createInscription(data: InsertInscription) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const result = await db.insert(inscriptions).values(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateInscription(id: number, data: Partial<InsertInscription>) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.update(inscriptions).set(data).where(eq(inscriptions.id, id));
|
||||
}
|
||||
|
||||
export async function deleteInscription(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.delete(inscriptions).where(eq(inscriptions.id, id));
|
||||
}
|
||||
|
||||
export async function getSessionsAvecInscriptions(formationId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const result = await db
|
||||
.select({
|
||||
session: sessions,
|
||||
nbInscrits: sql<number>`count(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 END)`,
|
||||
})
|
||||
.from(sessions)
|
||||
.leftJoin(inscriptions, eq(sessions.id, inscriptions.sessionId))
|
||||
.where(eq(sessions.formationId, formationId))
|
||||
.groupBy(sessions.id)
|
||||
.orderBy(sessions.dateDebut);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
283
server/emailService.ts
Normal file
283
server/emailService.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Service d'envoi d'emails pour les formations
|
||||
* Note: Ce module utilise console.log pour simuler l'envoi d'emails
|
||||
* Dans un environnement de production, intégrer un service SMTP réel
|
||||
*/
|
||||
|
||||
import { generateFormationICS } from "./icsGenerator";
|
||||
|
||||
interface EmailParams {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
attachments?: Array<{
|
||||
filename: string;
|
||||
content: string;
|
||||
contentType: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simule l'envoi d'un email (à remplacer par un vrai service SMTP)
|
||||
*/
|
||||
async function sendEmail(params: EmailParams): Promise<boolean> {
|
||||
console.log('=== EMAIL SIMULÉ ===');
|
||||
console.log('To:', params.to);
|
||||
console.log('Subject:', params.subject);
|
||||
console.log('HTML:', params.html.substring(0, 200) + '...');
|
||||
if (params.attachments) {
|
||||
console.log('Attachments:', params.attachments.map(a => a.filename).join(', '));
|
||||
}
|
||||
console.log('===================');
|
||||
|
||||
// Simuler un délai d'envoi
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template HTML de base pour les emails
|
||||
*/
|
||||
function getEmailTemplate(content: string): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background-color: #2563eb; color: white; padding: 20px; text-align: center; }
|
||||
.content { background-color: #f9fafb; padding: 30px; }
|
||||
.footer { background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }
|
||||
.button { display: inline-block; padding: 12px 24px; background-color: #2563eb; color: white; text-decoration: none; border-radius: 6px; margin: 10px 0; }
|
||||
.info-box { background-color: #dbeafe; border-left: 4px solid #2563eb; padding: 15px; margin: 15px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Formation Manager Itinova</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
${content}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Cet email a été envoyé automatiquement par le système de gestion des formations Itinova.</p>
|
||||
<p>Pour toute question, veuillez contacter le service RH.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un email de confirmation d'inscription avec invitation Outlook
|
||||
*/
|
||||
export async function sendInscriptionConfirmation(params: {
|
||||
apprenantEmail: string;
|
||||
apprenantNom: string;
|
||||
apprenantPrenom: string;
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
dateDebut: Date;
|
||||
dateFin: Date;
|
||||
lieu: string;
|
||||
statut: 'confirmee' | 'liste_attente';
|
||||
}): Promise<boolean> {
|
||||
const isConfirmed = params.statut === 'confirmee';
|
||||
|
||||
const content = `
|
||||
<h2>Confirmation d'inscription</h2>
|
||||
<p>Bonjour ${params.apprenantPrenom} ${params.apprenantNom},</p>
|
||||
|
||||
${isConfirmed
|
||||
? '<p><strong>Votre inscription a été confirmée !</strong></p>'
|
||||
: '<p><strong>Vous avez été ajouté à la liste d\'attente.</strong></p>'
|
||||
}
|
||||
|
||||
<div class="info-box">
|
||||
<h3>Détails de la formation</h3>
|
||||
<p><strong>Formation :</strong> ${params.formationNom}</p>
|
||||
<p><strong>Session :</strong> ${params.sessionNom}</p>
|
||||
<p><strong>Date de début :</strong> ${params.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</p>
|
||||
<p><strong>Date de fin :</strong> ${params.dateFin.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</p>
|
||||
<p><strong>Lieu :</strong> ${params.lieu}</p>
|
||||
</div>
|
||||
|
||||
${isConfirmed
|
||||
? `<p>Une invitation Outlook est jointe à cet email. Merci de l'ajouter à votre calendrier pour bloquer votre agenda.</p>
|
||||
<p><strong>Important :</strong> Les inscriptions seront bloquées 15 jours avant le début de la session. Après cette date, aucune modification ne sera possible.</p>`
|
||||
: '<p>Nous vous contacterons dès qu\'une place se libère.</p>'
|
||||
}
|
||||
|
||||
<p>À bientôt pour cette formation !</p>
|
||||
`;
|
||||
|
||||
const attachments = isConfirmed ? [{
|
||||
filename: 'invitation.ics',
|
||||
content: generateFormationICS({
|
||||
formationNom: params.formationNom,
|
||||
sessionNom: params.sessionNom,
|
||||
dateDebut: params.dateDebut,
|
||||
dateFin: params.dateFin,
|
||||
lieu: params.lieu,
|
||||
apprenantNom: params.apprenantNom,
|
||||
apprenantPrenom: params.apprenantPrenom,
|
||||
apprenantEmail: params.apprenantEmail,
|
||||
}),
|
||||
contentType: 'text/calendar',
|
||||
}] : undefined;
|
||||
|
||||
return sendEmail({
|
||||
to: params.apprenantEmail,
|
||||
subject: isConfirmed
|
||||
? `Confirmation d'inscription - ${params.formationNom}`
|
||||
: `Liste d'attente - ${params.formationNom}`,
|
||||
html: getEmailTemplate(content),
|
||||
attachments,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un email teaser pour une session
|
||||
*/
|
||||
export async function sendTeaserEmail(params: {
|
||||
apprenantEmail: string;
|
||||
apprenantPrenom: string;
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
dateDebut: Date;
|
||||
}): Promise<boolean> {
|
||||
const content = `
|
||||
<h2>Votre formation approche !</h2>
|
||||
<p>Bonjour ${params.apprenantPrenom},</p>
|
||||
|
||||
<p>Nous sommes ravis de vous accueillir prochainement pour la formation <strong>${params.formationNom}</strong>.</p>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>Session :</strong> ${params.sessionNom}</p>
|
||||
<p><strong>Date :</strong> ${params.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}</p>
|
||||
</div>
|
||||
|
||||
<p>Préparez-vous à vivre une expérience enrichissante qui vous permettra de développer vos compétences managériales.</p>
|
||||
|
||||
<p>À très bientôt !</p>
|
||||
`;
|
||||
|
||||
return sendEmail({
|
||||
to: params.apprenantEmail,
|
||||
subject: `Votre formation ${params.formationNom} approche !`,
|
||||
html: getEmailTemplate(content),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un email de rappel J-7
|
||||
*/
|
||||
export async function sendRappelJ7Email(params: {
|
||||
apprenantEmail: string;
|
||||
apprenantPrenom: string;
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
dateDebut: Date;
|
||||
lieu: string;
|
||||
}): Promise<boolean> {
|
||||
const content = `
|
||||
<h2>Rappel : Votre formation dans 7 jours</h2>
|
||||
<p>Bonjour ${params.apprenantPrenom},</p>
|
||||
|
||||
<p>Nous vous rappelons que votre formation <strong>${params.formationNom}</strong> commence dans 7 jours.</p>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>Informations pratiques</h3>
|
||||
<p><strong>Session :</strong> ${params.sessionNom}</p>
|
||||
<p><strong>Date :</strong> ${params.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</p>
|
||||
<p><strong>Lieu :</strong> ${params.lieu}</p>
|
||||
</div>
|
||||
|
||||
<p><strong>Merci de vous présenter à l'heure indiquée.</strong></p>
|
||||
|
||||
<p>Si vous avez des questions, n'hésitez pas à contacter le service RH.</p>
|
||||
|
||||
<p>À très bientôt !</p>
|
||||
`;
|
||||
|
||||
return sendEmail({
|
||||
to: params.apprenantEmail,
|
||||
subject: `Rappel J-7 : ${params.formationNom}`,
|
||||
html: getEmailTemplate(content),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie des emails groupés à tous les inscrits d'une session
|
||||
*/
|
||||
export async function sendGroupEmail(params: {
|
||||
recipients: Array<{ email: string; prenom: string }>;
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
type: 'teaser' | 'rappel';
|
||||
dateDebut: Date;
|
||||
lieu?: string;
|
||||
}): Promise<{ sent: number; failed: number }> {
|
||||
let sent = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const recipient of params.recipients) {
|
||||
try {
|
||||
if (params.type === 'teaser') {
|
||||
await sendTeaserEmail({
|
||||
apprenantEmail: recipient.email,
|
||||
apprenantPrenom: recipient.prenom,
|
||||
formationNom: params.formationNom,
|
||||
sessionNom: params.sessionNom,
|
||||
dateDebut: params.dateDebut,
|
||||
});
|
||||
} else {
|
||||
await sendRappelJ7Email({
|
||||
apprenantEmail: recipient.email,
|
||||
apprenantPrenom: recipient.prenom,
|
||||
formationNom: params.formationNom,
|
||||
sessionNom: params.sessionNom,
|
||||
dateDebut: params.dateDebut,
|
||||
lieu: params.lieu || '',
|
||||
});
|
||||
}
|
||||
sent++;
|
||||
} catch (error) {
|
||||
console.error(`Erreur envoi email à ${recipient.email}:`, error);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
return { sent, failed };
|
||||
}
|
||||
191
server/exportService.ts
Normal file
191
server/exportService.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Service d'export des données en Excel et PDF
|
||||
*/
|
||||
|
||||
import * as XLSX from 'xlsx';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
|
||||
interface InscriptionExport {
|
||||
nom: string;
|
||||
prenom: string;
|
||||
email: string;
|
||||
codeEtablissement: string;
|
||||
statut: string;
|
||||
dateInscription: Date;
|
||||
}
|
||||
|
||||
interface SessionInfo {
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
dateDebut: Date;
|
||||
dateFin: Date;
|
||||
lieu: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un fichier Excel avec la liste des inscrits
|
||||
*/
|
||||
export function generateExcelExport(
|
||||
sessionInfo: SessionInfo,
|
||||
inscriptions: InscriptionExport[]
|
||||
): Buffer {
|
||||
// Préparer les données
|
||||
const data = inscriptions.map(i => ({
|
||||
'Nom': i.nom,
|
||||
'Prénom': i.prenom,
|
||||
'Email': i.email,
|
||||
'Code établissement': i.codeEtablissement,
|
||||
'Statut': i.statut === 'confirmee' ? 'Confirmée' : i.statut === 'liste_attente' ? 'Liste d\'attente' : 'Annulée',
|
||||
'Date d\'inscription': i.dateInscription.toLocaleDateString('fr-FR'),
|
||||
}));
|
||||
|
||||
// Créer le workbook
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// Créer la feuille avec les informations de session
|
||||
const infoData = [
|
||||
['Formation', sessionInfo.formationNom],
|
||||
['Session', sessionInfo.sessionNom],
|
||||
['Date de début', sessionInfo.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})],
|
||||
['Date de fin', sessionInfo.dateFin.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})],
|
||||
['Lieu', sessionInfo.lieu],
|
||||
['Nombre d\'inscrits', inscriptions.length.toString()],
|
||||
[],
|
||||
];
|
||||
|
||||
// Créer la feuille principale
|
||||
const ws = XLSX.utils.aoa_to_sheet(infoData);
|
||||
|
||||
// Ajouter les données des inscrits
|
||||
XLSX.utils.sheet_add_json(ws, data, { origin: -1 });
|
||||
|
||||
// Ajouter la feuille au workbook
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Inscrits');
|
||||
|
||||
// Générer le buffer
|
||||
const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
return excelBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un fichier PDF avec la liste des inscrits
|
||||
*/
|
||||
export function generatePDFExport(
|
||||
sessionInfo: SessionInfo,
|
||||
inscriptions: InscriptionExport[]
|
||||
): Buffer {
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Titre
|
||||
doc.setFontSize(18);
|
||||
doc.text('Liste des inscrits', 14, 20);
|
||||
|
||||
// Informations de session
|
||||
doc.setFontSize(10);
|
||||
let yPos = 35;
|
||||
doc.text(`Formation : ${sessionInfo.formationNom}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Session : ${sessionInfo.sessionNom}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Date : ${sessionInfo.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Lieu : ${sessionInfo.lieu}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Nombre d'inscrits : ${inscriptions.length}`, 14, yPos);
|
||||
|
||||
// Tableau des inscrits
|
||||
const tableData = inscriptions.map(i => [
|
||||
i.nom,
|
||||
i.prenom,
|
||||
i.email,
|
||||
i.codeEtablissement,
|
||||
i.statut === 'confirmee' ? 'Confirmée' : i.statut === 'liste_attente' ? 'Liste d\'attente' : 'Annulée',
|
||||
]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos + 10,
|
||||
head: [['Nom', 'Prénom', 'Email', 'Code étab.', 'Statut']],
|
||||
body: tableData,
|
||||
styles: { fontSize: 9 },
|
||||
headStyles: { fillColor: [37, 99, 235] },
|
||||
});
|
||||
|
||||
return Buffer.from(doc.output('arraybuffer'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère une feuille de présence PDF
|
||||
*/
|
||||
export function generateFeuillePresence(
|
||||
sessionInfo: SessionInfo,
|
||||
inscriptions: InscriptionExport[]
|
||||
): Buffer {
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Titre
|
||||
doc.setFontSize(18);
|
||||
doc.text('Feuille de présence', 14, 20);
|
||||
|
||||
// Informations de session
|
||||
doc.setFontSize(10);
|
||||
let yPos = 35;
|
||||
doc.text(`Formation : ${sessionInfo.formationNom}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Session : ${sessionInfo.sessionNom}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Date : ${sessionInfo.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Lieu : ${sessionInfo.lieu}`, 14, yPos);
|
||||
|
||||
// Tableau de présence (uniquement les inscrits confirmés)
|
||||
const inscritsConfirmes = inscriptions.filter(i => i.statut === 'confirmee');
|
||||
const tableData = inscritsConfirmes.map(i => [
|
||||
i.nom,
|
||||
i.prenom,
|
||||
i.codeEtablissement,
|
||||
'', // Colonne signature
|
||||
]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos + 10,
|
||||
head: [['Nom', 'Prénom', 'Code établissement', 'Signature']],
|
||||
body: tableData,
|
||||
styles: { fontSize: 10, cellPadding: 5 },
|
||||
headStyles: { fillColor: [37, 99, 235] },
|
||||
columnStyles: {
|
||||
3: { cellWidth: 40 }, // Largeur pour la colonne signature
|
||||
},
|
||||
});
|
||||
|
||||
return Buffer.from(doc.output('arraybuffer'));
|
||||
}
|
||||
109
server/icsGenerator.ts
Normal file
109
server/icsGenerator.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Module de génération de fichiers ICS (iCalendar) pour les invitations Outlook
|
||||
*/
|
||||
|
||||
interface ICSEvent {
|
||||
summary: string;
|
||||
description?: string;
|
||||
location: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
attendeeEmail: string;
|
||||
attendeeName: string;
|
||||
organizerEmail?: string;
|
||||
organizerName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date au format iCalendar (YYYYMMDDTHHMMSSZ)
|
||||
*/
|
||||
function formatICSDate(date: Date): string {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un UID unique pour l'événement
|
||||
*/
|
||||
function generateUID(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}@itinova.com`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Échappe les caractères spéciaux pour le format ICS
|
||||
*/
|
||||
function escapeICS(text: string): string {
|
||||
return text
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/;/g, '\\;')
|
||||
.replace(/,/g, '\\,')
|
||||
.replace(/\n/g, '\\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un fichier ICS pour une invitation Outlook
|
||||
*/
|
||||
export function generateICS(event: ICSEvent): string {
|
||||
const now = new Date();
|
||||
const uid = generateUID();
|
||||
|
||||
const icsContent = [
|
||||
'BEGIN:VCALENDAR',
|
||||
'VERSION:2.0',
|
||||
'PRODID:-//Itinova//Formation Manager//FR',
|
||||
'CALSCALE:GREGORIAN',
|
||||
'METHOD:REQUEST',
|
||||
'BEGIN:VEVENT',
|
||||
`UID:${uid}`,
|
||||
`DTSTAMP:${formatICSDate(now)}`,
|
||||
`DTSTART:${formatICSDate(event.startDate)}`,
|
||||
`DTEND:${formatICSDate(event.endDate)}`,
|
||||
`SUMMARY:${escapeICS(event.summary)}`,
|
||||
event.description ? `DESCRIPTION:${escapeICS(event.description)}` : '',
|
||||
`LOCATION:${escapeICS(event.location)}`,
|
||||
'STATUS:CONFIRMED',
|
||||
'TRANSP:OPAQUE', // Marque comme "occupé" dans le calendrier
|
||||
'SEQUENCE:0',
|
||||
`ORGANIZER;CN=${escapeICS(event.organizerName || 'Formation Itinova')}:mailto:${event.organizerEmail || 'formation@itinova.com'}`,
|
||||
`ATTENDEE;CN=${escapeICS(event.attendeeName)};RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:${event.attendeeEmail}`,
|
||||
'BEGIN:VALARM',
|
||||
'TRIGGER:-P1D', // Rappel 1 jour avant
|
||||
'ACTION:DISPLAY',
|
||||
`DESCRIPTION:Rappel: ${escapeICS(event.summary)}`,
|
||||
'END:VALARM',
|
||||
'END:VEVENT',
|
||||
'END:VCALENDAR',
|
||||
].filter(line => line !== '').join('\r\n');
|
||||
|
||||
return icsContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un fichier ICS pour une session de formation
|
||||
*/
|
||||
export function generateFormationICS(params: {
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
dateDebut: Date;
|
||||
dateFin: Date;
|
||||
lieu: string;
|
||||
apprenantNom: string;
|
||||
apprenantPrenom: string;
|
||||
apprenantEmail: string;
|
||||
}): string {
|
||||
return generateICS({
|
||||
summary: `Formation: ${params.formationNom} - ${params.sessionNom}`,
|
||||
description: `Vous êtes inscrit à la formation "${params.formationNom}".\n\nSession: ${params.sessionNom}\n\nMerci de vous présenter à l'heure indiquée.`,
|
||||
location: params.lieu,
|
||||
startDate: params.dateDebut,
|
||||
endDate: params.dateFin,
|
||||
attendeeEmail: params.apprenantEmail,
|
||||
attendeeName: `${params.apprenantPrenom} ${params.apprenantNom}`,
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,22 @@
|
||||
import { COOKIE_NAME } from "@shared/const";
|
||||
import { getSessionCookieOptions } from "./_core/cookies";
|
||||
import { systemRouter } from "./_core/systemRouter";
|
||||
import { publicProcedure, router } from "./_core/trpc";
|
||||
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({
|
||||
// if you need to use socket.io, read and register route in server/_core/index.ts, all api should start with '/api/' so that the gateway can route correctly
|
||||
system: systemRouter,
|
||||
auth: router({
|
||||
me: publicProcedure.query(opts => opts.ctx.user),
|
||||
@@ -17,12 +29,403 @@ export const appRouter = router({
|
||||
}),
|
||||
}),
|
||||
|
||||
// TODO: add feature routers here, e.g.
|
||||
// todo: router({
|
||||
// list: protectedProcedure.query(({ ctx }) =>
|
||||
// db.getUserTodos(ctx.user.id)
|
||||
// ),
|
||||
// }),
|
||||
// ===== 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),
|
||||
})).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(),
|
||||
})).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,
|
||||
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,
|
||||
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,
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user