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 {
|
@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.
|
* Custom container utility that centers content and adds responsive padding.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export default function AdminAnalytics() {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground">
|
||||||
Statistiques et indicateurs de performance des formations
|
Statistiques et indicateurs de performance des formations
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function AdminApprenantDetail() {
|
|||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
Retour aux apprenants
|
Retour aux apprenants
|
||||||
</Button>
|
</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">
|
<p className="text-muted-foreground">
|
||||||
Consultez toutes les inscriptions de cet apprenant
|
Consultez toutes les inscriptions de cet apprenant
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export default function AdminApprenants() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Apprenants</h1>
|
<h1 className="page-title">Apprenants</h1>
|
||||||
<p className="text-muted-foreground mt-2">
|
<p className="text-muted-foreground mt-2">
|
||||||
Gérez les apprenants et leurs informations
|
Gérez les apprenants et leurs informations
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export default function AdminCalendrier() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground">
|
||||||
Vue mensuelle de toutes les séquences planifiées
|
Vue mensuelle de toutes les séquences planifiées
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function AdminEmailConfig() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="container py-8 max-w-4xl">
|
<div className="container py-8 max-w-4xl">
|
||||||
<div className="mb-8">
|
<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">
|
<p className="text-muted-foreground mt-2">
|
||||||
Configurez l'envoi d'emails via Resend et basculez entre mode simulation et production
|
Configurez l'envoi d'emails via Resend et basculez entre mode simulation et production
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export default function AdminEmailTemplates() {
|
|||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="mb-6">
|
<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">
|
<p className="text-muted-foreground">
|
||||||
Personnalisez l'apparence de vos emails automatiques
|
Personnalisez l'apparence de vos emails automatiques
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function AdminEtablissements() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Établissements</h1>
|
<h1 className="page-title">Établissements</h1>
|
||||||
<p className="text-muted-foreground mt-2">
|
<p className="text-muted-foreground mt-2">
|
||||||
Vue d'ensemble des établissements et de leur participation
|
Vue d'ensemble des établissements et de leur participation
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default function AdminFormations() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Formations</h1>
|
<h1 className="page-title">Formations</h1>
|
||||||
<p className="text-muted-foreground mt-2">
|
<p className="text-muted-foreground mt-2">
|
||||||
Gérez les formations et leurs liens d'inscription
|
Gérez les formations et leurs liens d'inscription
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export default function AdminGestionAttestations() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground">
|
||||||
Configurez le mode de génération et d'envoi des attestations pour chaque formation
|
Configurez le mode de génération et d'envoi des attestations pour chaque formation
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export default function AdminImportExcel() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="container mx-auto py-8">
|
<div className="container mx-auto py-8">
|
||||||
<div className="mb-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">
|
<p className="text-muted-foreground">
|
||||||
Importez plusieurs formations et séquences en une seule fois à partir d'un fichier Excel
|
Importez plusieurs formations et séquences en une seule fois à partir d'un fichier Excel
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function AdminParametres() {
|
|||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<Settings className="h-8 w-8 text-primary" />
|
<Settings className="h-8 w-8 text-primary" />
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground">
|
||||||
Configuration globale de l'application
|
Configuration globale de l'application
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export default function AdminQuestionnaireAnalyse() {
|
|||||||
<div className="container mx-auto py-8">
|
<div className="container mx-auto py-8">
|
||||||
<div className="mb-6 flex justify-between items-start">
|
<div className="mb-6 flex justify-between items-start">
|
||||||
<div>
|
<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>
|
<p className="text-muted-foreground mt-1">Analyse des réponses</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export default function AdminQuestionnaireEdit() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="container mx-auto py-8 max-w-4xl">
|
<div className="container mx-auto py-8 max-w-4xl">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-3xl font-bold">
|
<h1 className="page-title">
|
||||||
{questionnaireId ? "Modifier le questionnaire" : "Nouveau questionnaire"}
|
{questionnaireId ? "Modifier le questionnaire" : "Nouveau questionnaire"}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function AdminQuestionnaireSuivi() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground mt-2">
|
||||||
Tableau de bord de suivi des questionnaires avec taux de réponse et statistiques
|
Tableau de bord de suivi des questionnaires avec taux de réponse et statistiques
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function AdminQuestionnaires() {
|
|||||||
<div className="container mx-auto py-8">
|
<div className="container mx-auto py-8">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Questionnaires</h1>
|
<h1 className="page-title">Questionnaires</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1">
|
||||||
Gérez les questionnaires de satisfaction et d'évaluation
|
Gérez les questionnaires de satisfaction et d'évaluation
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ export default function AdminRappels() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Rappels automatiques</h1>
|
<h1 className="page-title">Rappels automatiques</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Configurez les rappels envoyés automatiquement avant les séquences
|
Configurez les rappels envoyés automatiquement avant les séquences
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -20,9 +20,15 @@ export default function AdminRapportPublicCible() {
|
|||||||
|
|
||||||
const totalInscrits = sequenceInscriptions.length;
|
const totalInscrits = sequenceInscriptions.length;
|
||||||
const mismatchCount = sequenceInscriptions.filter((insc: any) => {
|
const mismatchCount = sequenceInscriptions.filter((insc: any) => {
|
||||||
const apprenantFonction = insc.apprenant.fonction;
|
const apprenantFonction = insc.apprenant?.fonction;
|
||||||
const sequencePublicCible = sequence.publicCible;
|
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
|
// Mapper les fonctions aux publics cibles
|
||||||
const fonctionToPublicCible: Record<string, string> = {
|
const fonctionToPublicCible: Record<string, string> = {
|
||||||
directeur: "directeur",
|
directeur: "directeur",
|
||||||
@@ -106,7 +112,7 @@ export default function AdminRapportPublicCible() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="container mx-auto py-8 space-y-6">
|
<div className="container mx-auto py-8 space-y-6">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground mt-1">
|
||||||
Comparaison entre le public cible des séquences et la fonction des apprenants inscrits
|
Comparaison entre le public cible des séquences et la fonction des apprenants inscrits
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ export default function AdminSequenceInscrits() {
|
|||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
Retour aux séquences
|
Retour aux séquences
|
||||||
</Button>
|
</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">
|
<p className="text-muted-foreground mt-1">
|
||||||
{getFormationName()} - {sequence.nom}
|
{getFormationName()} - {sequence.nom}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ export default function AdminSequences() {
|
|||||||
<div className="container mx-auto py-8 space-y-6">
|
<div className="container mx-auto py-8 space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground mt-1">
|
||||||
Créez et gérez les séquences de formation avec leurs dates
|
Créez et gérez les séquences de formation avec leurs dates
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export default function AdminUsers() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="mb-6 flex justify-between items-start">
|
<div className="mb-6 flex justify-between items-start">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground">
|
||||||
Gérez les comptes utilisateurs de l'application
|
Gérez les comptes utilisateurs de l'application
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export default function ComponentsShowcase() {
|
|||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<main className="container max-w-6xl mx-auto">
|
<main className="container max-w-6xl mx-auto">
|
||||||
<div className="space-y-2 justify-between flex">
|
<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
|
Shadcn/ui Component Library
|
||||||
</h2>
|
</h2>
|
||||||
<Button variant="outline" size="icon" onClick={toggleTheme}>
|
<Button variant="outline" size="icon" onClick={toggleTheme}>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function FormateurDashboard() {
|
|||||||
<div className="container py-8 space-y-8">
|
<div className="container py-8 space-y-8">
|
||||||
{/* En-tête */}
|
{/* En-tête */}
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground mt-2">
|
||||||
Vue d'ensemble de vos formations et prochaines interventions
|
Vue d'ensemble de vos formations et prochaines interventions
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default function FormateurEmargement() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="container py-8 space-y-6">
|
<div className="container py-8 space-y-6">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground mt-2">
|
||||||
Gérez les présences de vos apprenants avec le QR code
|
Gérez les présences de vos apprenants avec le QR code
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function HistoriqueAttestations() {
|
|||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<FileText className="h-8 w-8 text-primary" />
|
<FileText className="h-8 w-8 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Historique des attestations</h1>
|
<h1 className="page-title">Historique des attestations</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Suivi de tous les envois d'attestations de formation
|
Suivi de tous les envois d'attestations de formation
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default function Home() {
|
|||||||
{/* Features Grid */}
|
{/* Features Grid */}
|
||||||
<section className="py-16 px-4 bg-white">
|
<section className="py-16 px-4 bg-white">
|
||||||
<div className="container mx-auto max-w-6xl">
|
<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">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{features.map((feature, index) => (
|
{features.map((feature, index) => (
|
||||||
<Card key={index} className="hover:shadow-lg transition-shadow">
|
<Card key={index} className="hover:shadow-lg transition-shadow">
|
||||||
@@ -135,7 +135,7 @@ export default function Home() {
|
|||||||
{/* How it works */}
|
{/* How it works */}
|
||||||
<section className="py-16 px-4">
|
<section className="py-16 px-4">
|
||||||
<div className="container mx-auto max-w-4xl">
|
<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">
|
<div className="space-y-8">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ export default function Inscription() {
|
|||||||
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
<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>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>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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<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" />
|
<img src={APP_LOGO} alt="Logo" className="h-20 w-auto" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<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">
|
<CardDescription className="mt-3 text-base">
|
||||||
Choisissez votre méthode de connexion
|
Choisissez votre méthode de connexion
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default function QuestionnaireReponse() {
|
|||||||
{APP_LOGO && (
|
{APP_LOGO && (
|
||||||
<img src={APP_LOGO} alt={APP_TITLE} className="h-16 mx-auto mb-4" />
|
<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 && (
|
{data.questionnaire?.description && (
|
||||||
<p className="text-muted-foreground">{data.questionnaire.description}</p>
|
<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">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<FileText className="h-8 w-8 text-primary" />
|
<FileText className="h-8 w-8 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Configuration des attestations</h1>
|
<h1 className="page-title">Configuration des attestations</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Personnalisez le contenu et la signature des attestations de formation
|
Personnalisez le contenu et la signature des attestations de formation
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function AdminFail2Ban() {
|
|||||||
<div className="container mx-auto py-8 space-y-6">
|
<div className="container mx-auto py-8 space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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>
|
<p className="text-muted-foreground">Surveillance des tentatives d'intrusion SSH</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleRefresh} variant="outline">
|
<Button onClick={handleRefresh} variant="outline">
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function AdminRappelsHistorique() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground mt-1">
|
||||||
Consultez tous les rappels envoyés avec filtres avancés
|
Consultez tous les rappels envoyés avec filtres avancés
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function AdminRappelsStats() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground mt-1">
|
||||||
Analysez les performances du système de rappels
|
Analysez les performances du système de rappels
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function FormateurDashboard() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Mon Calendrier</h1>
|
<h1 className="page-title">Mon Calendrier</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Vos interventions à venir
|
Vos interventions à venir
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default function FormateurHistorique() {
|
|||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground">
|
||||||
Toutes vos formations passées
|
Toutes vos formations passées
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export default function FormateurSequence() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* En-tête */}
|
{/* En-tête */}
|
||||||
<div>
|
<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>
|
<p className="text-muted-foreground">{sequence.formationNom}</p>
|
||||||
</div>
|
</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,
|
"when": 1768553182073,
|
||||||
"tag": "0002_concerned_karma",
|
"tag": "0002_concerned_karma",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768924576044,
|
||||||
|
"tag": "0003_calm_frog_thor",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -50,23 +50,14 @@ export async function getTauxRemplissageByMonth() {
|
|||||||
.select({
|
.select({
|
||||||
mois: sql<string>`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`,
|
mois: sql<string>`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`,
|
||||||
capaciteTotale: sql<number>`SUM(${sequences.capaciteMax})`,
|
capaciteTotale: sql<number>`SUM(${sequences.capaciteMax})`,
|
||||||
inscritsConfirmes: sql<number>`SUM(
|
inscritsConfirmes: sql<number>`COUNT(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN ${inscriptions.id} END)`,
|
||||||
(SELECT COUNT(*)
|
|
||||||
FROM ${inscriptions}
|
|
||||||
WHERE ${inscriptions.sequenceId} = ${sequences.id}
|
|
||||||
AND ${inscriptions.statut} = 'confirmee')
|
|
||||||
)`,
|
|
||||||
tauxRemplissage: sql<number>`ROUND(
|
tauxRemplissage: sql<number>`ROUND(
|
||||||
(SUM(
|
(COUNT(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN ${inscriptions.id} END) / SUM(${sequences.capaciteMax})) * 100,
|
||||||
(SELECT COUNT(*)
|
|
||||||
FROM ${inscriptions}
|
|
||||||
WHERE ${inscriptions.sequenceId} = ${sequences.id}
|
|
||||||
AND ${inscriptions.statut} = 'confirmee')
|
|
||||||
) / SUM(${sequences.capaciteMax})) * 100,
|
|
||||||
2
|
2
|
||||||
)`,
|
)`,
|
||||||
})
|
})
|
||||||
.from(sequences)
|
.from(sequences)
|
||||||
|
.leftJoin(inscriptions, eq(sequences.id, inscriptions.sequenceId))
|
||||||
.groupBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`)
|
.groupBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`)
|
||||||
.orderBy(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();
|
const expiresAt = new Date();
|
||||||
expiresAt.setHours(expiresAt.getHours() + 24); // Token valide 24h
|
expiresAt.setHours(expiresAt.getHours() + 24); // Token valide 24h
|
||||||
|
|
||||||
const result = await db.insert(passwordResetTokens).values({
|
// Convertir la date au format MySQL DATETIME (YYYY-MM-DD HH:MM:SS)
|
||||||
email,
|
const mysqlDate = expiresAt.toISOString().slice(0, 19).replace('T', ' ');
|
||||||
token,
|
|
||||||
expiresAt,
|
// Utiliser une requête SQL brute pour éviter les problèmes avec Drizzle
|
||||||
used: false,
|
const result = await db.execute(sql`
|
||||||
});
|
INSERT INTO passwordResetTokens (email, token, expiresAt)
|
||||||
|
VALUES (${email}, ${token}, ${mysqlDate})
|
||||||
|
`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -361,9 +361,13 @@ export const appRouter = router({
|
|||||||
update: adminProcedure.input(z.object({
|
update: adminProcedure.input(z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
nom: z.string().min(1).optional(),
|
nom: z.string().min(1).optional(),
|
||||||
email: z.string().email().nullable().optional(),
|
email: z.string().email().or(z.literal('')).nullable().optional(),
|
||||||
})).mutation(async ({ input }) => {
|
})).mutation(async ({ input }) => {
|
||||||
const { id, ...data } = input;
|
const { id, ...data } = input;
|
||||||
|
// Convertir les chaînes vides en null
|
||||||
|
if (data.email === '') {
|
||||||
|
data.email = null;
|
||||||
|
}
|
||||||
await db.updateFormateur(id, data);
|
await db.updateFormateur(id, data);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
@@ -1257,12 +1261,8 @@ export const appRouter = router({
|
|||||||
const crypto = await import('crypto');
|
const crypto = await import('crypto');
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
const token = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
// Définir l'expiration à 24h
|
// Sauvegarder le token en base (la fonction gère l'expiration automatiquement)
|
||||||
const expiresAt = new Date();
|
await db.createPasswordResetToken(user.email, token);
|
||||||
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
||||||
|
|
||||||
// Sauvegarder le token en base
|
|
||||||
await db.createPasswordResetToken(user.id, token, expiresAt);
|
|
||||||
|
|
||||||
// Générer le lien de réinitialisation
|
// Générer le lien de réinitialisation
|
||||||
// TODO: Remplacer par l'URL réelle de votre application en production
|
// 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] Identifier les requêtes concernées dans server/routers.ts
|
||||||
- [x] Corriger les requêtes SQL
|
- [x] Corriger les requêtes SQL
|
||||||
- [x] Tester les corrections
|
- [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