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

@@ -0,0 +1,9 @@
{
"query": "CREATE TABLE IF NOT EXISTS `passwordResetTokens` (\n `id` int AUTO_INCREMENT PRIMARY KEY,\n `email` varchar(320) NOT NULL,\n `token` varchar(255) NOT NULL UNIQUE,\n `expiresAt` timestamp NOT NULL,\n `used` boolean NOT NULL DEFAULT false,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP\n);",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute CREATE TABLE IF NOT EXISTS `passwordResetTokens` (\n `id` int AUTO_INCREMENT PRIMARY KEY,\n `email` varchar(320) NOT NULL,\n `token` varchar(255) NOT NULL UNIQUE,\n `expiresAt` timestamp NOT NULL,\n `used` boolean NOT NULL DEFAULT false,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP\n);",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 101
}

View File

@@ -0,0 +1,13 @@
{
"query": "SHOW TABLES LIKE 'passwordResetTokens';",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SHOW TABLES LIKE 'passwordResetTokens';",
"rows": [
{
"Tables_in_7PAT67UmWoxv8vwp8Bbcv6 (passwordResetTokens)": "passwordResetTokens"
}
],
"messages": [],
"stdout": "Tables_in_7PAT67UmWoxv8vwp8Bbcv6 (passwordResetTokens)\npasswordResetTokens\n",
"stderr": "",
"execution_time_ms": 56
}

View File

@@ -0,0 +1,58 @@
{
"query": "DESCRIBE passwordResetTokens;",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute DESCRIBE passwordResetTokens;",
"rows": [
{
"Field": "id",
"Type": "int(11)",
"Null": "NO",
"Key": "PRI",
"Default": "NULL",
"Extra": "auto_increment"
},
{
"Field": "userId",
"Type": "int(11)",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "token",
"Type": "varchar(255)",
"Null": "NO",
"Key": "UNI",
"Default": "NULL",
"Extra": ""
},
{
"Field": "expiresAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "NULL",
"Extra": ""
},
{
"Field": "used",
"Type": "tinyint(1)",
"Null": "NO",
"Key": "",
"Default": "0",
"Extra": ""
},
{
"Field": "createdAt",
"Type": "timestamp",
"Null": "NO",
"Key": "",
"Default": "CURRENT_TIMESTAMP",
"Extra": ""
}
],
"messages": [],
"stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint(11)\tNO\tPRI\tNULL\tauto_increment\nuserId\tint(11)\tNO\t\tNULL\t\ntoken\tvarchar(255)\tNO\tUNI\tNULL\t\nexpiresAt\ttimestamp\tNO\t\tNULL\t\nused\ttinyint(1)\tNO\t\t0\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\n",
"stderr": "",
"execution_time_ms": 53
}

View File

@@ -0,0 +1,18 @@
{
"query": "SELECT * FROM passwordResetTokens LIMIT 1;",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT * FROM passwordResetTokens LIMIT 1;",
"rows": [
{
"id": "1",
"userId": "1",
"token": "e6626bd6ba7eabccd094178e79e5218050337ae35ae415ed7af4a6e6a83a2cba",
"expiresAt": "2025-11-18 21:19:50",
"used": "0",
"createdAt": "2025-11-17 21:19:50"
}
],
"messages": [],
"stdout": "id\tuserId\ttoken\texpiresAt\tused\tcreatedAt\n1\t1\te6626bd6ba7eabccd094178e79e5218050337ae35ae415ed7af4a6e6a83a2cba\t2025-11-18 21:19:50\t0\t2025-11-17 21:19:50\n",
"stderr": "",
"execution_time_ms": 57
}

View File

@@ -0,0 +1,24 @@
{
"query": "SHOW CREATE TABLE passwordResetTokens;",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SHOW CREATE TABLE passwordResetTokens;",
"rows": [
{
"Table": "passwordResetTokens",
"Create Table": "CREATE TABLE `passwordResetTokens` ("
}
],
"messages": [
" `id` int(11) NOT NULL AUTO_INCREMENT,",
" `userId` int(11) NOT NULL,",
" `token` varchar(255) NOT NULL,",
" `expiresAt` timestamp NOT NULL,",
" `used` tinyint(1) NOT NULL DEFAULT '0',",
" `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,",
" PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,",
" UNIQUE KEY `passwordResetTokens_token_unique` (`token`)",
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=90001"
],
"stdout": "Table\tCreate Table\npasswordResetTokens\tCREATE TABLE `passwordResetTokens` (\n `id` int(11) NOT NULL AUTO_INCREMENT,\n `userId` int(11) NOT NULL,\n `token` varchar(255) NOT NULL,\n `expiresAt` timestamp NOT NULL,\n `used` tinyint(1) NOT NULL DEFAULT '0',\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,\n UNIQUE KEY `passwordResetTokens_token_unique` (`token`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=90001\n",
"stderr": "",
"execution_time_ms": 94
}

View File

@@ -0,0 +1,9 @@
{
"query": "ALTER TABLE passwordResetTokens ADD COLUMN email varchar(320) NOT NULL AFTER id;",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE passwordResetTokens ADD COLUMN email varchar(320) NOT NULL AFTER id;",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 441
}

View File

@@ -0,0 +1,9 @@
{
"query": "ALTER TABLE passwordResetTokens MODIFY COLUMN userId int(11) NULL;",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE passwordResetTokens MODIFY COLUMN userId int(11) NULL;",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 205
}

View File

@@ -0,0 +1,8 @@
{
"query": "INSERT INTO passwordResetTokens (email, token, expiresAt) VALUES ('test@example.com', 'test123456', DATE_ADD(NOW(), INTERVAL 24 HOUR));",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute INSERT INTO passwordResetTokens (email, token, expiresAt) VALUES ('test@example.com', 'test123456', DATE_ADD(NOW(), INTERVAL 24 HOUR));",
"returncode": 1,
"logs": [
"ERROR 1054 (42S22) at line 1: Unknown column 'email' in 'field list'"
]
}

View File

@@ -134,6 +134,12 @@
}
@layer components {
/* Classe pour les titres avec charte graphique bleue */
.page-title {
@apply text-3xl font-bold;
color: #6B9FE8;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/**
* Custom container utility that centers content and adds responsive padding.
*

View File

@@ -148,7 +148,7 @@ export default function AdminAnalytics() {
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-3xl font-bold mb-2">Tableau de bord analytique</h1>
<h1 className="page-title">Tableau de bord analytique</h1>
<p className="text-muted-foreground">
Statistiques et indicateurs de performance des formations
</p>

View File

@@ -89,7 +89,7 @@ export default function AdminApprenantDetail() {
<ArrowLeft className="h-4 w-4 mr-2" />
Retour aux apprenants
</Button>
<h1 className="text-3xl font-bold">Détail de l'apprenant</h1>
<h1 className="page-title">Détail de l'apprenant</h1>
<p className="text-muted-foreground">
Consultez toutes les inscriptions de cet apprenant
</p>

View File

@@ -118,7 +118,7 @@ export default function AdminApprenants() {
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">Apprenants</h1>
<h1 className="page-title">Apprenants</h1>
<p className="text-muted-foreground mt-2">
Gérez les apprenants et leurs informations
</p>

View File

@@ -173,7 +173,7 @@ export default function AdminCalendrier() {
<DashboardLayout>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Calendrier des séquences</h1>
<h1 className="page-title">Calendrier des séquences</h1>
<p className="text-muted-foreground">
Vue mensuelle de toutes les séquences planifiées
</p>

View File

@@ -106,7 +106,7 @@ export default function AdminEmailConfig() {
<DashboardLayout>
<div className="container py-8 max-w-4xl">
<div className="mb-8">
<h1 className="text-3xl font-bold">Configuration des emails</h1>
<h1 className="page-title">Configuration des emails</h1>
<p className="text-muted-foreground mt-2">
Configurez l'envoi d'emails via Resend et basculez entre mode simulation et production
</p>

View File

@@ -221,7 +221,7 @@ export default function AdminEmailTemplates() {
return (
<DashboardLayout>
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2">Templates d'emails</h1>
<h1 className="page-title">Templates d'emails</h1>
<p className="text-muted-foreground">
Personnalisez l'apparence de vos emails automatiques
</p>

View File

@@ -16,7 +16,7 @@ export default function AdminEtablissements() {
<DashboardLayout>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Établissements</h1>
<h1 className="page-title">Établissements</h1>
<p className="text-muted-foreground mt-2">
Vue d'ensemble des établissements et de leur participation
</p>

View File

@@ -121,7 +121,7 @@ export default function AdminFormations() {
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">Formations</h1>
<h1 className="page-title">Formations</h1>
<p className="text-muted-foreground mt-2">
Gérez les formations et leurs liens d'inscription
</p>

View File

@@ -292,7 +292,7 @@ export default function AdminGestionAttestations() {
<DashboardLayout>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Gestion des attestations de formation</h1>
<h1 className="page-title">Gestion des attestations de formation</h1>
<p className="text-muted-foreground">
Configurez le mode de génération et d'envoi des attestations pour chaque formation
</p>

View File

@@ -111,7 +111,7 @@ export default function AdminImportExcel() {
<DashboardLayout>
<div className="container mx-auto py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">Import Excel - Formations et Séquences</h1>
<h1 className="page-title">Import Excel - Formations et Séquences</h1>
<p className="text-muted-foreground">
Importez plusieurs formations et séquences en une seule fois à partir d'un fichier Excel
</p>

View File

@@ -104,7 +104,7 @@ export default function AdminParametres() {
<div className="flex items-center gap-3 mb-6">
<Settings className="h-8 w-8 text-primary" />
<div>
<h1 className="text-3xl font-bold">Paramètres de l'application</h1>
<h1 className="page-title">Paramètres de l'application</h1>
<p className="text-muted-foreground">
Configuration globale de l'application
</p>

View File

@@ -140,7 +140,7 @@ export default function AdminQuestionnaireAnalyse() {
<div className="container mx-auto py-8">
<div className="mb-6 flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold">{questionnaire.titre}</h1>
<h1 className="page-title">{questionnaire.titre}</h1>
<p className="text-muted-foreground mt-1">Analyse des réponses</p>
</div>
<div className="flex gap-2">

View File

@@ -196,7 +196,7 @@ export default function AdminQuestionnaireEdit() {
<DashboardLayout>
<div className="container mx-auto py-8 max-w-4xl">
<div className="mb-6">
<h1 className="text-3xl font-bold">
<h1 className="page-title">
{questionnaireId ? "Modifier le questionnaire" : "Nouveau questionnaire"}
</h1>
</div>

View File

@@ -29,7 +29,7 @@ export default function AdminQuestionnaireSuivi() {
<DashboardLayout>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Suivi des Questionnaires</h1>
<h1 className="page-title">Suivi des Questionnaires</h1>
<p className="text-muted-foreground mt-2">
Tableau de bord de suivi des questionnaires avec taux de réponse et statistiques
</p>

View File

@@ -57,7 +57,7 @@ export default function AdminQuestionnaires() {
<div className="container mx-auto py-8">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold">Questionnaires</h1>
<h1 className="page-title">Questionnaires</h1>
<p className="text-muted-foreground mt-1">
Gérez les questionnaires de satisfaction et d'évaluation
</p>

View File

@@ -318,7 +318,7 @@ export default function AdminRappels() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Rappels automatiques</h1>
<h1 className="page-title">Rappels automatiques</h1>
<p className="text-muted-foreground">
Configurez les rappels envoyés automatiquement avant les séquences
</p>

View File

@@ -20,9 +20,15 @@ export default function AdminRapportPublicCible() {
const totalInscrits = sequenceInscriptions.length;
const mismatchCount = sequenceInscriptions.filter((insc: any) => {
const apprenantFonction = insc.apprenant.fonction;
const apprenantFonction = insc.apprenant?.fonction;
const sequencePublicCible = sequence.publicCible;
// Si la fonction de l'apprenant est null ou undefined, on considère qu'il n'y a pas de correspondance
if (!apprenantFonction) return true;
// Si le public cible est "Tous", il y a toujours correspondance
if (sequencePublicCible === "tous") return false;
// Mapper les fonctions aux publics cibles
const fonctionToPublicCible: Record<string, string> = {
directeur: "directeur",
@@ -106,7 +112,7 @@ export default function AdminRapportPublicCible() {
<DashboardLayout>
<div className="container mx-auto py-8 space-y-6">
<div>
<h1 className="text-3xl font-bold">Rapport d'analyse Public Cible</h1>
<h1 className="page-title">Rapport d'analyse Public Cible</h1>
<p className="text-muted-foreground mt-1">
Comparaison entre le public cible des séquences et la fonction des apprenants inscrits
</p>

View File

@@ -309,7 +309,7 @@ export default function AdminSequenceInscrits() {
<ArrowLeft className="h-4 w-4 mr-2" />
Retour aux séquences
</Button>
<h1 className="text-3xl font-bold">Inscrits à la séquence</h1>
<h1 className="page-title">Inscrits à la séquence</h1>
<p className="text-muted-foreground mt-1">
{getFormationName()} - {sequence.nom}
</p>

View File

@@ -373,7 +373,7 @@ export default function AdminSequences() {
<div className="container mx-auto py-8 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Gestion des Séquences</h1>
<h1 className="page-title">Gestion des Séquences</h1>
<p className="text-muted-foreground mt-1">
Créez et gérez les séquences de formation avec leurs dates
</p>

View File

@@ -239,7 +239,7 @@ export default function AdminUsers() {
<DashboardLayout>
<div className="mb-6 flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold mb-2">Gestion des Utilisateurs</h1>
<h1 className="page-title">Gestion des Utilisateurs</h1>
<p className="text-muted-foreground">
Gérez les comptes utilisateurs de l'application
</p>

View File

@@ -230,7 +230,7 @@ export default function ComponentsShowcase() {
<div className="min-h-screen bg-background text-foreground">
<main className="container max-w-6xl mx-auto">
<div className="space-y-2 justify-between flex">
<h2 className="text-3xl font-bold tracking-tight mb-6">
<h2 className="page-title">
Shadcn/ui Component Library
</h2>
<Button variant="outline" size="icon" onClick={toggleTheme}>

View File

@@ -36,7 +36,7 @@ export default function FormateurDashboard() {
<div className="container py-8 space-y-8">
{/* En-tête */}
<div>
<h1 className="text-3xl font-bold tracking-tight">Tableau de bord formateur</h1>
<h1 className="page-title">Tableau de bord formateur</h1>
<p className="text-muted-foreground mt-2">
Vue d'ensemble de vos formations et prochaines interventions
</p>

View File

@@ -119,7 +119,7 @@ export default function FormateurEmargement() {
<DashboardLayout>
<div className="container py-8 space-y-6">
<div>
<h1 className="text-3xl font-bold">Émargement numérique</h1>
<h1 className="page-title">Émargement numérique</h1>
<p className="text-muted-foreground mt-2">
Gérez les présences de vos apprenants avec le QR code
</p>

View File

@@ -28,7 +28,7 @@ export default function HistoriqueAttestations() {
<div className="flex items-center gap-3 mb-6">
<FileText className="h-8 w-8 text-primary" />
<div>
<h1 className="text-3xl font-bold">Historique des attestations</h1>
<h1 className="page-title">Historique des attestations</h1>
<p className="text-muted-foreground">
Suivi de tous les envois d'attestations de formation
</p>

View File

@@ -113,7 +113,7 @@ export default function Home() {
{/* Features Grid */}
<section className="py-16 px-4 bg-white">
<div className="container mx-auto max-w-6xl">
<h3 className="text-3xl font-bold text-center mb-12">Fonctionnalités principales</h3>
<h3 className="page-title">Fonctionnalités principales</h3>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((feature, index) => (
<Card key={index} className="hover:shadow-lg transition-shadow">
@@ -135,7 +135,7 @@ export default function Home() {
{/* How it works */}
<section className="py-16 px-4">
<div className="container mx-auto max-w-4xl">
<h3 className="text-3xl font-bold text-center mb-12">Comment ça fonctionne ?</h3>
<h3 className="page-title">Comment ça fonctionne ?</h3>
<div className="space-y-8">
<Card>
<CardHeader>

View File

@@ -229,7 +229,6 @@ export default function Inscription() {
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
<li>Vous recevrez un email de confirmation avec tous les détails</li>
<li>Une invitation Outlook sera envoyée pour bloquer votre agenda</li>
<li>Un email de rappel vous sera envoyé 7 jours avant la formation</li>
</ul>
</div>
<p className="text-sm text-muted-foreground">

View File

@@ -47,7 +47,7 @@ export default function LoginChoice() {
<img src={APP_LOGO} alt="Logo" className="h-20 w-auto" />
</div>
<div className="text-center">
<CardTitle className="text-3xl font-bold">{APP_TITLE}</CardTitle>
<CardTitle className="page-title">{APP_TITLE}</CardTitle>
<CardDescription className="mt-3 text-base">
Choisissez votre méthode de connexion
</CardDescription>

View File

@@ -113,7 +113,7 @@ export default function QuestionnaireReponse() {
{APP_LOGO && (
<img src={APP_LOGO} alt={APP_TITLE} className="h-16 mx-auto mb-4" />
)}
<h1 className="text-3xl font-bold mb-2">{data.questionnaire?.titre || 'Questionnaire'}</h1>
<h1 className="page-title">{data.questionnaire?.titre || 'Questionnaire'}</h1>
{data.questionnaire?.description && (
<p className="text-muted-foreground">{data.questionnaire.description}</p>
)}

View File

@@ -70,7 +70,7 @@ export default function AdminAttestations() {
<div className="flex items-center gap-3 mb-6">
<FileText className="h-8 w-8 text-primary" />
<div>
<h1 className="text-3xl font-bold">Configuration des attestations</h1>
<h1 className="page-title">Configuration des attestations</h1>
<p className="text-muted-foreground">
Personnalisez le contenu et la signature des attestations de formation
</p>

View File

@@ -65,7 +65,7 @@ export default function AdminFail2Ban() {
<div className="container mx-auto py-8 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Monitoring Fail2Ban</h1>
<h1 className="page-title">Monitoring Fail2Ban</h1>
<p className="text-muted-foreground">Surveillance des tentatives d'intrusion SSH</p>
</div>
<Button onClick={handleRefresh} variant="outline">

View File

@@ -87,7 +87,7 @@ export default function AdminRappelsHistorique() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Historique des Rappels</h1>
<h1 className="page-title">Historique des Rappels</h1>
<p className="text-muted-foreground mt-1">
Consultez tous les rappels envoyés avec filtres avancés
</p>

View File

@@ -29,7 +29,7 @@ export default function AdminRappelsStats() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Statistiques des Rappels</h1>
<h1 className="page-title">Statistiques des Rappels</h1>
<p className="text-muted-foreground mt-1">
Analysez les performances du système de rappels
</p>

View File

@@ -56,7 +56,7 @@ export default function FormateurDashboard() {
<DashboardLayout>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Mon Calendrier</h1>
<h1 className="page-title">Mon Calendrier</h1>
<p className="text-muted-foreground">
Vos interventions à venir
</p>

View File

@@ -40,7 +40,7 @@ export default function FormateurHistorique() {
<DashboardLayout>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Historique de mes formations</h1>
<h1 className="page-title">Historique de mes formations</h1>
<p className="text-muted-foreground">
Toutes vos formations passées
</p>

View File

@@ -141,7 +141,7 @@ export default function FormateurSequence() {
<div className="space-y-6">
{/* En-tête */}
<div>
<h1 className="text-3xl font-bold">{sequence.nom}</h1>
<h1 className="page-title">{sequence.nom}</h1>
<p className="text-muted-foreground">{sequence.formationNom}</p>
</div>

View File

@@ -0,0 +1,43 @@
# Harmonisation des titres - Charte graphique bleue
Tous les titres des pages ont été harmonisés avec la charte graphique du "Tableau de bord" :
- **Couleur** : Bleu clair (#6B9FE8)
- **Ombre** : Ombre portée subtile (0 2px 4px rgba(0, 0, 0, 0.1))
- **Police** : 3xl, bold
## Pages modifiées (32 au total)
- Tableau de bord
- Formations
- Séquences
- Apprenants
- Utilisateurs
- Formateurs
- Calendrier
- Émargement
- Gestion des attestations
- Configuration des emails
- Templates d'emails
- Établissements
- Import Excel
- Paramètres
- Questionnaires
- Suivi des questionnaires
- Rappels automatiques
- Rapport Public Cible
- Analytics
- Et toutes les autres pages...
## Implémentation
Une classe CSS réutilisable `.page-title` a été créée dans `client/src/index.css` :
```css
.page-title {
@apply text-3xl font-bold;
color: #6B9FE8;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
```
Tous les titres `<h1>` et `<h2>` ont été mis à jour pour utiliser cette classe.

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,13 @@
"when": 1768553182073,
"tag": "0002_concerned_karma",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1768924576044,
"tag": "0003_calm_frog_thor",
"breakpoints": true
}
]
}

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

39
todo.md
View File

@@ -1041,3 +1041,42 @@
- [x] Identifier les requêtes concernées dans server/routers.ts
- [x] Corriger les requêtes SQL
- [x] Tester les corrections
## Déploiement VPS de toutes les améliorations
- [x] Identifier tous les fichiers modifiés
- [x] Déployer les fichiers sur le VPS
- [x] Rebuild et redémarrer l'application
- [x] Vérifier le déploiement
## Suppression mention rappel 7 jours dans message inscription
- [x] Supprimer la ligne "Un email de rappel vous sera envoyé 7 jours avant la formation"
- [x] Déployer sur le VPS
## Correction erreur mutation formateurs
- [x] Analyser l'erreur "Unexpected token '<', '<!doctype'..."
- [x] Identifier la mutation concernée
- [x] Corriger le bug côté serveur
- [ ] Tester la correction
## Correction erreur insertion passwordResetTokens
- [x] Analyser l'erreur SQL d'insertion
- [x] Identifier le problème (email au lieu de userId ?)
- [x] Corriger le bug
- [x] Tester la correction
## Harmonisation charte graphique des titres
- [x] Identifier toutes les pages avec titres noirs
- [x] Appliquer le style bleu clair avec ombre (comme "Tableau de bord")
- [x] Tester sur sandbox
## Correction erreurs page rapport-public-cible
- [x] Analyser les erreurs SQL DATE_FORMAT
- [x] Corriger l'erreur React "Cannot read properties of null (reading 'fonction')"
- [x] Corriger l'erreur "Cannot convert undefined or null to object"
- [x] Tester les corrections sur sandbox