**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
240 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|