Checkpoint: Ajout de la fonctionnalité "Détail par apprenant"

 Nouvelle procédure tRPC `apprenants.getInscriptions` pour récupérer toutes les inscriptions d'un apprenant avec les détails de formation et séquence
 Nouvelle page AdminApprenantDetail (/admin/apprenants/:id) affichant :
   - Informations personnelles de l'apprenant (nom, prénom, email, code établissement, fonction)
   - Tableau complet de toutes ses inscriptions avec :
     * Nom de la formation
     * Nom de la séquence
     * Toutes les dates de la séquence
     * Lieu
     * Public cible
     * Date d'inscription
     * Statut (avec badge coloré : vert pour confirmée, gris pour en attente, rouge pour annulée)
 Bouton "Détail" (icône œil) ajouté dans la colonne Actions de la page AdminApprenants
 Route /admin/apprenants/:id configurée dans App.tsx
 Bouton "Retour aux apprenants" pour navigation facile

Correction appliquée : La procédure getInscriptions extrait maintenant correctement l'objet inscription du résultat retourné par getInscriptionsByApprenant.

Tests réussis : Affichage correct des inscriptions pour l'apprenant "Nouveau Apprenant" (1 inscription à la séquence "Groupe A" avec statut confirmée).
This commit is contained in:
Manus Sandbox
2025-11-20 11:00:53 -05:00
parent 017ad5dd18
commit 2975d92568
5 changed files with 233 additions and 1 deletions

View File

@@ -0,0 +1,191 @@
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="text-3xl font-bold">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>
);
}