- Ajout du composant DashboardLayout à la page FormateurDashboard - Le menu de navigation latéral s'affiche maintenant correctement pour les utilisateurs avec le rôle formateur - Navigation cohérente entre toutes les pages de l'espace formateur (Dashboard et Émargement)
209 lines
9.0 KiB
TypeScript
209 lines
9.0 KiB
TypeScript
import { trpc } from "@/lib/trpc";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { Calendar, Users, GraduationCap, Clock, MapPin, AlertCircle } from "lucide-react";
|
|
import { format } from "date-fns";
|
|
import { fr } from "date-fns/locale";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import DashboardLayout from "@/components/DashboardLayout";
|
|
|
|
export default function FormateurDashboard() {
|
|
const { data: stats, isLoading: statsLoading } = trpc.formateurs.dashboardStats.useQuery();
|
|
const { data: prochainesSequences, isLoading: sequencesLoading } = trpc.formateurs.prochainesSequences.useQuery({ limit: 10 });
|
|
|
|
if (statsLoading) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="container py-8 space-y-8">
|
|
<div>
|
|
<Skeleton className="h-10 w-96 mb-2" />
|
|
<Skeleton className="h-6 w-64" />
|
|
</div>
|
|
<div className="grid gap-6 md:grid-cols-3">
|
|
<Skeleton className="h-32" />
|
|
<Skeleton className="h-32" />
|
|
<Skeleton className="h-32" />
|
|
</div>
|
|
<Skeleton className="h-96" />
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="container py-8 space-y-8">
|
|
{/* En-tête */}
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Tableau de bord formateur</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Vue d'ensemble de vos formations et prochaines interventions
|
|
</p>
|
|
</div>
|
|
|
|
{/* Cartes de statistiques */}
|
|
<div className="grid gap-6 md:grid-cols-3">
|
|
<Card>
|
|
<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>
|
|
|
|
{/* Liste des prochaines séquences */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Prochaines séquences</CardTitle>
|
|
<CardDescription>
|
|
Vos interventions à venir triées par date
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{sequencesLoading ? (
|
|
<div className="space-y-4">
|
|
{[1, 2, 3].map((i) => (
|
|
<Skeleton key={i} className="h-32" />
|
|
))}
|
|
</div>
|
|
) : prochainesSequences && prochainesSequences.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{prochainesSequences.map((seq: any) => {
|
|
const pourcentageRemplissage = seq.capaciteMax > 0
|
|
? Math.round((seq.nbInscrits / seq.capaciteMax) * 100)
|
|
: 0;
|
|
const estComplete = pourcentageRemplissage >= 100;
|
|
const estPresqueComplete = pourcentageRemplissage >= 80 && pourcentageRemplissage < 100;
|
|
|
|
return (
|
|
<Card key={seq.id} className="overflow-hidden">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between">
|
|
<div className="space-y-1">
|
|
<CardTitle className="text-lg">{seq.nom}</CardTitle>
|
|
<CardDescription className="flex items-center gap-2">
|
|
<GraduationCap className="h-4 w-4" />
|
|
{seq.formation?.nom || "Formation inconnue"}
|
|
</CardDescription>
|
|
</div>
|
|
<Badge variant={seq.statut === 'ouverte' ? 'default' : seq.statut === 'bloquee' ? 'secondary' : 'outline'}>
|
|
{seq.statut === 'ouverte' ? 'Ouverte' : seq.statut === 'bloquee' ? 'Bloquée' : 'Terminée'}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Lieu */}
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<MapPin className="h-4 w-4" />
|
|
<span>{seq.lieu}</span>
|
|
</div>
|
|
|
|
{/* Dates */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
<Clock className="h-4 w-4" />
|
|
<span>Dates de formation</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2 ml-6">
|
|
{seq.dates.map((date: any) => {
|
|
const dateDebut = new Date(date.dateDebut);
|
|
const dateFin = new Date(date.dateFin);
|
|
const estFuture = dateDebut >= new Date();
|
|
|
|
return (
|
|
<Badge
|
|
key={date.id}
|
|
variant={estFuture ? "default" : "outline"}
|
|
className="text-xs"
|
|
>
|
|
{format(dateDebut, "dd MMM", { locale: fr })}
|
|
{dateDebut.toDateString() !== dateFin.toDateString() &&
|
|
` - ${format(dateFin, "dd MMM", { locale: fr })}`
|
|
}
|
|
</Badge>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Capacité */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<Users className="h-4 w-4" />
|
|
<span className="font-medium">Inscriptions</span>
|
|
</div>
|
|
<span className="text-muted-foreground">
|
|
{seq.nbInscrits} / {seq.capaciteMax}
|
|
</span>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Progress
|
|
value={pourcentageRemplissage}
|
|
className="h-2"
|
|
/>
|
|
{estComplete && (
|
|
<div className="flex items-center gap-1 text-xs text-orange-600">
|
|
<AlertCircle className="h-3 w-3" />
|
|
<span>Capacité maximale atteinte</span>
|
|
</div>
|
|
)}
|
|
{estPresqueComplete && (
|
|
<div className="flex items-center gap-1 text-xs text-yellow-600">
|
|
<AlertCircle className="h-3 w-3" />
|
|
<span>Presque complète ({pourcentageRemplissage}%)</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>Aucune séquence à venir pour le moment</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|