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:
@@ -18,6 +18,7 @@ import AdminEmailTemplates from "./pages/AdminEmailTemplates";
|
||||
import AdminEmailConfig from "./pages/AdminEmailConfig";
|
||||
import AdminCalendrier from "./pages/AdminCalendrier";
|
||||
import AdminRappels from "./pages/AdminRappels";
|
||||
import AdminAnalytics from "./pages/AdminAnalytics";
|
||||
import Inscription from "./pages/Inscription";
|
||||
import Login from "./pages/Login";
|
||||
|
||||
@@ -40,6 +41,7 @@ function Router() {
|
||||
<Route path={"/admin/email-config"} component={AdminEmailConfig} />
|
||||
<Route path={"/admin/calendrier"} component={AdminCalendrier} />
|
||||
<Route path={"/admin/rappels"} component={AdminRappels} />
|
||||
<Route path={"/admin/analytics"} component={AdminAnalytics} />
|
||||
<Route path={"/404"} component={NotFound} />
|
||||
{/* Final fallback route */}
|
||||
<Route component={NotFound} />
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from "@/components/ui/sidebar";
|
||||
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
|
||||
import { useIsMobile } from "@/hooks/useMobile";
|
||||
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings } from "lucide-react";
|
||||
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings, TrendingUp } from "lucide-react";
|
||||
import { CSSProperties, useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
||||
@@ -37,6 +37,7 @@ const menuItems = [
|
||||
{ icon: UserCog, label: "Utilisateurs", path: "/admin/users" },
|
||||
{ icon: Mail, label: "Templates d'emails", path: "/admin/email-templates" },
|
||||
{ icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" },
|
||||
{ icon: TrendingUp, label: "Tableau de bord analytique", path: "/admin/analytics" },
|
||||
{ icon: BarChart3, label: "Rapport Public Cible", path: "/admin/rapport-public-cible" },
|
||||
];
|
||||
|
||||
|
||||
284
client/src/pages/AdminAnalytics.tsx
Normal file
284
client/src/pages/AdminAnalytics.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user