Rollback to 641200f3
This commit is contained in:
@@ -1,432 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user