This commit is contained in:
Manus Sandbox
2025-11-19 08:41:10 -05:00
parent 5085159cf7
commit 37e0f8da49
54 changed files with 13118 additions and 55 deletions

View File

@@ -0,0 +1,432 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { trpc } from "@/lib/trpc";
import { useRoute } from "wouter";
import { Skeleton } from "@/components/ui/skeleton";
import { useState, useMemo } from "react";
import { toast } from "sonner";
import { format } from "date-fns";
import { fr } from "date-fns/locale";
import { CheckCircle, XCircle, AlertCircle, Calendar, MapPin, Users } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { APP_LOGO, APP_TITLE } from "@/const";
export default function Inscription() {
const [, params] = useRoute("/inscription/:lien");
const lien = params?.lien || "";
const [formData, setFormData] = useState({
nom: "",
prenom: "",
email: "",
codeEtablissement: "",
fonction: "autre" as "directeur" | "chef_service" | "autre",
});
const [selectedSequenceId, setSelectedSequenceId] = useState<number | null>(null);
const [inscriptionSuccess, setInscriptionSuccess] = useState(false);
const [inscriptionStatut, setInscriptionStatut] = useState<string>("");
const { data: formation, isLoading: loadingFormation } = trpc.formations.getByLien.useQuery({ lien });
const { data: sequences, isLoading: loadingSessions } = trpc.sequences.listByFormation.useQuery(
{ formationId: formation?.id || 0 },
{ enabled: !!formation?.id }
);
const utils = trpc.useUtils();
const inscriptionMutation = trpc.inscriptions.inscrire.useMutation({
onSuccess: (data) => {
setInscriptionSuccess(true);
setInscriptionStatut(data.statut);
utils.sequences.listByFormation.invalidate({ formationId: formation?.id || 0 });
utils.apprenants.list.invalidate(); // Invalider la liste des apprenants
if (data.statut === "confirmee") {
toast.success("Inscription confirmée avec succès !");
} else {
toast.info("Vous êtes inscrit en liste d'attente.");
}
},
onError: (error) => {
toast.error(error.message);
},
});
const createApprenantMutation = trpc.apprenants.create.useMutation();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedSequenceId) {
toast.error("Veuillez sélectionner une sequence");
return;
}
// Vérifier si le public cible correspond à la fonction de l'apprenant
const selectedSequence = sequences?.find(s => s.id === selectedSequenceId);
if (selectedSequence && selectedSequence.publicCible) {
const publicCibleMap: Record<string, string> = {
"directeur": "directeur",
"chef_service": "chef_service",
"autre": "autre"
};
if (publicCibleMap[selectedSequence.publicCible] !== formData.fonction) {
const publicCibleLabel = selectedSequence.publicCible === "directeur" ? "Directeurs" :
selectedSequence.publicCible === "chef_service" ? "Chefs de service" : "Autre";
const fonctionLabel = formData.fonction === "directeur" ? "Directeur" :
formData.fonction === "chef_service" ? "Chef de service" : "Autre";
toast.info(
`Information : Cette séquence est destinée aux ${publicCibleLabel}, mais vous êtes inscrit(e) en tant que ${fonctionLabel}. Vous pouvez tout de même vous inscrire.`,
{ duration: 8000 }
);
// Attendre 2 secondes pour que l'utilisateur puisse lire l'alerte
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
try {
// Vérifier si l'apprenant existe déjà
const existingApprenant = await utils.client.apprenants.getByEmail.query({ email: formData.email });
let apprenantId: number;
if (existingApprenant) {
apprenantId = existingApprenant.id;
} else {
// Créer l'apprenant
await createApprenantMutation.mutateAsync(formData);
// Récupérer l'apprenant nouvellement créé
const newApprenant = await utils.client.apprenants.getByEmail.query({ email: formData.email });
if (!newApprenant) {
throw new Error("Erreur lors de la création de l'apprenant");
}
apprenantId = newApprenant.id;
}
// Inscrire l'apprenant à la sequence
await inscriptionMutation.mutateAsync({
apprenantId,
sequenceId: selectedSequenceId,
});
} catch (error: any) {
console.error("Erreur lors de l'inscription:", error);
}
};
// Calculer les places disponibles pour chaque sequence
const sessionsAvecPlaces = useMemo(() => {
if (!sequences) return [];
return sequences.map(sequence => {
const now = new Date();
const dateBlocage = new Date(sequence.dateBlocage);
const isBloquee = sequence.statut === 'bloquee' || now >= dateBlocage;
const placesRestantes = Math.max(0, sequence.capaciteMax - (sequence.nbInscrits || 0));
return {
...sequence,
isBloquee,
placesRestantes,
};
});
}, [sequences]);
const formatDate = (dateValue: Date | string) => {
if (!dateValue) return "N/A";
try {
let localDate: Date;
// Si c'est déjà un objet Date JavaScript, l'utiliser directement
if (dateValue instanceof Date) {
localDate = dateValue;
} else {
// Parser la date MySQL sans conversion UTC
const dateStr = String(dateValue);
const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})[T ]?(\d{2}):(\d{2})/);
if (!match) {
console.warn('Format de date non reconnu:', dateValue);
return "N/A";
}
const [, year, month, day, hours, minutes] = match;
// Créer une Date en heure locale
localDate = new Date(
parseInt(year),
parseInt(month) - 1,
parseInt(day),
parseInt(hours),
parseInt(minutes)
);
}
return format(localDate, "EEEE d MMMM yyyy 'à' HH:mm", { locale: fr });
} catch (error) {
console.error('Erreur de formatage de date:', error, dateValue);
return "N/A";
}
};
if (loadingFormation || loadingSessions) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<Card className="w-full max-w-2xl">
<CardHeader>
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-96 mt-2" />
</CardHeader>
<CardContent>
<Skeleton className="h-64 w-full" />
</CardContent>
</Card>
</div>
);
}
if (!formation || !formation.actif) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<Card className="w-full max-w-2xl">
<CardHeader>
<div className="flex items-center gap-2 text-destructive">
<XCircle className="w-6 h-6" />
<CardTitle>Formation introuvable</CardTitle>
</div>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Cette formation n'existe pas ou n'est plus disponible.
</p>
</CardContent>
</Card>
</div>
);
}
if (inscriptionSuccess) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<Card className="w-full max-w-2xl">
<CardHeader>
<div className="flex items-center gap-2 text-green-600">
<CheckCircle className="w-8 h-8" />
<CardTitle className="text-2xl">Inscription réussie !</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-lg">
{inscriptionStatut === "confirmee"
? "Votre inscription à la formation a été confirmée."
: "Vous avez été ajouté à la liste d'attente. Nous vous contacterons si une place se libère."}
</p>
<div className="bg-blue-50 p-4 rounded-lg space-y-2">
<p className="font-semibold">Prochaines étapes :</p>
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
<li>Vous recevrez un email de confirmation avec tous les détails</li>
<li>Une invitation Outlook sera envoyée pour bloquer votre agenda</li>
<li>Un email de rappel vous sera envoyé 7 jours avant la formation</li>
</ul>
</div>
<p className="text-sm text-muted-foreground">
Pour toute question, veuillez contacter le service RH.
</p>
</CardContent>
</Card>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
<div className="max-w-4xl mx-auto space-y-6">
{/* Header */}
<div className="text-center space-y-4">
<div className="flex items-center justify-center gap-3">
<img src={APP_LOGO} alt={APP_TITLE} className="h-12 w-12" />
<h1 className="text-4xl font-bold text-gray-900">{formation.nom}</h1>
</div>
{formation.description && (
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
{formation.description}
</p>
)}
</div>
{/* Sessions disponibles */}
<Card>
<CardHeader>
<CardTitle>Sessions disponibles</CardTitle>
<CardDescription>
Sélectionnez une sequence pour vous inscrire (capacité : 12 participants maximum)
</CardDescription>
</CardHeader>
<CardContent>
{sessionsAvecPlaces && sessionsAvecPlaces.length > 0 ? (
<div className="space-y-3">
{sessionsAvecPlaces.map((sequence) => (
<div
key={sequence.id}
className={`border rounded-lg p-4 cursor-pointer transition-all ${
selectedSequenceId === sequence.id
? "border-blue-500 bg-blue-50"
: sequence.isBloquee
? "border-gray-300 bg-gray-50 cursor-not-allowed opacity-60"
: "border-gray-300 hover:border-blue-300 hover:bg-blue-50/50"
}`}
onClick={() => !sequence.isBloquee && setSelectedSequenceId(sequence.id)}
>
<div className="flex items-start justify-between">
<div className="space-y-2 flex-1">
<h3 className="font-semibold text-lg">{sequence.nom}</h3>
<div className="space-y-1 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<div className="space-y-0.5">
{sequence.dates.map((date: any) => (
<div key={date.id}>
<span className="font-medium">Date {date.ordre}:</span> {formatDate(date.dateDebut)}
</div>
))}
</div>
</div>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
<span>{sequence.lieu}</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
<span>Public cible : {sequence.publicCible === "directeur" ? "Directeurs" : sequence.publicCible === "chef_service" ? "Chefs de service" : "Autre"}</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
<span>{sequence.placesRestantes} places disponibles</span>
</div>
</div>
</div>
<div>
{sequence.isBloquee ? (
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-200 text-gray-800">
<XCircle className="w-3 h-3 mr-1" />
Fermée
</span>
) : sequence.placesRestantes === 0 ? (
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
<AlertCircle className="w-3 h-3 mr-1" />
Complète
</span>
) : (
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
<CheckCircle className="w-3 h-3 mr-1" />
Disponible
</span>
)}
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
Aucune sequence disponible pour le moment.
</div>
)}
</CardContent>
</Card>
{/* Formulaire d'inscription */}
{selectedSequenceId && (
<Card>
<CardHeader>
<CardTitle>Vos informations</CardTitle>
<CardDescription>
Complétez le formulaire pour finaliser votre inscription
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="nom">Nom *</Label>
<Input
id="nom"
value={formData.nom}
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
placeholder="Dupont"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="prenom">Prénom *</Label>
<Input
id="prenom"
value={formData.prenom}
onChange={(e) => setFormData({ ...formData, prenom: e.target.value })}
placeholder="Jean"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email professionnel *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="jean.dupont@example.com"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="codeEtablissement">Code établissement *</Label>
<Input
id="codeEtablissement"
value={formData.codeEtablissement}
onChange={(e) => setFormData({ ...formData, codeEtablissement: e.target.value })}
placeholder="ETB001"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="fonction">Fonction *</Label>
<Select value={formData.fonction} onValueChange={(value: any) => setFormData({ ...formData, fonction: value })}>
<SelectTrigger id="fonction">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="directeur">Directeur</SelectItem>
<SelectItem value="chef_service">Chef de service</SelectItem>
<SelectItem value="autre">Autre</SelectItem>
</SelectContent>
</Select>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-sm text-amber-800">
<strong>Important :</strong> Les inscriptions seront bloquées 15 jours avant le début de la sequence.
Après cette date, aucune modification ne sera possible.
</p>
</div>
<Button
type="submit"
className="w-full"
disabled={inscriptionMutation.isPending || createApprenantMutation.isPending}
>
{inscriptionMutation.isPending || createApprenantMutation.isPending
? "Inscription en cours..."
: "Confirmer mon inscription"}
</Button>
</form>
</CardContent>
</Card>
)}
</div>
</div>
);
}