Files
formation-manager-itinova/client/src/pages/AdminQuestionnaireSuivi.tsx
Manus fc4df8ed36 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
2026-01-20 11:46:56 -05:00

240 lines
11 KiB
TypeScript

import { useState } from "react";
import { trpc } from "@/lib/trpc";
import DashboardLayout from "@/components/DashboardLayout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
import { Loader2, TrendingUp, Users, FileText, Target } from "lucide-react";
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D', '#FFC658', '#FF6B6B'];
export default function AdminQuestionnaireSuivi() {
const { data: statsGlobales, isLoading: loadingGlobales } = trpc.questionnaireSuivi.statsGlobales.useQuery();
const { data: statsByFormation, isLoading: loadingFormation } = trpc.questionnaireSuivi.statsByFormation.useQuery();
const { data: evolutionTemporelle, isLoading: loadingEvolution } = trpc.questionnaireSuivi.evolutionTemporelle.useQuery({});
const { data: statsByFormateur, isLoading: loadingFormateur } = trpc.questionnaireSuivi.statsByFormateur.useQuery();
const isLoading = loadingGlobales || loadingFormation || loadingEvolution || loadingFormateur;
if (isLoading) {
return (
<DashboardLayout>
<div className="flex items-center justify-center h-96">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
</DashboardLayout>
);
}
return (
<DashboardLayout>
<div className="space-y-6">
<div>
<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>
</div>
{/* Statistiques globales */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Questionnaires</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{statsGlobales?.totalQuestionnaires || 0}</div>
<p className="text-xs text-muted-foreground">Total créés</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Envois</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{statsGlobales?.totalEnvois || 0}</div>
<p className="text-xs text-muted-foreground">Total envoyés</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Réponses</CardTitle>
<Target className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{statsGlobales?.totalReponses || 0}</div>
<p className="text-xs text-muted-foreground">Total reçues</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Taux de réponse</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{statsGlobales?.tauxReponseGlobal?.toFixed(1) || 0}%</div>
<p className="text-xs text-muted-foreground">Global</p>
</CardContent>
</Card>
</div>
{/* Taux de réponse par formation */}
<Card>
<CardHeader>
<CardTitle>Taux de réponse par formation</CardTitle>
<CardDescription>Comparaison des taux de réponse entre les différentes formations</CardDescription>
</CardHeader>
<CardContent>
{statsByFormation && statsByFormation.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={statsByFormation}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="formationNom" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="totalEnvois" fill="#8884d8" name="Envois" />
<Bar dataKey="totalReponses" fill="#82ca9d" name="Réponses" />
</BarChart>
</ResponsiveContainer>
) : (
<p className="text-center text-muted-foreground py-8">Aucune donnée disponible</p>
)}
</CardContent>
</Card>
<div className="grid gap-6 md:grid-cols-2">
{/* Évolution temporelle */}
<Card>
<CardHeader>
<CardTitle>Évolution dans le temps</CardTitle>
<CardDescription>Évolution mensuelle des envois et réponses</CardDescription>
</CardHeader>
<CardContent>
{evolutionTemporelle && evolutionTemporelle.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={evolutionTemporelle}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mois" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="totalEnvois" stroke="#8884d8" name="Envois" />
<Line type="monotone" dataKey="totalReponses" stroke="#82ca9d" name="Réponses" />
</LineChart>
</ResponsiveContainer>
) : (
<p className="text-center text-muted-foreground py-8">Aucune donnée disponible</p>
)}
</CardContent>
</Card>
{/* Taux de réponse par formation (camembert) */}
<Card>
<CardHeader>
<CardTitle>Répartition des réponses</CardTitle>
<CardDescription>Distribution des réponses par formation</CardDescription>
</CardHeader>
<CardContent>
{statsByFormation && statsByFormation.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={statsByFormation}
dataKey="totalReponses"
nameKey="formationNom"
cx="50%"
cy="50%"
outerRadius={80}
label={(entry) => `${entry.formationNom}: ${entry.tauxReponse}%`}
>
{statsByFormation && Array.isArray(statsByFormation) && statsByFormation.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
) : (
<p className="text-center text-muted-foreground py-8">Aucune donnée disponible</p>
)}
</CardContent>
</Card>
</div>
{/* Comparaison par formateur */}
<Card>
<CardHeader>
<CardTitle>Comparaison par formateur</CardTitle>
<CardDescription>Taux de réponse et satisfaction moyenne par formateur</CardDescription>
</CardHeader>
<CardContent>
{statsByFormateur && statsByFormateur.length > 0 ? (
<div className="space-y-4">
<ResponsiveContainer width="100%" height={300}>
<BarChart data={statsByFormateur}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="formateurNom" />
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" domain={[0, 10]} />
<Tooltip />
<Legend />
<Bar yAxisId="left" dataKey="tauxReponse" fill="#8884d8" name="Taux de réponse (%)" />
<Bar yAxisId="right" dataKey="moyenneSatisfaction" fill="#82ca9d" name="Satisfaction moyenne (/10)" />
</BarChart>
</ResponsiveContainer>
{/* Tableau détaillé */}
<div className="rounded-md border">
<table className="w-full">
<thead>
<tr className="border-b bg-muted/50">
<th className="p-2 text-left font-medium">Formateur</th>
<th className="p-2 text-center font-medium">Envois</th>
<th className="p-2 text-center font-medium">Réponses</th>
<th className="p-2 text-center font-medium">Taux</th>
<th className="p-2 text-center font-medium">Satisfaction</th>
</tr>
</thead>
<tbody>
{statsByFormateur && Array.isArray(statsByFormateur) && statsByFormateur.map((formateur) => (
<tr key={formateur.formateurId} className="border-b">
<td className="p-2">{formateur.formateurNom}</td>
<td className="p-2 text-center">{formateur.totalEnvois}</td>
<td className="p-2 text-center">{formateur.totalReponses}</td>
<td className="p-2 text-center">
<span className={`font-medium ${
formateur.tauxReponse >= 70 ? 'text-green-600' :
formateur.tauxReponse >= 50 ? 'text-orange-600' :
'text-red-600'
}`}>
{formateur.tauxReponse}%
</span>
</td>
<td className="p-2 text-center">
{formateur.moyenneSatisfaction ? (
<span className="font-medium">{formateur.moyenneSatisfaction}/10</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<p className="text-center text-muted-foreground py-8">Aucune donnée disponible</p>
)}
</CardContent>
</Card>
</div>
</DashboardLayout>
);
}