Checkpoint: Refonte majeure : transformation du système de sessions en séquences avec support de jusqu'à 4 dates par séquence. L'inscription à une séquence inscrit automatiquement l'apprenant à toutes les dates. Mise à jour complète du backend (schéma DB, procédures tRPC, emails, exports) et du frontend (AdminSequences, AdminSequenceInscrits, Inscription, Admin). Toutes les fonctionnalités existantes (filtres, statistiques, exports PDF/Excel, emails automatiques) sont préservées et adaptées au nouveau modèle.

This commit is contained in:
Manus Sandbox
2025-11-12 09:57:58 -05:00
parent ca9b1c3d37
commit 5c9b6637c4
20 changed files with 2445 additions and 1410 deletions

View File

@@ -24,12 +24,12 @@ export default function Inscription() {
codeEtablissement: "",
fonction: "autre" as "directeur" | "chef_service" | "autre",
});
const [selectedSessionId, setSelectedSessionId] = useState<number | null>(null);
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: sessions, isLoading: loadingSessions } = trpc.sessions.listByFormation.useQuery(
const { data: sequences, isLoading: loadingSessions } = trpc.sequences.listByFormation.useQuery(
{ formationId: formation?.id || 0 },
{ enabled: !!formation?.id }
);
@@ -39,7 +39,7 @@ export default function Inscription() {
onSuccess: (data) => {
setInscriptionSuccess(true);
setInscriptionStatut(data.statut);
utils.sessions.listByFormation.invalidate({ formationId: formation?.id || 0 });
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 !");
@@ -57,8 +57,8 @@ export default function Inscription() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedSessionId) {
toast.error("Veuillez sélectionner une session");
if (!selectedSequenceId) {
toast.error("Veuillez sélectionner une sequence");
return;
}
@@ -81,32 +81,32 @@ export default function Inscription() {
apprenantId = newApprenant.id;
}
// Inscrire l'apprenant à la session
// Inscrire l'apprenant à la sequence
await inscriptionMutation.mutateAsync({
apprenantId,
sessionId: selectedSessionId,
sequenceId: selectedSequenceId,
});
} catch (error: any) {
console.error("Erreur lors de l'inscription:", error);
}
};
// Calculer les places disponibles pour chaque session
// Calculer les places disponibles pour chaque sequence
const sessionsAvecPlaces = useMemo(() => {
if (!sessions) return [];
if (!sequences) return [];
return sessions.map(session => {
return sequences.map(sequence => {
const now = new Date();
const dateBlocage = new Date(session.dateBlocage);
const isBloquee = session.statut === 'bloquee' || now >= dateBlocage;
const dateBlocage = new Date(sequence.dateBlocage);
const isBloquee = sequence.statut === 'bloquee' || now >= dateBlocage;
return {
...session,
...sequence,
isBloquee,
placesRestantes: session.capaciteMax,
placesRestantes: sequence.capaciteMax,
};
});
}, [sessions]);
}, [sequences]);
const formatDate = (date: Date | string) => {
return format(new Date(date), "EEEE d MMMM yyyy 'à' HH:mm", { locale: fr });
@@ -202,49 +202,55 @@ export default function Inscription() {
<CardHeader>
<CardTitle>Sessions disponibles</CardTitle>
<CardDescription>
Sélectionnez une session pour vous inscrire (capacité : 12 participants maximum)
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((session) => (
{sessionsAvecPlaces.map((sequence) => (
<div
key={session.id}
key={sequence.id}
className={`border rounded-lg p-4 cursor-pointer transition-all ${
selectedSessionId === session.id
selectedSequenceId === sequence.id
? "border-blue-500 bg-blue-50"
: session.isBloquee
: 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={() => !session.isBloquee && setSelectedSessionId(session.id)}
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">{session.nom}</h3>
<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" />
<span>{formatDate(session.dateDebut)}</span>
<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>{session.lieu}</span>
<span>{sequence.lieu}</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
<span>{session.placesRestantes} places disponibles</span>
<span>{sequence.placesRestantes} places disponibles</span>
</div>
</div>
</div>
<div>
{session.isBloquee ? (
{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>
) : session.placesRestantes === 0 ? (
) : 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
@@ -262,14 +268,14 @@ export default function Inscription() {
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
Aucune session disponible pour le moment.
Aucune sequence disponible pour le moment.
</div>
)}
</CardContent>
</Card>
{/* Formulaire d'inscription */}
{selectedSessionId && (
{selectedSequenceId && (
<Card>
<CardHeader>
<CardTitle>Vos informations</CardTitle>
@@ -338,7 +344,7 @@ export default function Inscription() {
<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 session.
<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>