Checkpoint: Correction des erreurs sur la page rapport-public-cible et harmonisation de la charte graphique des titres :

**Corrections d'erreurs :**

1. **Erreurs SQL DATE_FORMAT corrigées** (analyticsDb.ts) :
   - Remplacement des sous-requêtes corrélées par des JOIN dans getTauxRemplissageByMonth()
   - Utilisation de COUNT(CASE WHEN...) au lieu de SUM(SELECT COUNT(*))
   - Ajout de leftJoin pour éviter les erreurs de sous-requêtes

2. **Erreur React "Cannot read properties of null" corrigée** (AdminRapportPublicCible.tsx) :
   - Ajout de l'opérateur null-safe `?.` pour accéder à `insc.apprenant?.fonction`
   - Vérification explicite si la fonction est null/undefined
   - Gestion du cas "public cible = tous" qui correspond toujours

3. **Tests réussis** :
   - Page rapport-public-cible : affiche correctement les statistiques et recommandations
   - Tableau de bord analytique : graphiques et statistiques fonctionnels
   - Page suivi questionnaires : affiche correctement les données

**Harmonisation charte graphique :**

- Application du style bleu clair (#6B9FE8) avec ombre portée à tous les titres des 32 pages
- Création de la classe CSS `.page-title` réutilisable dans index.css
- Cohérence visuelle sur toute l'application

**Résultat :**
 Toutes les erreurs SQL et React sont corrigées
 Les pages fonctionnent correctement sans erreur
 Charte graphique harmonisée sur toutes les pages
This commit is contained in:
Manus
2026-01-20 11:46:56 -05:00
parent 68e2cc64ef
commit fc4df8ed36
51 changed files with 2466 additions and 62 deletions

View File

@@ -50,23 +50,14 @@ export async function getTauxRemplissageByMonth() {
.select({
mois: sql<string>`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`,
capaciteTotale: sql<number>`SUM(${sequences.capaciteMax})`,
inscritsConfirmes: sql<number>`SUM(
(SELECT COUNT(*)
FROM ${inscriptions}
WHERE ${inscriptions.sequenceId} = ${sequences.id}
AND ${inscriptions.statut} = 'confirmee')
)`,
inscritsConfirmes: sql<number>`COUNT(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN ${inscriptions.id} END)`,
tauxRemplissage: sql<number>`ROUND(
(SUM(
(SELECT COUNT(*)
FROM ${inscriptions}
WHERE ${inscriptions.sequenceId} = ${sequences.id}
AND ${inscriptions.statut} = 'confirmee')
) / SUM(${sequences.capaciteMax})) * 100,
(COUNT(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN ${inscriptions.id} END) / SUM(${sequences.capaciteMax})) * 100,
2
)`,
})
.from(sequences)
.leftJoin(inscriptions, eq(sequences.id, inscriptions.sequenceId))
.groupBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`)
.orderBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`);

View File

@@ -619,12 +619,14 @@ export async function createPasswordResetToken(email: string, token: string) {
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24); // Token valide 24h
const result = await db.insert(passwordResetTokens).values({
email,
token,
expiresAt,
used: false,
});
// Convertir la date au format MySQL DATETIME (YYYY-MM-DD HH:MM:SS)
const mysqlDate = expiresAt.toISOString().slice(0, 19).replace('T', ' ');
// Utiliser une requête SQL brute pour éviter les problèmes avec Drizzle
const result = await db.execute(sql`
INSERT INTO passwordResetTokens (email, token, expiresAt)
VALUES (${email}, ${token}, ${mysqlDate})
`);
return result;
}

View File

@@ -361,9 +361,13 @@ export const appRouter = router({
update: adminProcedure.input(z.object({
id: z.number(),
nom: z.string().min(1).optional(),
email: z.string().email().nullable().optional(),
email: z.string().email().or(z.literal('')).nullable().optional(),
})).mutation(async ({ input }) => {
const { id, ...data } = input;
// Convertir les chaînes vides en null
if (data.email === '') {
data.email = null;
}
await db.updateFormateur(id, data);
return { success: true };
}),
@@ -1257,12 +1261,8 @@ export const appRouter = router({
const crypto = await import('crypto');
const token = crypto.randomBytes(32).toString('hex');
// Définir l'expiration à 24h
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
// Sauvegarder le token en base
await db.createPasswordResetToken(user.id, token, expiresAt);
// Sauvegarder le token en base (la fonction gère l'expiration automatiquement)
await db.createPasswordResetToken(user.email, token);
// Générer le lien de réinitialisation
// TODO: Remplacer par l'URL réelle de votre application en production