From 731a5b4d02b4a94edbd840b1729887e539f702c5 Mon Sep 17 00:00:00 2001 From: Manus Date: Mon, 19 Jan 2026 11:04:24 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20manuel=20d'apprenant=20:?= =?UTF-8?q?=20Bouton=20et=20dialogue=20pour=20inscrire=20manuellement=20un?= =?UTF-8?q?=20apprenant=20=C3=A0=20une=20s=C3=A9quence.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Fonctionnalité ajoutée :** - Bouton "Ajouter un inscrit" avec icône UserPlus dans le header de la liste des inscrits - Dialogue de sélection avec liste déroulante des apprenants disponibles (filtrés pour exclure ceux déjà inscrits) - Sélection du statut (Confirmé / Liste d'attente) - Mutation backend `inscriptions.create` pour l'ajout manuel par l'admin (sans vérifications de blocage/capacité) **Fichiers modifiés :** - client/src/pages/AdminSequenceInscrits.tsx : Ajout du bouton et du dialogue - client/src/components/AddInscritForm.tsx : Nouveau composant de formulaire - server/routers.ts : Ajout de la mutation inscriptions.create **Résultat :** Les administrateurs peuvent maintenant ajouter manuellement des apprenants à une séquence depuis l'interface des inscrits. --- client/src/components/AddInscritForm.tsx | 106 +++++++++++++++++++++ client/src/pages/AdminRappels.tsx | 4 +- client/src/pages/AdminSequenceInscrits.tsx | 46 ++++++++- server/routers.ts | 37 +++++++ todo.md | 15 +++ 5 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 client/src/components/AddInscritForm.tsx diff --git a/client/src/components/AddInscritForm.tsx b/client/src/components/AddInscritForm.tsx new file mode 100644 index 0000000..c47f813 --- /dev/null +++ b/client/src/components/AddInscritForm.tsx @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { trpc } from "@/lib/trpc"; +import { toast } from "sonner"; + +interface AddInscritFormProps { + sequenceId: number; + onSuccess: () => void; + onCancel: () => void; +} + +export function AddInscritForm({ sequenceId, onSuccess, onCancel }: AddInscritFormProps) { + const [selectedApprenantId, setSelectedApprenantId] = useState(""); + const [statut, setStatut] = useState<"confirme" | "liste_attente">("confirme"); + + // Récupérer tous les apprenants + const { data: apprenants, isLoading: loadingApprenants } = trpc.apprenants.list.useQuery(); + + // Récupérer les inscrits actuels pour filtrer + const { data: inscriptions } = trpc.inscriptions.listBySequence.useQuery({ sequenceId }); + + // Mutation pour créer l'inscription + const createInscription = trpc.inscriptions.create.useMutation({ + onSuccess: () => { + onSuccess(); + }, + onError: (error) => { + toast.error(`Erreur : ${error.message}`); + }, + }); + + // Filtrer les apprenants déjà inscrits + const apprenantsDisponibles = apprenants?.filter( + (apprenant) => !inscriptions?.some((inscription) => inscription.apprenantId === apprenant.id) + ) || []; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!selectedApprenantId) { + toast.error("Veuillez sélectionner un apprenant"); + return; + } + + createInscription.mutate({ + sequenceId, + apprenantId: parseInt(selectedApprenantId), + statut, + }); + }; + + return ( +
+
+ + {loadingApprenants ? ( +
Chargement...
+ ) : apprenantsDisponibles.length === 0 ? ( +
+ Tous les apprenants sont déjà inscrits à cette séquence +
+ ) : ( + + )} +
+ +
+ + +
+ +
+ + +
+
+ ); +} diff --git a/client/src/pages/AdminRappels.tsx b/client/src/pages/AdminRappels.tsx index d1a596b..988c627 100644 --- a/client/src/pages/AdminRappels.tsx +++ b/client/src/pages/AdminRappels.tsx @@ -30,8 +30,8 @@ import { DateFormationMultiSelect } from "@/components/DateFormationMultiSelect" // Types de templates disponibles pour les rappels const TEMPLATE_TYPES = [ - { value: "rappel", label: "Rappel 1", joursAvant: 7 }, - { value: "rappelJ1", label: "Rappel 2", joursAvant: 1 }, + { value: "rappel1", label: "Rappel 1", joursAvant: 7 }, + { value: "rappel2", label: "Rappel 2", joursAvant: 1 }, { value: "rappel3", label: "Rappel 3", joursAvant: 14 }, { value: "rappel4", label: "Rappel 4", joursAvant: 21 }, { value: "rappel5", label: "Rappel 5", joursAvant: 30 }, diff --git a/client/src/pages/AdminSequenceInscrits.tsx b/client/src/pages/AdminSequenceInscrits.tsx index e43de72..a0e159c 100644 --- a/client/src/pages/AdminSequenceInscrits.tsx +++ b/client/src/pages/AdminSequenceInscrits.tsx @@ -4,12 +4,13 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com 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 { ArrowLeft, Download, Mail, Search, Eye } from "lucide-react"; +import { ArrowLeft, Download, Mail, Search, Eye, UserPlus } from "lucide-react"; import { useLocation, useRoute } from "wouter"; import { useState, useMemo } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; +import { AddInscritForm } from "@/components/AddInscritForm"; import { toast } from "sonner"; import { Dialog, @@ -48,6 +49,8 @@ export default function AdminSequenceInscrits() { const [filterEtablissement, setFilterEtablissement] = useState("all"); const [filterFonction, setFilterFonction] = useState("all"); const [sortBy, setSortBy] = useState<"date" | "nom">("date"); + const [isAddInscritOpen, setIsAddInscritOpen] = useState(false); + const [selectedApprenantId, setSelectedApprenantId] = useState(null); const [showPreviewModal, setShowPreviewModal] = useState(false); const [previewType, setPreviewType] = useState<"teaser" | "rappel" | "rappel_j1">("teaser"); @@ -506,10 +509,22 @@ export default function AdminSequenceInscrits() { {/* Liste des inscrits */} - Liste des inscrits - - {nbConfirmes} confirmé(s), {nbListeAttente} en liste d'attente - +
+
+ Liste des inscrits + + {nbConfirmes} confirmé(s), {nbListeAttente} en liste d'attente + +
+ +
{filteredAndSortedInscriptions.length === 0 ? ( @@ -577,6 +592,27 @@ export default function AdminSequenceInscrits() {
+ {/* Dialogue d'ajout d'inscrit */} + + + + Ajouter un inscrit à la séquence + + Sélectionnez un apprenant à inscrire à cette séquence + + + { + setIsAddInscritOpen(false); + utils.inscriptions.getBySequence.invalidate({ sequenceId }); + toast.success("Apprenant inscrit avec succès"); + }} + onCancel={() => setIsAddInscritOpen(false)} + /> + + + {/* Modal d'aperçu d'email */} diff --git a/server/routers.ts b/server/routers.ts index f54eb5e..5e31902 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -1120,6 +1120,43 @@ export const appRouter = router({ filename: `feuille_presence_${sequence.nom.replace(/\s+/g, '_')}.pdf`, }; }), + + // Ajout manuel d'un inscrit par l'admin (sans vérifications de blocage/capacité) + create: adminProcedure.input(z.object({ + apprenantId: z.number(), + sequenceId: z.number(), + statut: z.enum(["confirme", "liste_attente"]), + })).mutation(async ({ input }) => { + // Vérifier si l'apprenant existe + const apprenant = await db.getApprenantById(input.apprenantId); + if (!apprenant) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Apprenant introuvable' }); + } + + // Vérifier si la séquence existe + const sequence = await db.getSequenceById(input.sequenceId); + if (!sequence) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' }); + } + + // Vérifier si l'apprenant est déjà inscrit + const existing = await db.checkExistingInscription(input.apprenantId, input.sequenceId); + if (existing) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'Cet apprenant est déjà inscrit à cette séquence' + }); + } + + // Créer l'inscription avec le statut choisi + await db.createInscription({ + apprenantId: input.apprenantId, + sequenceId: input.sequenceId, + statut: input.statut === "confirme" ? "confirmee" : "en_attente", + }); + + return { success: true }; + }), }), // ===== GESTION DES UTILISATEURS ===== diff --git a/todo.md b/todo.md index 7e5a0b0..b8c3bd5 100644 --- a/todo.md +++ b/todo.md @@ -949,3 +949,18 @@ - [x] Corriger la validation pour accepter rappel1-6 au lieu de rappel/rappelJ1 - [x] Tester la création d'un rappel - [x] Déployer sur le VPS + +## Recherche exhaustive des enums de types de rappels + +- [x] Rechercher tous les enums dans le code (backend + frontend) +- [x] Corriger tous les endroits manqués (TEMPLATE_TYPES dans AdminRappels.tsx) +- [x] Déployer et tester + +## Ajout manuel d'apprenant dans la fenêtre "Inscrits à la séquence" + +- [x] Analyser la fenêtre des inscrits (AdminSequenceInscrits.tsx) +- [x] Ajouter un bouton "Ajouter un inscrit" avec icône UserPlus dans le header +- [x] Créer un dialogue de sélection d'apprenant (AddInscritForm.tsx) +- [x] Implémenter la mutation backend inscriptions.create +- [x] Tester l'ajout manuel +- [x] Déployer sur le VPS