Files
formation-manager-itinova/client/src/pages/AdminQuestionnaireSuivi.tsx
Manus Sandbox 46b3f377d1 Checkpoint: Tableau de bord de suivi des questionnaires
 **Nouvelles fonctionnalités implémentées** :

**Backend complet** :
- Fichier questionnaireSuiviDb.ts avec 4 fonctions de statistiques
- getStatsByFormation() : Taux de réponse par formation
- getEvolutionTemporelle() : Évolution mensuelle des envois/réponses
- getStatsByFormateur() : Comparaison par formateur avec satisfaction moyenne
- getStatsGlobales() : Statistiques globales (questionnaires, envois, réponses, taux)
- Procédures tRPC complètes dans routers.ts

**Interface utilisateur** :
- Page AdminQuestionnaireSuivi avec graphiques interactifs (Recharts)
- 4 cartes de statistiques globales (questionnaires, envois, réponses, taux)
- Graphique en barres : Taux de réponse par formation
- Graphique en ligne : Évolution temporelle mensuelle
- Graphique camembert : Répartition des réponses par formation
- Graphique double axe : Comparaison formateurs (taux + satisfaction)
- Tableau détaillé des formateurs avec code couleur (vert >70%, orange >50%, rouge <50%)
- Lien "Suivi Questionnaires" ajouté dans le menu Analyses

Le tableau de bord offre une vue complète des performances des questionnaires pour optimiser le suivi et l'amélioration continue.
2025-12-07 16:03:24 -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="text-3xl font-bold">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.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.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>
);
}