Checkpoint: Refonte majeure : transformation du système de sessions en séquences avec support de jusqu'à 4 dates par séquence. L'inscription à une séquence inscrit automatiquement l'apprenant à toutes les dates. Mise à jour complète du backend (schéma DB, procédures tRPC, emails, exports) et du frontend (AdminSequences, AdminSequenceInscrits, Inscription, Admin). Toutes les fonctionnalités existantes (filtres, statistiques, exports PDF/Excel, emails automatiques) sont préservées et adaptées au nouveau modèle.
This commit is contained in:
284
server/db.ts
284
server/db.ts
@@ -1,16 +1,22 @@
|
||||
import { eq, and, sql, lt, gt } from "drizzle-orm";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/mysql2";
|
||||
import {
|
||||
InsertUser,
|
||||
users,
|
||||
formations,
|
||||
apprenants,
|
||||
sessions,
|
||||
sequences,
|
||||
datesFormation,
|
||||
inscriptions,
|
||||
InsertFormation,
|
||||
InsertApprenant,
|
||||
InsertSession,
|
||||
InsertInscription
|
||||
InsertSequence,
|
||||
InsertDateFormation,
|
||||
InsertInscription,
|
||||
Sequence,
|
||||
DateFormation,
|
||||
Apprenant,
|
||||
Formation
|
||||
} from "../drizzle/schema";
|
||||
import { ENV } from './_core/env';
|
||||
|
||||
@@ -100,230 +106,256 @@ export async function getUserByOpenId(openId: string) {
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
// ===== FORMATIONS =====
|
||||
// ==================== FORMATIONS ====================
|
||||
|
||||
export async function getAllFormations() {
|
||||
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 getFormations() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(formations).orderBy(formations.createdAt);
|
||||
|
||||
return await db.select().from(formations);
|
||||
}
|
||||
|
||||
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];
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
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 =====
|
||||
// ==================== SÉQUENCES ====================
|
||||
|
||||
export async function getAllApprenants() {
|
||||
export async function createSequence(data: InsertSequence) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(apprenants).orderBy(apprenants.nom, apprenants.prenom);
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
const result = await db.insert(sequences).values(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getApprenantById(id: number) {
|
||||
export async function getSequences() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
return await db.select().from(sequences);
|
||||
}
|
||||
|
||||
export async function getSequenceById(id: number): Promise<Sequence | undefined> {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
|
||||
const result = await db.select().from(sequences).where(eq(sequences.id, id)).limit(1);
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
export async function getSequencesByFormation(formationId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
return await db.select().from(sequences).where(eq(sequences.formationId, formationId));
|
||||
}
|
||||
|
||||
export async function updateSequence(id: number, data: Partial<InsertSequence>) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
await db.update(sequences).set(data).where(eq(sequences.id, id));
|
||||
}
|
||||
|
||||
export async function deleteSequence(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
await db.delete(sequences).where(eq(sequences.id, id));
|
||||
}
|
||||
|
||||
// ==================== DATES DE FORMATION ====================
|
||||
|
||||
export async function createDateFormation(data: InsertDateFormation) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
const result = await db.insert(datesFormation).values(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getDatesBySequence(sequenceId: number): Promise<DateFormation[]> {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const results = await db.select()
|
||||
.from(datesFormation)
|
||||
.where(eq(datesFormation.sequenceId, sequenceId))
|
||||
.orderBy(datesFormation.ordre);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function deleteDatesBySequence(sequenceId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
await db.delete(datesFormation).where(eq(datesFormation.sequenceId, sequenceId));
|
||||
}
|
||||
|
||||
// ==================== APPRENANTS ====================
|
||||
|
||||
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 getApprenants() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
return await db.select().from(apprenants);
|
||||
}
|
||||
|
||||
export async function getApprenantById(id: number): Promise<Apprenant | undefined> {
|
||||
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];
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
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 =====
|
||||
// ==================== INSCRIPTIONS ====================
|
||||
|
||||
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) {
|
||||
export async function createInscription(data: InsertInscription) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const result = await db.insert(sessions).values(data);
|
||||
|
||||
const result = await db.insert(inscriptions).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) {
|
||||
export async function getInscriptionsBySequence(sequenceId: 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);
|
||||
const results = await db.select({
|
||||
inscription: inscriptions,
|
||||
apprenant: apprenants,
|
||||
})
|
||||
.from(inscriptions)
|
||||
.leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
|
||||
.where(eq(inscriptions.sequenceId, sequenceId));
|
||||
|
||||
return result;
|
||||
return results;
|
||||
}
|
||||
|
||||
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);
|
||||
const results = await db.select({
|
||||
inscription: inscriptions,
|
||||
sequence: sequences,
|
||||
})
|
||||
.from(inscriptions)
|
||||
.leftJoin(sequences, eq(inscriptions.sequenceId, sequences.id))
|
||||
.where(eq(inscriptions.apprenantId, apprenantId));
|
||||
|
||||
return result;
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function getInscriptionByApprenantAndSession(apprenantId: number, sessionId: number) {
|
||||
export async function checkExistingInscription(apprenantId: number, sequenceId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
const result = await db.select()
|
||||
.from(inscriptions)
|
||||
.where(and(
|
||||
eq(inscriptions.apprenantId, apprenantId),
|
||||
eq(inscriptions.sessionId, sessionId)
|
||||
eq(inscriptions.sequenceId, sequenceId)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
return result[0];
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
export async function countInscriptionsConfirmees(sessionId: number) {
|
||||
export async function countInscriptionsBySequence(sequenceId: number, statut?: string) {
|
||||
const db = await getDb();
|
||||
if (!db) return 0;
|
||||
|
||||
const result = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
const conditions = [eq(inscriptions.sequenceId, sequenceId)];
|
||||
if (statut) {
|
||||
conditions.push(eq(inscriptions.statut, statut as any));
|
||||
}
|
||||
|
||||
const result = await db.select({ count: sql<number>`count(*)` })
|
||||
.from(inscriptions)
|
||||
.where(and(
|
||||
eq(inscriptions.sessionId, sessionId),
|
||||
eq(inscriptions.statut, "confirmee")
|
||||
));
|
||||
.where(and(...conditions));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ function getEmailTemplate(content: string): string {
|
||||
.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; }
|
||||
.date-item { margin: 10px 0; padding: 10px; background-color: white; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -88,7 +89,7 @@ function getEmailTemplate(content: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un email de confirmation d'inscription avec invitation Outlook
|
||||
* Envoie un email de confirmation d'inscription avec invitations Outlook pour toutes les dates
|
||||
*/
|
||||
export async function sendInscriptionConfirmation(params: {
|
||||
apprenantEmail: string;
|
||||
@@ -96,9 +97,8 @@ export async function sendInscriptionConfirmation(params: {
|
||||
apprenantPrenom: string;
|
||||
apprenantFonction: string;
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
dateDebut: Date;
|
||||
dateFin: Date;
|
||||
sequenceNom: string;
|
||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
||||
lieu: string;
|
||||
statut: 'confirmee' | 'liste_attente';
|
||||
}): Promise<boolean> {
|
||||
@@ -106,6 +106,29 @@ export async function sendInscriptionConfirmation(params: {
|
||||
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
|
||||
params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre';
|
||||
|
||||
// Générer la liste des dates
|
||||
const datesHTML = params.dates.map(date => `
|
||||
<div class="date-item">
|
||||
<strong>Date ${date.ordre} :</strong><br/>
|
||||
Du ${date.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}<br/>
|
||||
au ${date.dateFin.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const content = `
|
||||
<h2>Confirmation d'inscription</h2>
|
||||
<p>Bonjour ${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom},</p>
|
||||
@@ -118,49 +141,36 @@ export async function sendInscriptionConfirmation(params: {
|
||||
<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>Séquence :</strong> ${params.sequenceNom}</p>
|
||||
<p><strong>Lieu :</strong> ${params.lieu}</p>
|
||||
<h4>Dates de formation (${params.dates.length} séance${params.dates.length > 1 ? 's' : ''}) :</h4>
|
||||
${datesHTML}
|
||||
</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>Des invitations Outlook sont jointes à cet email pour chaque date de formation. Merci de les 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 séquence. 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',
|
||||
// Générer une invitation ICS pour chaque date
|
||||
const attachments = isConfirmed ? params.dates.map((date, index) => ({
|
||||
filename: `invitation_date_${date.ordre}.ics`,
|
||||
content: generateFormationICS({
|
||||
formationNom: params.formationNom,
|
||||
sessionNom: params.sessionNom,
|
||||
dateDebut: params.dateDebut,
|
||||
dateFin: params.dateFin,
|
||||
sessionNom: `${params.sequenceNom} - Date ${date.ordre}`,
|
||||
dateDebut: date.dateDebut,
|
||||
dateFin: date.dateFin,
|
||||
lieu: params.lieu,
|
||||
apprenantNom: params.apprenantNom,
|
||||
apprenantPrenom: params.apprenantPrenom,
|
||||
apprenantEmail: params.apprenantEmail,
|
||||
}),
|
||||
contentType: 'text/calendar',
|
||||
}] : undefined;
|
||||
})) : undefined;
|
||||
|
||||
return sendEmail({
|
||||
to: params.apprenantEmail,
|
||||
@@ -173,7 +183,7 @@ export async function sendInscriptionConfirmation(params: {
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un email teaser pour une session
|
||||
* Envoie un email teaser pour une séquence
|
||||
*/
|
||||
export async function sendTeaserEmail(params: {
|
||||
apprenantEmail: string;
|
||||
@@ -181,13 +191,24 @@ export async function sendTeaserEmail(params: {
|
||||
apprenantNom: string;
|
||||
apprenantFonction: string;
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
dateDebut: Date;
|
||||
sequenceNom: string;
|
||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
||||
}): Promise<boolean> {
|
||||
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
|
||||
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
|
||||
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
|
||||
|
||||
const datesHTML = params.dates.map(date => `
|
||||
<div class="date-item">
|
||||
<strong>Date ${date.ordre} :</strong> ${date.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const content = `
|
||||
<h2>Votre formation approche !</h2>
|
||||
<p>Bonjour ${salutation},</p>
|
||||
@@ -195,13 +216,9 @@ export async function sendTeaserEmail(params: {
|
||||
<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>
|
||||
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
|
||||
<h4>Dates :</h4>
|
||||
${datesHTML}
|
||||
</div>
|
||||
|
||||
<p>Préparez-vous à vivre une expérience enrichissante qui vous permettra de développer vos compétences managériales.</p>
|
||||
@@ -225,14 +242,28 @@ export async function sendRappelJ7Email(params: {
|
||||
apprenantNom: string;
|
||||
apprenantFonction: string;
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
dateDebut: Date;
|
||||
sequenceNom: string;
|
||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
||||
lieu: string;
|
||||
}): Promise<boolean> {
|
||||
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
|
||||
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
|
||||
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
|
||||
|
||||
const datesHTML = params.dates.map(date => `
|
||||
<div class="date-item">
|
||||
<strong>Date ${date.ordre} :</strong><br/>
|
||||
${date.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const content = `
|
||||
<h2>Rappel : Votre formation dans 7 jours</h2>
|
||||
<p>Bonjour ${salutation},</p>
|
||||
@@ -241,16 +272,10 @@ export async function sendRappelJ7Email(params: {
|
||||
|
||||
<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>Séquence :</strong> ${params.sequenceNom}</p>
|
||||
<p><strong>Lieu :</strong> ${params.lieu}</p>
|
||||
<h4>Dates :</h4>
|
||||
${datesHTML}
|
||||
</div>
|
||||
|
||||
<p><strong>Merci de vous présenter à l'heure indiquée.</strong></p>
|
||||
@@ -268,14 +293,14 @@ export async function sendRappelJ7Email(params: {
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie des emails groupés à tous les inscrits d'une session
|
||||
* Envoie des emails groupés à tous les inscrits d'une séquence
|
||||
*/
|
||||
export async function sendGroupEmail(params: {
|
||||
recipients: Array<{ email: string; prenom: string; nom: string; fonction: string }>;
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
sequenceNom: string;
|
||||
type: 'teaser' | 'rappel';
|
||||
dateDebut: Date;
|
||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
||||
lieu?: string;
|
||||
}): Promise<{ sent: number; failed: number }> {
|
||||
let sent = 0;
|
||||
@@ -290,8 +315,8 @@ export async function sendGroupEmail(params: {
|
||||
apprenantNom: recipient.nom,
|
||||
apprenantFonction: recipient.fonction,
|
||||
formationNom: params.formationNom,
|
||||
sessionNom: params.sessionNom,
|
||||
dateDebut: params.dateDebut,
|
||||
sequenceNom: params.sequenceNom,
|
||||
dates: params.dates,
|
||||
});
|
||||
} else {
|
||||
await sendRappelJ7Email({
|
||||
@@ -300,8 +325,8 @@ export async function sendGroupEmail(params: {
|
||||
apprenantNom: recipient.nom,
|
||||
apprenantFonction: recipient.fonction,
|
||||
formationNom: params.formationNom,
|
||||
sessionNom: params.sessionNom,
|
||||
dateDebut: params.dateDebut,
|
||||
sequenceNom: params.sequenceNom,
|
||||
dates: params.dates,
|
||||
lieu: params.lieu || '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,23 +16,35 @@ interface InscriptionExport {
|
||||
dateInscription: Date;
|
||||
}
|
||||
|
||||
interface SessionInfo {
|
||||
interface SequenceInfo {
|
||||
formationNom: string;
|
||||
sessionNom: string;
|
||||
dateDebut: Date;
|
||||
dateFin: Date;
|
||||
sequenceNom: string;
|
||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
||||
lieu: string;
|
||||
inscriptions: InscriptionExport[];
|
||||
}
|
||||
|
||||
interface ApprenantPresence {
|
||||
nom: string;
|
||||
prenom: string;
|
||||
codeEtablissement: string;
|
||||
fonction: string;
|
||||
}
|
||||
|
||||
interface FeuillePresenceInfo {
|
||||
formationNom: string;
|
||||
sequenceNom: string;
|
||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
||||
lieu: string;
|
||||
apprenants: ApprenantPresence[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un fichier Excel avec la liste des inscrits
|
||||
*/
|
||||
export function generateExcelExport(
|
||||
sessionInfo: SessionInfo,
|
||||
inscriptions: InscriptionExport[]
|
||||
): Buffer {
|
||||
export function generateExcelExport(sequenceInfo: SequenceInfo): Buffer {
|
||||
// Préparer les données
|
||||
const data = inscriptions.map(i => ({
|
||||
const data = sequenceInfo.inscriptions.map(i => ({
|
||||
'Nom': i.nom,
|
||||
'Prénom': i.prenom,
|
||||
'Email': i.email,
|
||||
@@ -45,31 +57,42 @@ export function generateExcelExport(
|
||||
// Créer le workbook
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// Créer la feuille avec les informations de session
|
||||
// Créer la feuille avec les informations de séquence
|
||||
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()],
|
||||
['Formation', sequenceInfo.formationNom],
|
||||
['Séquence', sequenceInfo.sequenceNom],
|
||||
['Lieu', sequenceInfo.lieu],
|
||||
['Nombre de dates', sequenceInfo.dates.length.toString()],
|
||||
[],
|
||||
['DATES DE FORMATION'],
|
||||
];
|
||||
|
||||
// Ajouter les dates
|
||||
sequenceInfo.dates.forEach(date => {
|
||||
infoData.push([
|
||||
`Date ${date.ordre}`,
|
||||
`Du ${date.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})} au ${date.dateFin.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}`
|
||||
]);
|
||||
});
|
||||
|
||||
infoData.push([]);
|
||||
infoData.push(['Nombre d\'inscrits', sequenceInfo.inscriptions.length.toString()]);
|
||||
infoData.push([]);
|
||||
|
||||
// Créer la feuille principale
|
||||
const ws = XLSX.utils.aoa_to_sheet(infoData);
|
||||
|
||||
@@ -88,38 +111,45 @@ export function generateExcelExport(
|
||||
/**
|
||||
* Génère un fichier PDF avec la liste des inscrits
|
||||
*/
|
||||
export function generatePDFExport(
|
||||
sessionInfo: SessionInfo,
|
||||
inscriptions: InscriptionExport[]
|
||||
): Buffer {
|
||||
export function generatePDFExport(sequenceInfo: SequenceInfo): Buffer {
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Titre
|
||||
doc.setFontSize(18);
|
||||
doc.text('Liste des inscrits', 14, 20);
|
||||
|
||||
// Informations de session
|
||||
// Informations de séquence
|
||||
doc.setFontSize(10);
|
||||
let yPos = 35;
|
||||
doc.text(`Formation : ${sessionInfo.formationNom}`, 14, yPos);
|
||||
doc.text(`Formation : ${sequenceInfo.formationNom}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Session : ${sessionInfo.sessionNom}`, 14, yPos);
|
||||
doc.text(`Séquence : ${sequenceInfo.sequenceNom}`, 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);
|
||||
doc.text(`Lieu : ${sequenceInfo.lieu}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Lieu : ${sessionInfo.lieu}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Nombre d'inscrits : ${inscriptions.length}`, 14, yPos);
|
||||
doc.text(`Nombre de dates : ${sequenceInfo.dates.length}`, 14, yPos);
|
||||
yPos += 8;
|
||||
|
||||
// Dates de formation
|
||||
doc.setFontSize(9);
|
||||
sequenceInfo.dates.forEach(date => {
|
||||
doc.text(`Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}`, 14, yPos);
|
||||
yPos += 5;
|
||||
});
|
||||
|
||||
yPos += 3;
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Nombre d'inscrits : ${sequenceInfo.inscriptions.length}`, 14, yPos);
|
||||
|
||||
// Tableau des inscrits
|
||||
const tableData = inscriptions.map(i => [
|
||||
const tableData = sequenceInfo.inscriptions.map(i => [
|
||||
i.nom,
|
||||
i.prenom,
|
||||
i.email,
|
||||
@@ -142,37 +172,39 @@ export function generatePDFExport(
|
||||
/**
|
||||
* Génère une feuille de présence PDF
|
||||
*/
|
||||
export function generateFeuillePresence(
|
||||
sessionInfo: SessionInfo,
|
||||
inscriptions: InscriptionExport[]
|
||||
): Buffer {
|
||||
export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffer {
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Titre
|
||||
doc.setFontSize(18);
|
||||
doc.text('Feuille de présence', 14, 20);
|
||||
|
||||
// Informations de session
|
||||
// Informations de séquence
|
||||
doc.setFontSize(10);
|
||||
let yPos = 35;
|
||||
doc.text(`Formation : ${sessionInfo.formationNom}`, 14, yPos);
|
||||
doc.text(`Formation : ${feuilleInfo.formationNom}`, 14, yPos);
|
||||
yPos += 6;
|
||||
doc.text(`Session : ${sessionInfo.sessionNom}`, 14, yPos);
|
||||
doc.text(`Séquence : ${feuilleInfo.sequenceNom}`, 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);
|
||||
doc.text(`Lieu : ${feuilleInfo.lieu}`, 14, yPos);
|
||||
yPos += 8;
|
||||
|
||||
// Tableau de présence (uniquement les inscrits confirmés)
|
||||
const inscritsConfirmes = inscriptions.filter(i => i.statut === 'confirmee');
|
||||
const tableData = inscritsConfirmes.map(i => [
|
||||
// Dates de formation
|
||||
doc.setFontSize(9);
|
||||
feuilleInfo.dates.forEach(date => {
|
||||
doc.text(`Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}`, 14, yPos);
|
||||
yPos += 5;
|
||||
});
|
||||
|
||||
// Tableau de présence
|
||||
const tableData = feuilleInfo.apprenants.map(i => [
|
||||
i.nom,
|
||||
i.prenom,
|
||||
i.codeEtablissement,
|
||||
|
||||
@@ -32,7 +32,7 @@ export const appRouter = router({
|
||||
// ===== FORMATIONS =====
|
||||
formations: router({
|
||||
list: adminProcedure.query(async () => {
|
||||
return db.getAllFormations();
|
||||
return db.getFormations();
|
||||
}),
|
||||
|
||||
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
||||
@@ -74,7 +74,7 @@ export const appRouter = router({
|
||||
// ===== APPRENANTS =====
|
||||
apprenants: router({
|
||||
list: adminProcedure.query(async () => {
|
||||
return db.getAllApprenants();
|
||||
return db.getApprenants();
|
||||
}),
|
||||
|
||||
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
||||
@@ -115,75 +115,133 @@ export const appRouter = router({
|
||||
}),
|
||||
}),
|
||||
|
||||
// ===== SESSIONS =====
|
||||
sessions: router({
|
||||
// ===== SÉQUENCES =====
|
||||
sequences: router({
|
||||
list: adminProcedure.query(async () => {
|
||||
return db.getAllSessions();
|
||||
const seqs = await db.getSequences();
|
||||
// Récupérer les dates pour chaque séquence
|
||||
const sequencesAvecDates = await Promise.all(
|
||||
seqs.map(async (seq) => {
|
||||
const dates = await db.getDatesBySequence(seq.id);
|
||||
return { ...seq, dates };
|
||||
})
|
||||
);
|
||||
return sequencesAvecDates;
|
||||
}),
|
||||
|
||||
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);
|
||||
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 }) => {
|
||||
return db.getSessionById(input.id);
|
||||
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),
|
||||
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(),
|
||||
dates: z.array(z.object({
|
||||
dateDebut: z.string(),
|
||||
dateFin: z.string(),
|
||||
ordre: z.number(),
|
||||
})).min(1).max(4),
|
||||
})).mutation(async ({ input }) => {
|
||||
await db.createSession({
|
||||
...input,
|
||||
dateDebut: new Date(input.dateDebut),
|
||||
dateFin: new Date(input.dateFin),
|
||||
dateBlocage: new Date(input.dateBlocage),
|
||||
const { dates, ...sequenceData } = input;
|
||||
|
||||
// Créer la séquence
|
||||
const result = await db.createSequence({
|
||||
...sequenceData,
|
||||
dateBlocage: new Date(sequenceData.dateBlocage),
|
||||
});
|
||||
return { success: true };
|
||||
|
||||
// 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: new Date(date.dateDebut),
|
||||
dateFin: new Date(date.dateFin),
|
||||
ordre: date.ordre,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, sequenceId };
|
||||
}),
|
||||
|
||||
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(),
|
||||
dates: z.array(z.object({
|
||||
dateDebut: z.string(),
|
||||
dateFin: z.string(),
|
||||
ordre: z.number(),
|
||||
})).min(1).max(4).optional(),
|
||||
})).mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
const updateData: any = { ...data };
|
||||
const { id, dates, ...sequenceData } = input;
|
||||
|
||||
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);
|
||||
// Mettre à jour la séquence
|
||||
const updateData: any = { ...sequenceData };
|
||||
if (sequenceData.dateBlocage) {
|
||||
updateData.dateBlocage = new Date(sequenceData.dateBlocage);
|
||||
}
|
||||
|
||||
await db.updateSequence(id, updateData);
|
||||
|
||||
// Si des dates sont fournies, les mettre à jour
|
||||
if (dates) {
|
||||
// Supprimer les anciennes dates
|
||||
await db.deleteDatesBySequence(id);
|
||||
|
||||
// Créer les nouvelles dates
|
||||
for (const date of dates) {
|
||||
await db.createDateFormation({
|
||||
sequenceId: id,
|
||||
dateDebut: new Date(date.dateDebut),
|
||||
dateFin: new Date(date.dateFin),
|
||||
ordre: date.ordre,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await db.updateSession(id, updateData);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
||||
await db.deleteSession(input.id);
|
||||
// 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({
|
||||
listBySession: adminProcedure.input(z.object({ sessionId: z.number() })).query(async ({ input }) => {
|
||||
return db.getInscriptionsBySession(input.sessionId);
|
||||
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 }) => {
|
||||
@@ -192,65 +250,72 @@ export const appRouter = router({
|
||||
|
||||
checkExisting: publicProcedure.input(z.object({
|
||||
apprenantId: z.number(),
|
||||
sessionId: z.number(),
|
||||
sequenceId: z.number(),
|
||||
})).query(async ({ input }) => {
|
||||
return db.getInscriptionByApprenantAndSession(input.apprenantId, input.sessionId);
|
||||
return db.checkExistingInscription(input.apprenantId, input.sequenceId);
|
||||
}),
|
||||
|
||||
inscrire: publicProcedure.input(z.object({
|
||||
apprenantId: z.number(),
|
||||
sessionId: z.number(),
|
||||
sequenceId: 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 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 session est bloquée
|
||||
// Vérifier si la séquence est bloquée
|
||||
const now = new Date();
|
||||
if (session.statut === 'bloquee' || now >= new Date(session.dateBlocage)) {
|
||||
if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Les inscriptions sont fermées pour cette session (J-15 dépassé)'
|
||||
message: 'Les inscriptions sont fermées pour cette séquence (J-15 dépassé)'
|
||||
});
|
||||
}
|
||||
|
||||
// Vérifier si l'apprenant existe déjà une inscription
|
||||
const existing = await db.getInscriptionByApprenantAndSession(input.apprenantId, input.sessionId);
|
||||
// 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 session'
|
||||
message: 'Vous êtes déjà inscrit à cette séquence'
|
||||
});
|
||||
}
|
||||
|
||||
// Vérifier la capacité
|
||||
const nbInscrits = await db.countInscriptionsConfirmees(input.sessionId);
|
||||
const statut = nbInscrits >= session.capaciteMax ? 'liste_attente' : 'confirmee';
|
||||
const nbInscrits = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
|
||||
const statut = nbInscrits >= sequence.capaciteMax ? 'liste_attente' : 'confirmee';
|
||||
|
||||
await db.createInscription({
|
||||
apprenantId: input.apprenantId,
|
||||
sessionId: input.sessionId,
|
||||
sequenceId: input.sequenceId,
|
||||
statut,
|
||||
});
|
||||
|
||||
// Envoyer l'email de confirmation avec invitation Outlook
|
||||
const inscriptionSession = await db.getSessionById(input.sessionId);
|
||||
// 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 = inscriptionSession ? await db.getFormationById(inscriptionSession.formationId) : null;
|
||||
const inscriptionFormation = inscriptionSequence ? await db.getFormationById(inscriptionSequence.formationId) : null;
|
||||
const dates = inscriptionSequence ? await db.getDatesBySequence(inscriptionSequence.id) : [];
|
||||
|
||||
if (inscriptionSession && inscriptionApprenant && inscriptionFormation) {
|
||||
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,
|
||||
sessionNom: inscriptionSession.nom,
|
||||
dateDebut: new Date(inscriptionSession.dateDebut),
|
||||
dateFin: new Date(inscriptionSession.dateFin),
|
||||
lieu: inscriptionSession.lieu,
|
||||
sequenceNom: inscriptionSequence.nom,
|
||||
dates: dates.map(d => ({
|
||||
dateDebut: new Date(d.dateDebut),
|
||||
dateFin: new Date(d.dateFin),
|
||||
ordre: d.ordre,
|
||||
})),
|
||||
lieu: inscriptionSequence.lieu,
|
||||
statut,
|
||||
});
|
||||
}
|
||||
@@ -260,25 +325,25 @@ export const appRouter = router({
|
||||
|
||||
desinscrire: publicProcedure.input(z.object({
|
||||
apprenantId: z.number(),
|
||||
sessionId: z.number(),
|
||||
sequenceId: 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 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 session est bloquée
|
||||
// Vérifier si la séquence est bloquée
|
||||
const now = new Date();
|
||||
if (session.statut === 'bloquee' || now >= new Date(session.dateBlocage)) {
|
||||
if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Les désinscriptions sont fermées pour cette session (J-15 dépassé)'
|
||||
message: 'Les désinscriptions sont fermées pour cette séquence (J-15 dépassé)'
|
||||
});
|
||||
}
|
||||
|
||||
// Trouver l'inscription
|
||||
const inscription = await db.getInscriptionByApprenantAndSession(input.apprenantId, input.sessionId);
|
||||
const inscription = await db.checkExistingInscription(input.apprenantId, input.sequenceId);
|
||||
if (!inscription) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Inscription introuvable' });
|
||||
}
|
||||
@@ -296,21 +361,26 @@ export const appRouter = router({
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
sendGroupEmails: adminProcedure.input(z.object({
|
||||
sessionId: z.number(),
|
||||
sendGroupEmail: adminProcedure.input(z.object({
|
||||
sequenceId: 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 sequence = await db.getSequenceById(input.sequenceId);
|
||||
if (!sequence) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
||||
}
|
||||
|
||||
const formation = await db.getFormationById(session.formationId);
|
||||
const formation = await db.getFormationById(sequence.formationId);
|
||||
if (!formation) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
||||
}
|
||||
|
||||
const inscriptions = await db.getInscriptionsBySession(input.sessionId);
|
||||
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 => ({
|
||||
@@ -323,23 +393,29 @@ export const appRouter = router({
|
||||
const result = await sendGroupEmail({
|
||||
recipients,
|
||||
formationNom: formation.nom,
|
||||
sessionNom: session.nom,
|
||||
sequenceNom: sequence.nom,
|
||||
type: input.type,
|
||||
dateDebut: new Date(session.dateDebut),
|
||||
lieu: session.lieu,
|
||||
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({ sessionId: z.number() })).mutation(async ({ input }) => {
|
||||
const session = await db.getSessionById(input.sessionId);
|
||||
if (!session) throw new TRPCError({ code: 'NOT_FOUND', message: 'Session introuvable' });
|
||||
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(session.formationId);
|
||||
const formation = await db.getFormationById(sequence.formationId);
|
||||
if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
||||
|
||||
const inscriptions = await db.getInscriptionsBySession(input.sessionId);
|
||||
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
||||
const dates = await db.getDatesBySequence(input.sequenceId);
|
||||
|
||||
const data = inscriptions
|
||||
.filter(i => i.apprenant)
|
||||
.map(i => ({
|
||||
@@ -349,31 +425,37 @@ export const appRouter = router({
|
||||
codeEtablissement: i.apprenant!.codeEtablissement,
|
||||
fonction: i.apprenant!.fonction,
|
||||
statut: i.inscription.statut,
|
||||
dateInscription: new Date(i.inscription.dateInscription),
|
||||
dateInscription: 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
|
||||
);
|
||||
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,
|
||||
inscriptions: data,
|
||||
});
|
||||
|
||||
return { data: buffer.toString('base64') };
|
||||
return {
|
||||
buffer: buffer.toString('base64'),
|
||||
filename: `inscriptions_${sequence.nom.replace(/\s+/g, '_')}.xlsx`,
|
||||
};
|
||||
}),
|
||||
|
||||
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' });
|
||||
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(session.formationId);
|
||||
const formation = await db.getFormationById(sequence.formationId);
|
||||
if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
||||
|
||||
const inscriptions = await db.getInscriptionsBySession(input.sessionId);
|
||||
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
||||
const dates = await db.getDatesBySequence(input.sequenceId);
|
||||
|
||||
const data = inscriptions
|
||||
.filter(i => i.apprenant)
|
||||
.map(i => ({
|
||||
@@ -383,55 +465,62 @@ export const appRouter = router({
|
||||
codeEtablissement: i.apprenant!.codeEtablissement,
|
||||
fonction: i.apprenant!.fonction,
|
||||
statut: i.inscription.statut,
|
||||
dateInscription: new Date(i.inscription.dateInscription),
|
||||
dateInscription: 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
|
||||
);
|
||||
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,
|
||||
inscriptions: data,
|
||||
});
|
||||
|
||||
return { data: buffer.toString('base64') };
|
||||
return {
|
||||
buffer: buffer.toString('base64'),
|
||||
filename: `inscriptions_${sequence.nom.replace(/\s+/g, '_')}.pdf`,
|
||||
};
|
||||
}),
|
||||
|
||||
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' });
|
||||
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(session.formationId);
|
||||
const formation = await db.getFormationById(sequence.formationId);
|
||||
if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
||||
|
||||
const inscriptions = await db.getInscriptionsBySession(input.sessionId);
|
||||
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
||||
const dates = await db.getDatesBySequence(input.sequenceId);
|
||||
|
||||
const data = inscriptions
|
||||
.filter(i => i.apprenant)
|
||||
.filter(i => i.inscription.statut === 'confirmee' && 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
|
||||
);
|
||||
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,
|
||||
apprenants: data,
|
||||
});
|
||||
|
||||
return { data: buffer.toString('base64') };
|
||||
return {
|
||||
buffer: buffer.toString('base64'),
|
||||
filename: `feuille_presence_${sequence.nom.replace(/\s+/g, '_')}.pdf`,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user