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.
This commit is contained in:
@@ -24,6 +24,7 @@ import AdminImportExcel from "./pages/AdminImportExcel";
|
||||
import AdminQuestionnaires from "./pages/AdminQuestionnaires";
|
||||
import AdminQuestionnaireEdit from "./pages/AdminQuestionnaireEdit";
|
||||
import AdminQuestionnaireAnalyse from "./pages/AdminQuestionnaireAnalyse";
|
||||
import AdminQuestionnaireSuivi from "./pages/AdminQuestionnaireSuivi";
|
||||
import QuestionnaireReponse from "./pages/QuestionnaireReponse";
|
||||
import Inscription from "./pages/Inscription";
|
||||
import Login from "./pages/Login";
|
||||
@@ -51,7 +52,8 @@ function Router() {
|
||||
<Route path={"/admin/etablissements"} component={AdminEtablissements} />
|
||||
<Route path={"/admin/import-excel"} component={AdminImportExcel} />
|
||||
<Route path={"/admin/questionnaires"} component={AdminQuestionnaires} />
|
||||
<Route path={"/admin/questionnaires/:id/analyse"} component={AdminQuestionnaireAnalyse} />
|
||||
<Route path="/admin/questionnaires/:id/analyse" component={AdminQuestionnaireAnalyse} />
|
||||
<Route path="/admin/questionnaires/suivi" component={AdminQuestionnaireSuivi} />
|
||||
<Route path={"/admin/questionnaires/:id"} component={AdminQuestionnaireEdit} />
|
||||
<Route path={"/questionnaire/:token"} component={QuestionnaireReponse} />
|
||||
<Route path={"/404"} component={NotFound} />
|
||||
|
||||
@@ -60,6 +60,7 @@ const menuSections = [
|
||||
{ icon: Building2, label: "Établissements", path: "/admin/etablissements" },
|
||||
{ icon: TrendingUp, label: "Tableau de bord analytique", path: "/admin/analytics" },
|
||||
{ icon: BarChart3, label: "Rapport Public Cible", path: "/admin/rapport-public-cible" },
|
||||
{ icon: TrendingUp, label: "Suivi Questionnaires", path: "/admin/questionnaires/suivi" },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
239
client/src/pages/AdminQuestionnaireSuivi.tsx
Normal file
239
client/src/pages/AdminQuestionnaireSuivi.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user