Checkpoint: Refonte complète de l'ergonomie du tableau de bord avec design moderne, animations et couleurs vives.

**Améliorations visuelles :**

1. **Icônes modernes et colorées** :
   - Icônes plus grandes (6x6) avec fond gradient coloré
   - Effet de rotation au survol des icônes (rotate-12)
   - 4 palettes de couleurs distinctes : bleu (formations), vert (séquences ouvertes), violet (apprenants), orange (séquences terminées)

2. **Liserés et bordures colorés** :
   - Bordure gauche épaisse (4px) sur chaque carte de statistique
   - Bordures colorées (2px) sur les cartes de graphiques
   - Fond légèrement teinté coordonné aux couleurs (bg-blue-50/50, bg-green-50/50, etc.)

3. **Palette de couleurs enrichie** :
   - Titre principal avec gradient bleu-violet (bg-gradient-to-r from-blue-600 to-purple-600)
   - Valeurs des statistiques avec gradients colorés
   - En-têtes de graphiques avec fond gradient subtil (from-blue-50 to-purple-50)

4. **Animations fluides** :
   - Fade-in au chargement de la page (animate-in fade-in duration-500)
   - Slide-in séquentiel des cartes avec délai progressif (100ms entre chaque carte)
   - Effet hover : scale-105 + translate-y-1 + shadow-xl
   - Effet de brillance au survol des cartes (gradient animé)
   - Zoom léger des graphiques au survol (scale-105)
   - Animation des graphiques recharts (800ms)

5. **Détails supplémentaires** :
   - Badge "Données en temps réel" avec icône TrendingUp en haut à droite
   - Indicateur "Actif" avec flèche verte sur chaque carte
   - Ombres portées dynamiques au survol (hover:shadow-xl)
   - Titres avec text-xl et font-bold pour plus d'impact

**Résultat :**
 Interface moderne et dynamique
 Expérience utilisateur améliorée avec animations fluides
 Meilleure hiérarchie visuelle grâce aux couleurs et liserés
 Design professionnel et engageant
 Testé et validé sur la sandbox
This commit is contained in:
Manus
2026-01-20 07:01:11 -05:00
parent 21ed7f7355
commit b7b2134289
2 changed files with 133 additions and 84 deletions

View File

@@ -1,7 +1,7 @@
import DashboardLayout from "@/components/DashboardLayout"; import DashboardLayout from "@/components/DashboardLayout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { trpc } from "@/lib/trpc"; import { trpc } from "@/lib/trpc";
import { GraduationCap, Calendar, Users, CheckCircle, PieChart as PieChartIcon, Building2 } from "lucide-react"; import { GraduationCap, Calendar, Users, CheckCircle, PieChart as PieChartIcon, Building2, TrendingUp } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"; import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts";
import { useMemo } from "react"; import { useMemo } from "react";
@@ -52,80 +52,132 @@ export default function Admin() {
{ {
title: "Formations actives", title: "Formations actives",
value: formations?.filter(f => f.actif).length || 0, value: formations?.filter(f => f.actif).length || 0,
icon: <GraduationCap className="w-8 h-8 text-blue-500" />, icon: GraduationCap,
gradient: "from-blue-500 to-blue-600",
borderColor: "border-l-blue-500",
bgColor: "bg-blue-50/50",
iconBg: "bg-gradient-to-br from-blue-500 to-blue-600",
loading: loadingFormations, loading: loadingFormations,
}, },
{ {
title: "Séquences ouvertes", title: "Séquences ouvertes",
value: sequences?.filter(s => s.statut === "ouverte").length || 0, value: sequences?.filter(s => s.statut === "ouverte").length || 0,
icon: <Calendar className="w-8 h-8 text-green-500" />, icon: Calendar,
gradient: "from-green-500 to-emerald-600",
borderColor: "border-l-green-500",
bgColor: "bg-green-50/50",
iconBg: "bg-gradient-to-br from-green-500 to-emerald-600",
loading: loadingSessions, loading: loadingSessions,
}, },
{ {
title: "Apprenants inscrits", title: "Apprenants inscrits",
value: apprenants?.length || 0, value: apprenants?.length || 0,
icon: <Users className="w-8 h-8 text-purple-500" />, icon: Users,
gradient: "from-purple-500 to-purple-600",
borderColor: "border-l-purple-500",
bgColor: "bg-purple-50/50",
iconBg: "bg-gradient-to-br from-purple-500 to-purple-600",
loading: loadingApprenants, loading: loadingApprenants,
}, },
{ {
title: "Séquences terminées", title: "Séquences terminées",
value: sequences?.filter(s => s.statut === "terminee").length || 0, value: sequences?.filter(s => s.statut === "terminee").length || 0,
icon: <CheckCircle className="w-8 h-8 text-orange-500" />, icon: CheckCircle,
gradient: "from-orange-500 to-orange-600",
borderColor: "border-l-orange-500",
bgColor: "bg-orange-50/50",
iconBg: "bg-gradient-to-br from-orange-500 to-orange-600",
loading: loadingSessions, loading: loadingSessions,
}, },
]; ];
return ( return (
<DashboardLayout> <DashboardLayout>
<div className="space-y-6"> <div className="space-y-8 animate-in fade-in duration-500">
<div> <div className="flex items-center justify-between">
<h1 className="text-3xl font-bold tracking-tight">Tableau de bord</h1> <div>
<p className="text-muted-foreground mt-2"> <h1 className="text-4xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Gestion des formations Manager Itinova Tableau de bord
</p> </h1>
<p className="text-muted-foreground mt-2 text-lg">
Gestion des formations Manager Itinova
</p>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<TrendingUp className="w-4 h-4 text-green-500" />
<span>Données en temps réel</span>
</div>
</div> </div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> {/* Cartes de statistiques avec animations */}
{stats.map((stat, index) => ( <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<Card key={index}> {stats.map((stat, index) => {
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> const Icon = stat.icon;
<CardTitle className="text-sm font-medium"> return (
{stat.title} <Card
</CardTitle> key={index}
{stat.icon} className={`
</CardHeader> relative overflow-hidden border-l-4 ${stat.borderColor} ${stat.bgColor}
<CardContent> transition-all duration-300 hover:shadow-xl hover:scale-105 hover:-translate-y-1
{stat.loading ? ( animate-in slide-in-from-bottom-4
<Skeleton className="h-8 w-16" /> `}
) : ( style={{ animationDelay: `${index * 100}ms` }}
<div className="text-2xl font-bold">{stat.value}</div> >
)} <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
</CardContent> <CardTitle className="text-sm font-semibold text-gray-700">
</Card> {stat.title}
))} </CardTitle>
<div className={`p-3 rounded-xl ${stat.iconBg} shadow-lg transition-transform duration-300 hover:rotate-12`}>
<Icon className="w-6 h-6 text-white" />
</div>
</CardHeader>
<CardContent>
{stat.loading ? (
<Skeleton className="h-10 w-20" />
) : (
<div className="space-y-1">
<div className={`text-3xl font-bold bg-gradient-to-r ${stat.gradient} bg-clip-text text-transparent`}>
{stat.value}
</div>
<div className="flex items-center gap-1 text-xs text-green-600 font-medium">
<TrendingUp className="w-3 h-3" />
<span>Actif</span>
</div>
</div>
)}
</CardContent>
{/* Effet de brillance au survol */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000" />
</Card>
);
})}
</div> </div>
{/* Graphiques de répartition */} {/* Graphiques de répartition avec animations */}
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
{/* Graphique de répartition par fonction */} {/* Graphique de répartition par fonction */}
{statsFonction.length > 0 && ( {statsFonction.length > 0 && (
<Card> <Card className="border-2 border-blue-100 transition-all duration-300 hover:shadow-xl hover:border-blue-300 animate-in slide-in-from-left-4" style={{ animationDelay: "400ms" }}>
<CardHeader> <CardHeader className="bg-gradient-to-r from-blue-50 to-purple-50 border-b-2 border-blue-100">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-3 text-xl">
<PieChartIcon className="w-5 h-5" /> <div className="p-2 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 shadow-md">
Répartition des apprenants par fonction <PieChartIcon className="w-6 h-6 text-white" />
</div>
<span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent font-bold">
Répartition des apprenants par fonction
</span>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-base mt-2">
Distribution des {apprenants?.length || 0} apprenants selon leur fonction Distribution des {apprenants?.length || 0} apprenants selon leur fonction
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="pt-6">
{loadingApprenants ? ( {loadingApprenants ? (
<div className="h-80 flex items-center justify-center"> <div className="h-80 flex items-center justify-center">
<Skeleton className="h-64 w-64 rounded-full" /> <Skeleton className="h-64 w-64 rounded-full" />
</div> </div>
) : ( ) : (
<div className="h-80"> <div className="h-80 transition-all duration-500 hover:scale-105">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie <Pie
@@ -135,42 +187,48 @@ export default function Admin() {
labelLine={false} labelLine={false}
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`} label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
outerRadius={100} outerRadius={100}
fill="#8884d8" fill="#8884d8"
dataKey="value" dataKey="value"
> animationBegin={0}
{statsFonction.map((entry, index) => ( animationDuration={800}
<Cell key={`cell-${index}`} fill={entry.color} /> >
))} {statsFonction.map((entry, index) => (
</Pie> <Cell key={`cell-${index}`} fill={entry.color} />
<Tooltip /> ))}
<Legend /> </Pie>
</PieChart> <Tooltip />
</ResponsiveContainer> <Legend />
</div> </PieChart>
)} </ResponsiveContainer>
</CardContent> </div>
</Card> )}
)} </CardContent>
</Card>
)}
{/* Graphique de répartition par établissement */} {/* Graphique de répartition par établissement */}
{statsEtablissement.length > 0 && ( {statsEtablissement.length > 0 && (
<Card> <Card className="border-2 border-green-100 transition-all duration-300 hover:shadow-xl hover:border-green-300 animate-in slide-in-from-right-4" style={{ animationDelay: "400ms" }}>
<CardHeader> <CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 border-b-2 border-green-100">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-3 text-xl">
<Building2 className="w-5 h-5" /> <div className="p-2 rounded-lg bg-gradient-to-br from-green-500 to-emerald-600 shadow-md">
Répartition des apprenants par établissement <Building2 className="w-6 h-6 text-white" />
</div>
<span className="bg-gradient-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent font-bold">
Répartition des apprenants par établissement
</span>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-base mt-2">
Top 10 des établissements avec le plus d'apprenants Top 10 des établissements avec le plus d'apprenants
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="pt-6">
{loadingApprenants ? ( {loadingApprenants ? (
<div className="h-80 flex items-center justify-center"> <div className="h-80 flex items-center justify-center">
<Skeleton className="h-64 w-64 rounded-full" /> <Skeleton className="h-64 w-64 rounded-full" />
</div> </div>
) : ( ) : (
<div className="h-80"> <div className="h-80 transition-all duration-500 hover:scale-105">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie <Pie
@@ -182,6 +240,8 @@ export default function Admin() {
outerRadius={100} outerRadius={100}
fill="#8884d8" fill="#8884d8"
dataKey="value" dataKey="value"
animationBegin={0}
animationDuration={800}
> >
{statsEtablissement.map((entry, index) => ( {statsEtablissement.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${index}`} fill={entry.color} />
@@ -197,27 +257,6 @@ export default function Admin() {
</Card> </Card>
)} )}
</div> </div>
<Card>
<CardHeader>
<CardTitle>Bienvenue dans l'espace d'administration</CardTitle>
<CardDescription>
Gérez vos formations, sequences et apprenants depuis le menu de navigation.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h3 className="font-semibold">Fonctionnalités principales :</h3>
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
<li>Créer et gérer les formations avec liens d'inscription uniques</li>
<li>Organiser les sequences avec limitation à 12 participants</li>
<li>Gérer les inscriptions avec blocage automatique à J-15</li>
<li>Exporter les listes d'inscrits en PDF ou Excel</li>
<li>Générer des invitations Outlook et envoyer des emails automatiques</li>
</ul>
</div>
</CardContent>
</Card>
</div> </div>
</DashboardLayout> </DashboardLayout>
); );

10
todo.md
View File

@@ -994,3 +994,13 @@
- [x] Liste des utilisateurs - [x] Liste des utilisateurs
- [x] Liste des formateurs - [x] Liste des formateurs
- [x] Déployer et tester - [x] Déployer et tester
## Refonte ergonomie du tableau de bord
- [x] Analyser le tableau de bord actuel (Admin.tsx)
- [x] Créer nouvelle version avec icônes modernes et colorées
- [x] Ajouter liserés colorés sur les cartes
- [x] Implémenter animations au survol et au chargement
- [x] Améliorer la palette de couleurs
- [x] Tester sur sandbox
- [ ] Déployer après validation utilisateur