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:
9
.manus/db/db-query-1768924734065.json
Normal file
9
.manus/db/db-query-1768924734065.json
Normal 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
|
||||
}
|
||||
13
.manus/db/db-query-1768925512631.json
Normal file
13
.manus/db/db-query-1768925512631.json
Normal 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
|
||||
}
|
||||
58
.manus/db/db-query-1768925517071.json
Normal file
58
.manus/db/db-query-1768925517071.json
Normal 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
|
||||
}
|
||||
18
.manus/db/db-query-1768925534190.json
Normal file
18
.manus/db/db-query-1768925534190.json
Normal 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
|
||||
}
|
||||
24
.manus/db/db-query-1768925802156.json
Normal file
24
.manus/db/db-query-1768925802156.json
Normal 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
|
||||
}
|
||||
9
.manus/db/db-query-1768925813992.json
Normal file
9
.manus/db/db-query-1768925813992.json
Normal 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
|
||||
}
|
||||
9
.manus/db/db-query-1768925878549.json
Normal file
9
.manus/db/db-query-1768925878549.json
Normal 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
|
||||
}
|
||||
8
.manus/db/db-query-error-1768925502688.json
Normal file
8
.manus/db/db-query-error-1768925502688.json
Normal 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'"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
43
docs/screenshot-titres-bleus.md
Normal file
43
docs/screenshot-titres-bleus.md
Normal 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.
|
||||
2163
drizzle/meta/0003_snapshot.json
Normal file
2163
drizzle/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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')`);
|
||||
|
||||
|
||||
14
server/db.ts
14
server/db.ts
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
39
todo.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user