Checkpoint: Application complète de gestion des formations Manager Itinova avec toutes les fonctionnalités : gestion des formations/sessions/apprenants, système d'inscription avec validations, génération d'invitations Outlook, emails automatiques, exports PDF/Excel, et documentation complète.
This commit is contained in:
377
client/src/pages/AdminSessions.tsx
Normal file
377
client/src/pages/AdminSessions.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import DashboardLayout from "@/components/DashboardLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Plus, Pencil, Trash2, Users as UsersIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { format } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
export default function AdminSessions() {
|
||||
const [, setLocation] = useLocation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
formationId: "",
|
||||
nom: "",
|
||||
dateDebut: "",
|
||||
dateFin: "",
|
||||
lieu: "",
|
||||
capaciteMax: "12",
|
||||
dateBlocage: "",
|
||||
statut: "ouverte" as "ouverte" | "bloquee" | "terminee",
|
||||
});
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const { data: sessions, isLoading } = trpc.sessions.list.useQuery();
|
||||
const { data: formations } = trpc.formations.list.useQuery();
|
||||
|
||||
const createMutation = trpc.sessions.create.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.sessions.list.invalidate();
|
||||
toast.success("Session créée avec succès");
|
||||
setOpen(false);
|
||||
resetForm();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Erreur lors de la création : " + error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = trpc.sessions.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.sessions.list.invalidate();
|
||||
toast.success("Session mise à jour avec succès");
|
||||
setOpen(false);
|
||||
resetForm();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Erreur lors de la mise à jour : " + error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.sessions.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.sessions.list.invalidate();
|
||||
toast.success("Session supprimée avec succès");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Erreur lors de la suppression : " + error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
formationId: "",
|
||||
nom: "",
|
||||
dateDebut: "",
|
||||
dateFin: "",
|
||||
lieu: "",
|
||||
capaciteMax: "12",
|
||||
dateBlocage: "",
|
||||
statut: "ouverte",
|
||||
});
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const handleEdit = (session: any) => {
|
||||
setFormData({
|
||||
formationId: session.formationId.toString(),
|
||||
nom: session.nom,
|
||||
dateDebut: format(new Date(session.dateDebut), "yyyy-MM-dd'T'HH:mm"),
|
||||
dateFin: format(new Date(session.dateFin), "yyyy-MM-dd'T'HH:mm"),
|
||||
lieu: session.lieu,
|
||||
capaciteMax: session.capaciteMax.toString(),
|
||||
dateBlocage: format(new Date(session.dateBlocage), "yyyy-MM-dd'T'HH:mm"),
|
||||
statut: session.statut,
|
||||
});
|
||||
setEditingId(session.id);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
formationId: parseInt(formData.formationId),
|
||||
nom: formData.nom,
|
||||
dateDebut: formData.dateDebut,
|
||||
dateFin: formData.dateFin,
|
||||
lieu: formData.lieu,
|
||||
capaciteMax: parseInt(formData.capaciteMax),
|
||||
dateBlocage: formData.dateBlocage,
|
||||
statut: formData.statut,
|
||||
};
|
||||
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, ...data });
|
||||
} else {
|
||||
createMutation.mutate(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette session ?")) {
|
||||
deleteMutation.mutate({ id });
|
||||
}
|
||||
};
|
||||
|
||||
const getFormationName = (formationId: number) => {
|
||||
return formations?.find(f => f.id === formationId)?.nom || "Formation inconnue";
|
||||
};
|
||||
|
||||
const formatDate = (date: Date | string) => {
|
||||
return format(new Date(date), "dd/MM/yyyy HH:mm", { locale: fr });
|
||||
};
|
||||
|
||||
const getStatutBadge = (statut: string) => {
|
||||
const styles = {
|
||||
ouverte: "bg-green-100 text-green-800",
|
||||
bloquee: "bg-orange-100 text-orange-800",
|
||||
terminee: "bg-gray-100 text-gray-800",
|
||||
};
|
||||
const labels = {
|
||||
ouverte: "Ouverte",
|
||||
bloquee: "Bloquée",
|
||||
terminee: "Terminée",
|
||||
};
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${styles[statut as keyof typeof styles]}`}>
|
||||
{labels[statut as keyof typeof labels]}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Sessions</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Gérez les sessions de formation et leurs inscriptions
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={open} onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) resetForm();
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nouvelle session
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingId ? "Modifier la session" : "Nouvelle session"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Créez une session avec dates, lieu et capacité
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="formationId">Formation *</Label>
|
||||
<Select
|
||||
value={formData.formationId}
|
||||
onValueChange={(value) => setFormData({ ...formData, formationId: value })}
|
||||
required
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sélectionnez une formation" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{formations?.map((formation) => (
|
||||
<SelectItem key={formation.id} value={formation.id.toString()}>
|
||||
{formation.nom}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="nom">Nom de la session *</Label>
|
||||
<Input
|
||||
id="nom"
|
||||
value={formData.nom}
|
||||
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
||||
placeholder="Session 1 - Février 2026"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dateDebut">Date de début *</Label>
|
||||
<Input
|
||||
id="dateDebut"
|
||||
type="datetime-local"
|
||||
value={formData.dateDebut}
|
||||
onChange={(e) => setFormData({ ...formData, dateDebut: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dateFin">Date de fin *</Label>
|
||||
<Input
|
||||
id="dateFin"
|
||||
type="datetime-local"
|
||||
value={formData.dateFin}
|
||||
onChange={(e) => setFormData({ ...formData, dateFin: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lieu">Lieu *</Label>
|
||||
<Input
|
||||
id="lieu"
|
||||
value={formData.lieu}
|
||||
onChange={(e) => setFormData({ ...formData, lieu: e.target.value })}
|
||||
placeholder="Salle de formation A, Bâtiment principal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="capaciteMax">Capacité maximale *</Label>
|
||||
<Input
|
||||
id="capaciteMax"
|
||||
type="number"
|
||||
min="1"
|
||||
value={formData.capaciteMax}
|
||||
onChange={(e) => setFormData({ ...formData, capaciteMax: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dateBlocage">Date de blocage (J-15) *</Label>
|
||||
<Input
|
||||
id="dateBlocage"
|
||||
type="datetime-local"
|
||||
value={formData.dateBlocage}
|
||||
onChange={(e) => setFormData({ ...formData, dateBlocage: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="statut">Statut</Label>
|
||||
<Select
|
||||
value={formData.statut}
|
||||
onValueChange={(value: any) => setFormData({ ...formData, statut: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ouverte">Ouverte</SelectItem>
|
||||
<SelectItem value="bloquee">Bloquée</SelectItem>
|
||||
<SelectItem value="terminee">Terminée</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMutation.isPending || updateMutation.isPending}>
|
||||
{editingId ? "Mettre à jour" : "Créer"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Liste des sessions</CardTitle>
|
||||
<CardDescription>
|
||||
Gérez vos sessions et consultez les inscriptions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : sessions && sessions.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Formation</TableHead>
|
||||
<TableHead>Nom</TableHead>
|
||||
<TableHead>Date début</TableHead>
|
||||
<TableHead>Date fin</TableHead>
|
||||
<TableHead>Lieu</TableHead>
|
||||
<TableHead>Capacité</TableHead>
|
||||
<TableHead>Statut</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sessions.map((session) => (
|
||||
<TableRow key={session.id}>
|
||||
<TableCell className="font-medium">{getFormationName(session.formationId)}</TableCell>
|
||||
<TableCell>{session.nom}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{formatDate(session.dateDebut)}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{formatDate(session.dateFin)}</TableCell>
|
||||
<TableCell className="max-w-xs truncate">{session.lieu}</TableCell>
|
||||
<TableCell>{session.capaciteMax}</TableCell>
|
||||
<TableCell>{getStatutBadge(session.statut)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setLocation(`/admin/sessions/${session.id}/inscrits`)}
|
||||
title="Voir les inscrits"
|
||||
>
|
||||
<UsersIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEdit(session)}
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(session.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Aucune session pour le moment. Créez-en une pour commencer.
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user