**Corrections d'erreurs :** 1. **Erreurs SQL DATE_FORMAT corrigées** (analyticsDb.ts) : - Remplacement des sous-requêtes corrélées par des JOIN dans getTauxRemplissageByMonth() - Utilisation de COUNT(CASE WHEN...) au lieu de SUM(SELECT COUNT(*)) - Ajout de leftJoin pour éviter les erreurs de sous-requêtes 2. **Erreur React "Cannot read properties of null" corrigée** (AdminRapportPublicCible.tsx) : - Ajout de l'opérateur null-safe `?.` pour accéder à `insc.apprenant?.fonction` - Vérification explicite si la fonction est null/undefined - Gestion du cas "public cible = tous" qui correspond toujours 3. **Tests réussis** : - Page rapport-public-cible : affiche correctement les statistiques et recommandations - Tableau de bord analytique : graphiques et statistiques fonctionnels - Page suivi questionnaires : affiche correctement les données **Harmonisation charte graphique :** - Application du style bleu clair (#6B9FE8) avec ombre portée à tous les titres des 32 pages - Création de la classe CSS `.page-title` réutilisable dans index.css - Cohérence visuelle sur toute l'application **Résultat :** ✅ Toutes les erreurs SQL et React sont corrigées ✅ Les pages fonctionnent correctement sans erreur ✅ Charte graphique harmonisée sur toutes les pages
129 lines
5.0 KiB
TypeScript
129 lines
5.0 KiB
TypeScript
import { useAuth } from "@/_core/hooks/useAuth";
|
|
import DashboardLayout from "@/components/DashboardLayout";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { trpc } from "@/lib/trpc";
|
|
import { Calendar, Clock, MapPin, Users } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { Link } from "wouter";
|
|
|
|
export default function FormateurDashboard() {
|
|
const { user } = useAuth();
|
|
const [dateDebut] = useState<Date>(new Date());
|
|
const [dateFin] = useState<Date>(() => {
|
|
const date = new Date();
|
|
date.setMonth(date.getMonth() + 3); // 3 mois à l'avance
|
|
return date;
|
|
});
|
|
|
|
// Récupérer l'ID du formateur associé à l'utilisateur
|
|
// Pour l'instant, on utilise un ID fixe, mais il faudrait le lier à l'utilisateur
|
|
const formateurId = 1; // TODO: Récupérer depuis la base de données
|
|
|
|
const { data: interventions, isLoading } = trpc.formateur.calendrier.useQuery({
|
|
formateurId,
|
|
dateDebut,
|
|
dateFin,
|
|
});
|
|
|
|
const formatDate = (date: Date) => {
|
|
return new Date(date).toLocaleDateString("fr-FR", {
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
};
|
|
|
|
const formatTime = (date: Date) => {
|
|
return new Date(date).toLocaleTimeString("fr-FR", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
};
|
|
|
|
// Grouper les interventions par date
|
|
const interventionsParDate = interventions?.reduce((acc, intervention) => {
|
|
const dateKey = new Date(intervention.dateDebut).toDateString();
|
|
if (!acc[dateKey]) {
|
|
acc[dateKey] = [];
|
|
}
|
|
acc[dateKey].push(intervention);
|
|
return acc;
|
|
}, {} as Record<string, typeof interventions>);
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="page-title">Mon Calendrier</h1>
|
|
<p className="text-muted-foreground">
|
|
Vos interventions à venir
|
|
</p>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
) : !interventions || interventions.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="py-12">
|
|
<div className="text-center text-muted-foreground">
|
|
<Calendar className="mx-auto h-12 w-12 mb-4 opacity-50" />
|
|
<p>Aucune intervention prévue pour les 3 prochains mois</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{Object.entries(interventionsParDate || {}).map(([dateKey, dayInterventions]) => (
|
|
<div key={dateKey} className="space-y-3">
|
|
<h2 className="text-xl font-semibold flex items-center gap-2">
|
|
<Calendar className="h-5 w-5" />
|
|
{formatDate(new Date(dateKey))}
|
|
</h2>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{dayInterventions.map((intervention) => (
|
|
<Card key={intervention.dateId} className="hover:shadow-md transition-shadow">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">{intervention.formationNom}</CardTitle>
|
|
<CardDescription>{intervention.sequenceNom}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
<span>
|
|
{formatTime(intervention.dateDebut)} - {formatTime(intervention.dateFin)}
|
|
</span>
|
|
</div>
|
|
{intervention.lieu && (
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<MapPin className="h-4 w-4 text-muted-foreground" />
|
|
<span>{intervention.lieu}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
<span>{intervention.nbInscrits} apprenant(s) inscrit(s)</span>
|
|
</div>
|
|
<div className="pt-2">
|
|
<Link href={`/formateur/sequence/${intervention.sequenceId}`}>
|
|
<Button variant="outline" size="sm" className="w-full">
|
|
Voir les détails
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|