Checkpoint: Création d'un tableau de bord analytique avancé avec graphiques interactifs (Recharts). Affiche les statistiques globales, l'évolution temporelle des inscriptions par mois, le taux de remplissage des séquences, le top 10 des établissements participants et la répartition par fonction. Accessible via le menu "Tableau de bord analytique".

This commit is contained in:
Manus Sandbox
2025-11-23 16:25:22 -05:00
parent 33161d1a4d
commit 64db3f1aa6
6 changed files with 475 additions and 1 deletions

View File

@@ -0,0 +1,284 @@
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 { BarChart3, TrendingUp, Users, Building2, Calendar, PieChart } from "lucide-react";
import {
LineChart,
Line,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
PieChart as RePieChart,
Pie,
Cell,
} from "recharts";
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
export default function AdminAnalytics() {
const { data: globalStats, isLoading: loadingGlobal } = trpc.analytics.globalStats.useQuery();
const { data: inscriptionsByMonth, isLoading: loadingInscriptions } = trpc.analytics.inscriptionsByMonth.useQuery({});
const { data: tauxRemplissage, isLoading: loadingTaux } = trpc.analytics.tauxRemplissageByMonth.useQuery();
const { data: participationEtablissement, isLoading: loadingEtablissement } = trpc.analytics.participationByEtablissement.useQuery();
const { data: participationFonction, isLoading: loadingFonction } = trpc.analytics.participationByFonction.useQuery();
// Formater les données pour les graphiques
const inscriptionsData = inscriptionsByMonth?.map(item => ({
mois: item.mois,
Confirmées: Number(item.confirmees),
"Liste d'attente": Number(item.listeAttente),
Annulées: Number(item.annulees),
Total: Number(item.total),
})) || [];
const tauxRemplissageData = tauxRemplissage?.map(item => ({
mois: item.mois,
"Taux de remplissage (%)": Number(item.tauxRemplissage) || 0,
"Capacité totale": Number(item.capaciteTotale),
"Inscrits confirmés": Number(item.inscritsConfirmes),
})) || [];
const etablissementData = participationEtablissement?.slice(0, 10).map(item => ({
etablissement: item.codeEtablissement,
"Total inscriptions": Number(item.totalInscriptions),
Confirmées: Number(item.inscriptionsConfirmees),
"Liste d'attente": Number(item.inscriptionsListeAttente),
})) || [];
const fonctionData = participationFonction?.map(item => ({
name: item.fonction === 'directeur' ? 'Directeurs' :
item.fonction === 'chef_service' ? 'Chefs de service' : 'Autres',
value: Number(item.totalInscriptions),
confirmees: Number(item.inscriptionsConfirmees),
})) || [];
return (
<DashboardLayout>
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2">Tableau de bord analytique</h1>
<p className="text-muted-foreground">
Statistiques et indicateurs de performance des formations
</p>
</div>
{/* Statistiques globales */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Formations</CardTitle>
<BarChart3 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{globalStats?.totalFormations || 0}</div>
<p className="text-xs text-muted-foreground">
{globalStats?.totalSequences || 0} séquences au total
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Séquences ouvertes</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{globalStats?.sequencesOuvertes || 0}</div>
<p className="text-xs text-muted-foreground">
Disponibles pour inscription
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Apprenants</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{globalStats?.totalApprenants || 0}</div>
<p className="text-xs text-muted-foreground">
{globalStats?.totalInscriptions || 0} inscriptions
</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 remplissage</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{globalStats?.tauxRemplissageMoyen || 0}%</div>
<p className="text-xs text-muted-foreground">
Moyenne globale
</p>
</CardContent>
</Card>
</div>
{/* Graphiques */}
<div className="grid gap-6 md:grid-cols-2 mb-6">
{/* Évolution des inscriptions */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Évolution des inscriptions par mois
</CardTitle>
<CardDescription>
Répartition des inscriptions confirmées, en liste d'attente et annulées
</CardDescription>
</CardHeader>
<CardContent>
{loadingInscriptions ? (
<div className="h-[300px] flex items-center justify-center">
<p className="text-muted-foreground">Chargement...</p>
</div>
) : inscriptionsData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={inscriptionsData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mois" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="Confirmées" stroke="#10b981" strokeWidth={2} />
<Line type="monotone" dataKey="Liste d'attente" stroke="#f59e0b" strokeWidth={2} />
<Line type="monotone" dataKey="Annulées" stroke="#ef4444" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center">
<p className="text-muted-foreground">Aucune donnée disponible</p>
</div>
)}
</CardContent>
</Card>
{/* Taux de remplissage */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
Taux de remplissage par mois
</CardTitle>
<CardDescription>
Pourcentage de places occupées par rapport à la capacité totale
</CardDescription>
</CardHeader>
<CardContent>
{loadingTaux ? (
<div className="h-[300px] flex items-center justify-center">
<p className="text-muted-foreground">Chargement...</p>
</div>
) : tauxRemplissageData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={tauxRemplissageData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mois" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="Taux de remplissage (%)" fill="#3b82f6" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center">
<p className="text-muted-foreground">Aucune donnée disponible</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Participation par établissement et fonction */}
<div className="grid gap-6 md:grid-cols-2">
{/* Top 10 établissements */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="h-5 w-5" />
Top 10 établissements
</CardTitle>
<CardDescription>
Établissements avec le plus d'inscriptions
</CardDescription>
</CardHeader>
<CardContent>
{loadingEtablissement ? (
<div className="h-[300px] flex items-center justify-center">
<p className="text-muted-foreground">Chargement...</p>
</div>
) : etablissementData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={etablissementData} layout="vertical">
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="etablissement" type="category" width={80} />
<Tooltip />
<Legend />
<Bar dataKey="Confirmées" fill="#10b981" />
<Bar dataKey="Liste d'attente" fill="#f59e0b" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center">
<p className="text-muted-foreground">Aucune donnée disponible</p>
</div>
)}
</CardContent>
</Card>
{/* Répartition par fonction */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<PieChart className="h-5 w-5" />
Répartition par fonction
</CardTitle>
<CardDescription>
Distribution des inscriptions par type de fonction
</CardDescription>
</CardHeader>
<CardContent>
{loadingFonction ? (
<div className="h-[300px] flex items-center justify-center">
<p className="text-muted-foreground">Chargement...</p>
</div>
) : fonctionData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<RePieChart>
<Pie
data={fonctionData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{fonctionData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</RePieChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center">
<p className="text-muted-foreground">Aucune donnée disponible</p>
</div>
)}
</CardContent>
</Card>
</div>
</DashboardLayout>
);
}