**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
192 lines
7.1 KiB
TypeScript
192 lines
7.1 KiB
TypeScript
import { useState } from "react";
|
|
import { useLocation, useParams } from "wouter";
|
|
import DashboardLayout from "@/components/DashboardLayout";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { trpc } from "@/lib/trpc";
|
|
import { ArrowLeft, Calendar, MapPin, User } from "lucide-react";
|
|
import { format } from "date-fns";
|
|
import { fr } from "date-fns/locale";
|
|
|
|
export default function AdminApprenantDetail() {
|
|
const params = useParams();
|
|
const apprenantId = params.id ? parseInt(params.id) : 0;
|
|
const [, setLocation] = useLocation();
|
|
|
|
const { data: apprenant, isLoading: loadingApprenant } = trpc.apprenants.getById.useQuery({ id: apprenantId });
|
|
const { data: inscriptions, isLoading: loadingInscriptions } = trpc.apprenants.getInscriptions.useQuery({ id: apprenantId });
|
|
|
|
if (loadingApprenant || loadingInscriptions) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-muted-foreground">Chargement...</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
if (!apprenant) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-muted-foreground">Apprenant introuvable</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
const getStatutBadge = (statut: string) => {
|
|
switch (statut) {
|
|
case 'confirmee':
|
|
return <Badge variant="default" className="bg-green-600">Confirmée</Badge>;
|
|
case 'en_attente':
|
|
return <Badge variant="secondary">En attente</Badge>;
|
|
case 'annulee':
|
|
return <Badge variant="destructive">Annulée</Badge>;
|
|
default:
|
|
return <Badge variant="outline">{statut}</Badge>;
|
|
}
|
|
};
|
|
|
|
const getFonctionLabel = (fonction: string) => {
|
|
switch (fonction) {
|
|
case 'directeur':
|
|
return 'Directeur';
|
|
case 'chef_service':
|
|
return 'Chef de service';
|
|
default:
|
|
return 'Autre';
|
|
}
|
|
};
|
|
|
|
const getPublicCibleLabel = (publicCible: string) => {
|
|
switch (publicCible) {
|
|
case 'directeur':
|
|
return 'Directeurs';
|
|
case 'chef_service':
|
|
return 'Chefs de service';
|
|
case 'tous':
|
|
return 'Tous';
|
|
default:
|
|
return 'Autre';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => setLocation('/admin/apprenants')}
|
|
className="mb-2"
|
|
>
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
Retour aux apprenants
|
|
</Button>
|
|
<h1 className="page-title">Détail de l'apprenant</h1>
|
|
<p className="text-muted-foreground">
|
|
Consultez toutes les inscriptions de cet apprenant
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Informations de l'apprenant */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<User className="h-5 w-5" />
|
|
Informations personnelles
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<div className="text-sm font-medium text-muted-foreground">Nom complet</div>
|
|
<div className="text-lg font-semibold">{apprenant.nom} {apprenant.prenom}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-muted-foreground">Email</div>
|
|
<div className="text-lg">{apprenant.email}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-muted-foreground">Code établissement</div>
|
|
<div className="text-lg">{apprenant.codeEtablissement}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-muted-foreground">Fonction</div>
|
|
<div className="text-lg">{getFonctionLabel(apprenant.fonction)}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Liste des inscriptions */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Inscriptions aux formations</CardTitle>
|
|
<CardDescription>
|
|
{inscriptions?.length || 0} inscription(s) au total
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!inscriptions || inscriptions.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
Aucune inscription trouvée pour cet apprenant
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Formation</TableHead>
|
|
<TableHead>Séquence</TableHead>
|
|
<TableHead>Dates</TableHead>
|
|
<TableHead>Lieu</TableHead>
|
|
<TableHead>Public cible</TableHead>
|
|
<TableHead>Date d'inscription</TableHead>
|
|
<TableHead>Statut</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{inscriptions.map((insc: any) => (
|
|
<TableRow key={insc.id}>
|
|
<TableCell className="font-medium">{insc.formation?.nom || 'N/A'}</TableCell>
|
|
<TableCell>{insc.sequence?.nom || 'N/A'}</TableCell>
|
|
<TableCell>
|
|
<div className="flex flex-col gap-1">
|
|
{insc.sequence?.dates?.map((date: any, idx: number) => (
|
|
<div key={idx} className="flex items-center gap-1 text-sm">
|
|
<Calendar className="h-3 w-3" />
|
|
{format(new Date(date.dateDebut), 'dd/MM/yyyy HH:mm', { locale: fr })}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-1">
|
|
<MapPin className="h-3 w-3" />
|
|
{insc.sequence?.lieu || 'N/A'}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{getPublicCibleLabel(insc.sequence?.publicCible || '')}</TableCell>
|
|
<TableCell>
|
|
{format(new Date(insc.dateInscription), 'dd/MM/yyyy HH:mm', { locale: fr })}
|
|
</TableCell>
|
|
<TableCell>{getStatutBadge(insc.statut)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|