Checkpoint: Mise à jour du tableau de bord formateur avec le même design moderne que le tableau de bord admin : cartes colorées avec dégradés, animations au survol, et redirection automatique des formateurs
This commit is contained in:
@@ -2,7 +2,7 @@ import { trpc } from "@/lib/trpc";
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Calendar, Users, GraduationCap, Clock, MapPin, AlertCircle } from "lucide-react";
|
import { Calendar, Users, GraduationCap, Clock, MapPin, AlertCircle, TrendingUp } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { fr } from "date-fns/locale";
|
import { fr } from "date-fns/locale";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
@@ -12,87 +12,121 @@ export default function FormateurDashboard() {
|
|||||||
const { data: stats, isLoading: statsLoading } = trpc.formateurs.dashboardStats.useQuery();
|
const { data: stats, isLoading: statsLoading } = trpc.formateurs.dashboardStats.useQuery();
|
||||||
const { data: prochainesSequences, isLoading: sequencesLoading } = trpc.formateurs.prochainesSequences.useQuery({ limit: 10 });
|
const { data: prochainesSequences, isLoading: sequencesLoading } = trpc.formateurs.prochainesSequences.useQuery({ limit: 10 });
|
||||||
|
|
||||||
if (statsLoading) {
|
const statsCards = [
|
||||||
return (
|
{
|
||||||
<DashboardLayout>
|
title: "Total séquences",
|
||||||
<div className="container py-8 space-y-8">
|
value: stats?.totalSequences || 0,
|
||||||
<div>
|
icon: GraduationCap,
|
||||||
<Skeleton className="h-10 w-96 mb-2" />
|
gradient: "from-blue-300 to-blue-400",
|
||||||
<Skeleton className="h-6 w-64" />
|
borderColor: "border-l-blue-400",
|
||||||
</div>
|
bgColor: "bg-blue-50/80",
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
iconBg: "bg-gradient-to-br from-blue-300 to-blue-400",
|
||||||
<Skeleton className="h-32" />
|
description: "Toutes vos séquences",
|
||||||
<Skeleton className="h-32" />
|
loading: statsLoading,
|
||||||
<Skeleton className="h-32" />
|
},
|
||||||
</div>
|
{
|
||||||
<Skeleton className="h-96" />
|
title: "Séquences à venir",
|
||||||
</div>
|
value: stats?.sequencesAvenir || 0,
|
||||||
</DashboardLayout>
|
icon: Calendar,
|
||||||
);
|
gradient: "from-green-300 to-emerald-400",
|
||||||
}
|
borderColor: "border-l-green-400",
|
||||||
|
bgColor: "bg-green-50/80",
|
||||||
|
iconBg: "bg-gradient-to-br from-green-300 to-emerald-400",
|
||||||
|
description: "Avec dates futures",
|
||||||
|
loading: statsLoading,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Total apprenants",
|
||||||
|
value: stats?.totalInscrits || 0,
|
||||||
|
icon: Users,
|
||||||
|
gradient: "from-purple-300 to-purple-400",
|
||||||
|
borderColor: "border-l-purple-400",
|
||||||
|
bgColor: "bg-purple-50/80",
|
||||||
|
iconBg: "bg-gradient-to-br from-purple-300 to-purple-400",
|
||||||
|
description: "Inscrits confirmés",
|
||||||
|
loading: statsLoading,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="container py-8 space-y-8">
|
<div className="space-y-8 animate-in fade-in duration-500">
|
||||||
{/* En-tête */}
|
{/* En-tête */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="page-title">Tableau de bord formateur</h1>
|
<h1 className="text-4xl font-bold tracking-tight bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
|
||||||
<p className="text-muted-foreground mt-2">
|
Tableau de bord formateur
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-2 text-lg">
|
||||||
Vue d'ensemble de vos formations et prochaines interventions
|
Vue d'ensemble de vos formations et prochaines interventions
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
{/* Cartes de statistiques */}
|
<TrendingUp className="w-4 h-4 text-green-500" />
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
<span>Données en temps réel</span>
|
||||||
<Card>
|
</div>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Total séquences</CardTitle>
|
|
||||||
<GraduationCap className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{stats?.totalSequences || 0}</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Toutes vos séquences de formation
|
|
||||||
</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 à venir</CardTitle>
|
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{stats?.sequencesAvenir || 0}</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Avec au moins une date future
|
|
||||||
</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">{stats?.totalInscrits || 0}</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Inscrits confirmés à vos séquences
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Liste des prochaines séquences */}
|
{/* Cartes de statistiques avec animations */}
|
||||||
<Card>
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
<CardHeader>
|
{statsCards.map((stat, index) => {
|
||||||
<CardTitle>Prochaines séquences</CardTitle>
|
const Icon = stat.icon;
|
||||||
<CardDescription>
|
return (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
className={`
|
||||||
|
relative overflow-hidden border-l-4 ${stat.borderColor} ${stat.bgColor}
|
||||||
|
transition-all duration-300 hover:shadow-xl hover:scale-105 hover:-translate-y-1
|
||||||
|
animate-in slide-in-from-bottom-4
|
||||||
|
`}
|
||||||
|
style={{ animationDelay: `${index * 100}ms` }}
|
||||||
|
>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
||||||
|
<CardTitle className="text-sm font-semibold text-gray-700">
|
||||||
|
{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>{stat.description}</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>
|
||||||
|
|
||||||
|
{/* Liste des prochaines séquences avec design amélioré */}
|
||||||
|
<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: "300ms" }}>
|
||||||
|
<CardHeader className="bg-gradient-to-r from-blue-50 to-purple-50 border-b-2 border-blue-100">
|
||||||
|
<CardTitle className="flex items-center gap-3 text-xl">
|
||||||
|
<div className="p-2 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 shadow-md">
|
||||||
|
<Calendar 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">
|
||||||
|
Prochaines séquences
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-base mt-2">
|
||||||
Vos interventions à venir triées par date
|
Vos interventions à venir triées par date
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="pt-6">
|
||||||
{sequencesLoading ? (
|
{sequencesLoading ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[1, 2, 3].map((i) => (
|
{[1, 2, 3].map((i) => (
|
||||||
@@ -101,7 +135,7 @@ export default function FormateurDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
) : prochainesSequences && prochainesSequences.length > 0 ? (
|
) : prochainesSequences && prochainesSequences.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{prochainesSequences.map((seq: any) => {
|
{prochainesSequences.map((seq: any, index: number) => {
|
||||||
const pourcentageRemplissage = seq.capaciteMax > 0
|
const pourcentageRemplissage = seq.capaciteMax > 0
|
||||||
? Math.round((seq.nbInscrits / seq.capaciteMax) * 100)
|
? Math.round((seq.nbInscrits / seq.capaciteMax) * 100)
|
||||||
: 0;
|
: 0;
|
||||||
@@ -109,35 +143,42 @@ export default function FormateurDashboard() {
|
|||||||
const estPresqueComplete = pourcentageRemplissage >= 80 && pourcentageRemplissage < 100;
|
const estPresqueComplete = pourcentageRemplissage >= 80 && pourcentageRemplissage < 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={seq.id} className="overflow-hidden">
|
<Card
|
||||||
<CardHeader className="pb-3">
|
key={seq.id}
|
||||||
|
className="overflow-hidden border-2 transition-all duration-300 hover:shadow-lg hover:border-blue-300 hover:scale-[1.02] animate-in slide-in-from-right-4"
|
||||||
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
|
>
|
||||||
|
<CardHeader className="pb-3 bg-gradient-to-r from-gray-50 to-blue-50/30">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<CardTitle className="text-lg">{seq.nom}</CardTitle>
|
<CardTitle className="text-lg font-bold text-gray-900">{seq.nom}</CardTitle>
|
||||||
<CardDescription className="flex items-center gap-2">
|
<CardDescription className="flex items-center gap-2 text-base">
|
||||||
<GraduationCap className="h-4 w-4" />
|
<GraduationCap className="h-4 w-4" />
|
||||||
{seq.formation?.nom || "Formation inconnue"}
|
{seq.formation?.nom || "Formation inconnue"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={seq.statut === 'ouverte' ? 'default' : seq.statut === 'bloquee' ? 'secondary' : 'outline'}>
|
<Badge
|
||||||
|
variant={seq.statut === 'ouverte' ? 'default' : seq.statut === 'bloquee' ? 'secondary' : 'outline'}
|
||||||
|
className="text-sm px-3 py-1"
|
||||||
|
>
|
||||||
{seq.statut === 'ouverte' ? 'Ouverte' : seq.statut === 'bloquee' ? 'Bloquée' : 'Terminée'}
|
{seq.statut === 'ouverte' ? 'Ouverte' : seq.statut === 'bloquee' ? 'Bloquée' : 'Terminée'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4 pt-4">
|
||||||
{/* Lieu */}
|
{/* Lieu */}
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-gray-50 p-3 rounded-lg border border-gray-200">
|
||||||
<MapPin className="h-4 w-4" />
|
<MapPin className="h-5 w-5 text-blue-500" />
|
||||||
<span>{seq.lieu}</span>
|
<span className="font-medium">{seq.lieu}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dates */}
|
{/* Dates */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2 text-sm font-medium">
|
<div className="flex items-center gap-2 text-sm font-semibold text-gray-700">
|
||||||
<Clock className="h-4 w-4" />
|
<Clock className="h-5 w-5 text-purple-500" />
|
||||||
<span>Dates de formation</span>
|
<span>Dates de formation</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2 ml-6">
|
<div className="flex flex-wrap gap-2 ml-7">
|
||||||
{seq.dates.map((date: any) => {
|
{seq.dates.map((date: any) => {
|
||||||
const dateDebut = new Date(date.dateDebut);
|
const dateDebut = new Date(date.dateDebut);
|
||||||
const dateFin = new Date(date.dateFin);
|
const dateFin = new Date(date.dateFin);
|
||||||
@@ -147,7 +188,7 @@ export default function FormateurDashboard() {
|
|||||||
<Badge
|
<Badge
|
||||||
key={date.id}
|
key={date.id}
|
||||||
variant={estFuture ? "default" : "outline"}
|
variant={estFuture ? "default" : "outline"}
|
||||||
className="text-xs"
|
className="text-sm px-3 py-1 transition-all duration-200 hover:scale-110"
|
||||||
>
|
>
|
||||||
{format(dateDebut, "dd MMM", { locale: fr })}
|
{format(dateDebut, "dd MMM", { locale: fr })}
|
||||||
{dateDebut.toDateString() !== dateFin.toDateString() &&
|
{dateDebut.toDateString() !== dateFin.toDateString() &&
|
||||||
@@ -160,30 +201,30 @@ export default function FormateurDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Capacité */}
|
{/* Capacité */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 bg-gray-50 p-4 rounded-lg border border-gray-200">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 font-semibold text-gray-700">
|
||||||
<Users className="h-4 w-4" />
|
<Users className="h-5 w-5 text-green-500" />
|
||||||
<span className="font-medium">Inscriptions</span>
|
<span>Inscriptions</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground">
|
<span className="text-lg font-bold text-gray-900">
|
||||||
{seq.nbInscrits} / {seq.capaciteMax}
|
{seq.nbInscrits} / {seq.capaciteMax}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Progress
|
<Progress
|
||||||
value={pourcentageRemplissage}
|
value={pourcentageRemplissage}
|
||||||
className="h-2"
|
className="h-3 transition-all duration-500"
|
||||||
/>
|
/>
|
||||||
{estComplete && (
|
{estComplete && (
|
||||||
<div className="flex items-center gap-1 text-xs text-orange-600">
|
<div className="flex items-center gap-1 text-xs text-orange-600 font-medium bg-orange-50 p-2 rounded border border-orange-200">
|
||||||
<AlertCircle className="h-3 w-3" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<span>Capacité maximale atteinte</span>
|
<span>Capacité maximale atteinte</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{estPresqueComplete && (
|
{estPresqueComplete && (
|
||||||
<div className="flex items-center gap-1 text-xs text-yellow-600">
|
<div className="flex items-center gap-1 text-xs text-yellow-600 font-medium bg-yellow-50 p-2 rounded border border-yellow-200">
|
||||||
<AlertCircle className="h-3 w-3" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<span>Presque complète ({pourcentageRemplissage}%)</span>
|
<span>Presque complète ({pourcentageRemplissage}%)</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -195,9 +236,12 @@ export default function FormateurDashboard() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<div className="text-center py-16 text-muted-foreground">
|
||||||
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
<div className="bg-gray-100 rounded-full w-24 h-24 flex items-center justify-center mx-auto mb-4">
|
||||||
<p>Aucune séquence à venir pour le moment</p>
|
<Calendar className="h-12 w-12 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-medium">Aucune séquence à venir pour le moment</p>
|
||||||
|
<p className="text-sm mt-2">Vos prochaines interventions apparaîtront ici</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ export default function Home() {
|
|||||||
const [, setLocation] = useLocation();
|
const [, setLocation] = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && isAuthenticated && user?.role === 'admin') {
|
if (!loading && isAuthenticated) {
|
||||||
|
if (user?.role === 'admin') {
|
||||||
setLocation('/admin');
|
setLocation('/admin');
|
||||||
|
} else if (user?.role === 'formateur') {
|
||||||
|
setLocation('/formateur');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [loading, isAuthenticated, user?.role, setLocation]);
|
}, [loading, isAuthenticated, user?.role, setLocation]);
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ export default function Home() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAuthenticated && user?.role === 'admin') {
|
if (isAuthenticated && (user?.role === 'admin' || user?.role === 'formateur')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
todo.md
11
todo.md
@@ -1233,5 +1233,14 @@
|
|||||||
|
|
||||||
- [x] Identifier le code de modification de séquence qui désactive les rappels
|
- [x] Identifier le code de modification de séquence qui désactive les rappels
|
||||||
- [x] Corriger pour ne désactiver que lors de changement de dates
|
- [x] Corriger pour ne désactiver que lors de changement de dates
|
||||||
- [ ] Tester la modification
|
- [x] Tester la modification
|
||||||
|
- [x] Déployer sur le VPS
|
||||||
|
|
||||||
|
## Créer un tableau de bord spécifique pour les formateurs
|
||||||
|
|
||||||
|
- [x] Analyser le tableau de bord actuel (Admin.tsx)
|
||||||
|
- [x] Vérifier les procédures backend existantes (déjà présentes)
|
||||||
|
- [x] Réécrire FormateurDashboard.tsx avec le même design moderne
|
||||||
|
- [x] Vérifier la route dans App.tsx (déjà présente)
|
||||||
|
- [x] Ajouter la redirection automatique des formateurs vers /formateur
|
||||||
- [ ] Déployer sur le VPS
|
- [ ] Déployer sur le VPS
|
||||||
|
|||||||
Reference in New Issue
Block a user