Checkpoint: Trois améliorations majeures pour la gestion des séquences :

 **1. Indicateur visuel de remplissage**
- Composant CapacityProgressBar avec barre de progression colorée
- Affichage "X / Y" avec pourcentage
- Couleurs automatiques : vert (<70%), orange (70-90%), rouge (>90%)
- Intégré dans la colonne capacité du tableau des séquences

 **2. Vue calendrier mensuel**
- Nouvelle page AdminCalendrier accessible depuis le menu
- Calendrier mensuel avec navigation (mois précédent/suivant, aujourd'hui)
- Affichage des séquences sur leurs dates respectives
- Dialog de détails au clic sur une séquence
- Vue complète avec toutes les informations (lieu, formateur, capacité, dates)

 **3. Système de rappels automatiques personnalisables**
- Tables rappels et rappelsEnvoyes créées
- Procédures tRPC complètes (list, create, update, delete)
- Page AdminRappels avec interface de gestion
- Configuration de rappels multiples (J-15, J-7, J-1, etc.)
- Templates d'emails personnalisables avec variables dynamiques
- Activation/désactivation des rappels
- Infrastructure prête pour l'envoi automatique (nécessite service de planification externe)

Ces améliorations facilitent grandement la planification et le suivi des formations.
This commit is contained in:
Manus Sandbox
2025-11-19 09:59:37 -05:00
parent 16351ea3d5
commit 4bf589dde3
15 changed files with 1778 additions and 4 deletions

View File

@@ -20,6 +20,7 @@ import {
InsertFormation,
InsertApprenant,
InsertFormateur,
InsertRappel,
Sequence,
DateFormation,
Apprenant,
@@ -693,3 +694,86 @@ export async function deleteFormateur(id: number) {
await db.update(formateurs).set({ actif: false }).where(eq(formateurs.id, id));
}
// ===== RAPPELS =====
/**
* Récupérer tous les rappels
*/
export async function getRappels() {
const db = await getDb();
if (!db) return [];
const { rappels } = await import("../drizzle/schema");
const result = await db.select().from(rappels);
return result;
}
/**
* Créer un nouveau rappel
*/
export async function createRappel(data: InsertRappel) {
const db = await getDb();
if (!db) return null;
const { rappels } = await import("../drizzle/schema");
const result = await db.insert(rappels).values(data);
return result;
}
/**
* Mettre à jour un rappel
*/
export async function updateRappel(id: number, data: Partial<InsertRappel>) {
const db = await getDb();
if (!db) return;
const { rappels } = await import("../drizzle/schema");
await db.update(rappels).set({
...data,
updatedAt: new Date(),
}).where(eq(rappels.id, id));
}
/**
* Supprimer un rappel
*/
export async function deleteRappel(id: number) {
const db = await getDb();
if (!db) return;
const { rappels } = await import("../drizzle/schema");
await db.delete(rappels).where(eq(rappels.id, id));
}
/**
* Enregistrer l'envoi d'un rappel
*/
export async function enregistrerRappelEnvoye(rappelId: number, inscriptionId: number) {
const db = await getDb();
if (!db) return;
const { rappelsEnvoyes } = await import("../drizzle/schema");
await db.insert(rappelsEnvoyes).values({
rappelId,
inscriptionId,
});
}
/**
* Vérifier si un rappel a déjà été envoyé
*/
export async function rappelDejaEnvoye(rappelId: number, inscriptionId: number) {
const db = await getDb();
if (!db) return false;
const { rappelsEnvoyes } = await import("../drizzle/schema");
const result = await db.select().from(rappelsEnvoyes)
.where(and(
eq(rappelsEnvoyes.rappelId, rappelId),
eq(rappelsEnvoyes.inscriptionId, inscriptionId)
))
.limit(1);
return result.length > 0;
}

View File

@@ -690,6 +690,40 @@ export const appRouter = router({
}),
}),
// ===== RAPPELS =====
rappels: router({
list: adminProcedure.query(async () => {
return db.getRappels();
}),
create: adminProcedure.input(z.object({
nom: z.string().min(1),
joursAvant: z.number().min(1),
emailTemplate: z.string().min(1),
actif: z.boolean().default(true),
})).mutation(async ({ input }) => {
await db.createRappel(input);
return { success: true };
}),
update: adminProcedure.input(z.object({
id: z.number(),
nom: z.string().min(1).optional(),
joursAvant: z.number().min(1).optional(),
emailTemplate: z.string().min(1).optional(),
actif: z.boolean().optional(),
})).mutation(async ({ input }) => {
const { id, ...data } = input;
await db.updateRappel(id, data);
return { success: true };
}),
delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
await db.deleteRappel(input.id);
return { success: true };
}),
}),
emailConfig: router({
get: adminProcedure.query(async () => {
return db.getActiveEmailConfig();