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:
@@ -10,6 +10,7 @@ import Admin from "./pages/Admin";
|
||||
import AdminFormations from "./pages/AdminFormations";
|
||||
import AdminSequences from "./pages/AdminSequences";
|
||||
import AdminApprenants from "./pages/AdminApprenants";
|
||||
import AdminApprenantDetail from "./pages/AdminApprenantDetail";
|
||||
import AdminSequenceInscrits from "./pages/AdminSequenceInscrits";
|
||||
import AdminUsers from "./pages/AdminUsers";
|
||||
import AdminEmailTemplates from "./pages/AdminEmailTemplates";
|
||||
@@ -28,6 +29,7 @@ function Router() {
|
||||
<Route path={"/admin/formations"} component={AdminFormations} />
|
||||
<Route path={"/admin/sequences"} component={AdminSequences} />
|
||||
<Route path={"/admin/apprenants"} component={AdminApprenants} />
|
||||
<Route path={"/admin/apprenants/:id"} component={AdminApprenantDetail} />
|
||||
<Route path={"/admin/sequences/:id/inscrits"} component={AdminSequenceInscrits} />
|
||||
<Route path={"/admin/users"} component={AdminUsers} />
|
||||
<Route path={"/admin/email-templates"} component={AdminEmailTemplates} />
|
||||
|
||||
191
client/src/pages/AdminApprenantDetail.tsx
Normal file
191
client/src/pages/AdminApprenantDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -7,12 +7,14 @@ import { Label } from "@/components/ui/label";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Plus, Pencil, Trash2, Search, SlidersHorizontal } from "lucide-react";
|
||||
import { Plus, Pencil, Trash2, Search, SlidersHorizontal, Eye } from "lucide-react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function AdminApprenants() {
|
||||
const [, setLocation] = useLocation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
@@ -298,6 +300,14 @@ export default function AdminApprenants() {
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setLocation(`/admin/apprenants/${apprenant.id}`)}
|
||||
title="Voir les détails et inscriptions"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
||||
Reference in New Issue
Block a user