From 0fc35596d94a311d85e5e0db0ec472d8aa858df7 Mon Sep 17 00:00:00 2001 From: Manus Sandbox Date: Wed, 12 Nov 2025 08:38:33 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Application=20compl=C3=A8te=20de?= =?UTF-8?q?=20gestion=20des=20formations=20Manager=20Itinova=20avec=20tout?= =?UTF-8?q?es=20les=20fonctionnalit=C3=A9s=20:=20gestion=20des=20formation?= =?UTF-8?q?s/sessions/apprenants,=20syst=C3=A8me=20d'inscription=20avec=20?= =?UTF-8?q?validations,=20g=C3=A9n=C3=A9ration=20d'invitations=20Outlook,?= =?UTF-8?q?=20emails=20automatiques,=20exports=20PDF/Excel,=20et=20documen?= =?UTF-8?q?tation=20compl=C3=A8te.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitkeep | 0 DOCUMENTATION.md | 285 +++++++++++++ client/src/App.tsx | 23 +- client/src/components/DashboardLayout.tsx | 14 +- client/src/pages/Admin.tsx | 92 +++++ client/src/pages/AdminApprenants.tsx | 244 +++++++++++ client/src/pages/AdminFormations.tsx | 265 ++++++++++++ client/src/pages/AdminSessionInscrits.tsx | 301 ++++++++++++++ client/src/pages/AdminSessions.tsx | 377 +++++++++++++++++ client/src/pages/Home.tsx | 191 ++++++++- client/src/pages/Inscription.tsx | 346 ++++++++++++++++ drizzle/0000_charming_grey_gargoyle.sql | 13 + drizzle/0001_smooth_warbird.sql | 54 +++ drizzle/meta/0000_snapshot.json | 110 +++++ drizzle/meta/0001_snapshot.json | 473 ++++++++++++++++++++++ drizzle/meta/_journal.json | 19 +- drizzle/schema.ts | 90 +++- package.json | 3 + pnpm-lock.yaml | 240 +++++++++++ server/db.ts | 243 ++++++++++- server/emailService.ts | 283 +++++++++++++ server/exportService.ts | 191 +++++++++ server/icsGenerator.ts | 109 +++++ server/routers.ts | 419 ++++++++++++++++++- todo.md | 58 +++ 25 files changed, 4391 insertions(+), 52 deletions(-) create mode 100644 .gitkeep create mode 100644 DOCUMENTATION.md create mode 100644 client/src/pages/Admin.tsx create mode 100644 client/src/pages/AdminApprenants.tsx create mode 100644 client/src/pages/AdminFormations.tsx create mode 100644 client/src/pages/AdminSessionInscrits.tsx create mode 100644 client/src/pages/AdminSessions.tsx create mode 100644 client/src/pages/Inscription.tsx create mode 100644 drizzle/0000_charming_grey_gargoyle.sql create mode 100644 drizzle/0001_smooth_warbird.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 server/emailService.ts create mode 100644 server/exportService.ts create mode 100644 server/icsGenerator.ts create mode 100644 todo.md diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..4afb8fb --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,285 @@ +# Documentation - Gestion des Formations Manager Itinova + +## Vue d'ensemble + +L'application **Gestion des Formations Manager Itinova** est une solution complète développée pour gérer l'organisation interne des formations destinées à environ 150 apprenants entre février et octobre 2026, puis 3 sessions par an. Elle permet à la DRH (assistante formation, responsable Développement RH) et à la Directrice Qualité et Transformation de gérer efficacement les inscriptions, les sessions, et les communications automatisées. + +## Fonctionnalités principales + +### 1. Gestion des formations + +L'application permet de créer et gérer plusieurs formations en parallèle. Chaque formation dispose d'un lien unique d'inscription qui peut être partagé avec les apprenants. + +**Caractéristiques :** +- Création de formations avec nom, description et lien unique personnalisé +- Activation/désactivation des formations +- Génération automatique de liens d'inscription distincts pour chaque formation + +### 2. Gestion des sessions + +Les sessions sont les instances concrètes d'une formation avec des dates, un lieu et une capacité définis. + +**Caractéristiques :** +- Création de sessions avec dates de début et fin +- Définition du lieu de formation +- Capacité maximale fixée à 12 participants par session +- Date de blocage automatique à J-15 avant le début de la session +- Statuts de session : ouverte, bloquée, terminée + +### 3. Gestion des apprenants + +L'application maintient une base de données des apprenants avec leurs informations essentielles. + +**Informations collectées :** +- Nom et prénom +- Email professionnel +- Code établissement + +### 4. Système d'inscription + +Les apprenants peuvent s'inscrire via un lien unique propre à chaque formation. Le système applique automatiquement toutes les contraintes définies. + +**Validations automatiques :** +- Vérification de la capacité maximale (12 participants) +- Prévention des doubles inscriptions +- Blocage des inscriptions et désinscriptions à J-15 +- Gestion automatique de la liste d'attente si la session est complète + +### 5. Invitations et communications automatiques + +Dès qu'un apprenant s'inscrit, le système génère et envoie automatiquement une invitation Outlook avec tous les détails de la formation. + +**Fonctionnalités d'envoi :** +- **Email de confirmation** : envoyé immédiatement après l'inscription avec invitation Outlook (.ics) en pièce jointe +- **Invitation Outlook** : fichier .ics avec statut "occupé" incluant le lieu de formation +- **Email teaser** : peut être envoyé manuellement par les administrateurs à tous les inscrits d'une session +- **Email de rappel J-7** : peut être envoyé manuellement 7 jours avant la formation + +### 6. Exports et feuilles de présence + +Les administrateurs peuvent exporter les listes d'inscrits dans différents formats pour faciliter la gestion administrative. + +**Formats disponibles :** +- **Export Excel** : liste complète des inscrits avec toutes leurs informations +- **Export PDF** : liste des inscrits au format PDF +- **Feuille de présence PDF** : document prêt à imprimer avec nom, prénom, code établissement et colonne signature + +## Guide d'utilisation + +### Pour les administrateurs + +#### Connexion + +1. Accédez à l'application via l'URL fournie +2. Cliquez sur "Se connecter" +3. Utilisez vos identifiants Manus OAuth +4. Vous serez redirigé vers le tableau de bord administrateur + +#### Créer une formation + +1. Dans le menu latéral, cliquez sur **Formations** +2. Cliquez sur le bouton **Nouvelle formation** +3. Remplissez les informations : + - Nom de la formation (ex: "Manager Itinova") + - Description (optionnelle) + - Lien unique (ex: "manager-itinova-2026") + - Cochez "Formation active" pour la rendre disponible +4. Cliquez sur **Créer** +5. Le lien d'inscription sera généré automatiquement : `/inscription/[votre-lien-unique]` + +#### Créer une session + +1. Dans le menu latéral, cliquez sur **Sessions** +2. Cliquez sur le bouton **Nouvelle session** +3. Remplissez les informations : + - Sélectionnez la formation associée + - Nom de la session (ex: "Session 1 - Février 2026") + - Date et heure de début + - Date et heure de fin + - Lieu de la formation (sera inclus dans l'invitation Outlook) + - Capacité maximale (par défaut 12) + - Date de blocage (J-15 avant le début) + - Statut (ouverte, bloquée, terminée) +4. Cliquez sur **Créer** + +#### Gérer les inscriptions + +1. Dans le menu **Sessions**, cliquez sur l'icône **Utilisateurs** à côté d'une session +2. Vous verrez : + - Le nombre de places confirmées sur la capacité totale + - Le nombre de personnes en liste d'attente + - La liste complète des inscrits avec leurs informations +3. Actions disponibles : + - Modifier le statut d'une inscription (confirmée, liste d'attente, annulée) + - Exporter la liste en Excel ou PDF + - Générer une feuille de présence + - Envoyer des emails groupés (teaser ou rappel J-7) + +#### Envoyer des emails groupés + +1. Accédez à la page des inscrits d'une session +2. Cliquez sur **Envoyer teaser** pour envoyer un email d'introduction à tous les inscrits confirmés +3. Cliquez sur **Envoyer rappel J-7** pour envoyer un rappel 7 jours avant la formation +4. Un message de confirmation indiquera le nombre d'emails envoyés avec succès + +#### Exporter les données + +1. Accédez à la page des inscrits d'une session +2. Cliquez sur : + - **Exporter Excel** : télécharge un fichier .xlsx avec toutes les informations + - **Exporter PDF** : télécharge un fichier PDF avec la liste des inscrits + - **Feuille de présence** : télécharge un PDF prêt à imprimer pour la signature des participants + +### Pour les apprenants + +#### S'inscrire à une formation + +1. Cliquez sur le lien d'inscription fourni par la DRH (ex: `https://[domaine]/inscription/manager-itinova-2026`) +2. Consultez les sessions disponibles avec : + - Les dates et horaires + - Le lieu de formation + - Le nombre de places disponibles + - Le statut (disponible, complète, fermée) +3. Sélectionnez la session souhaitée en cliquant dessus +4. Remplissez vos informations : + - Nom + - Prénom + - Email professionnel + - Code établissement +5. Cliquez sur **Confirmer mon inscription** +6. Vous recevrez immédiatement : + - Un email de confirmation + - Une invitation Outlook (.ics) à ajouter à votre calendrier + +#### Que se passe-t-il après l'inscription ? + +- **Si votre inscription est confirmée** : vous recevrez une invitation Outlook qui bloquera automatiquement votre agenda avec le statut "occupé" +- **Si la session est complète** : vous serez placé en liste d'attente et contacté si une place se libère +- **15 jours avant la formation** : les inscriptions et désinscriptions seront bloquées +- **7 jours avant la formation** : vous recevrez un email de rappel avec tous les détails pratiques + +## Gestion des rôles et permissions + +L'application utilise un système de rôles pour contrôler l'accès aux fonctionnalités : + +### Rôle Administrateur + +Accordé aux membres de la DRH (assistante formation, responsable Développement RH, Directrice Qualité et Transformation, stagiaire). + +**Permissions :** +- Accès complet au tableau de bord administrateur +- Création, modification et suppression de formations +- Création, modification et suppression de sessions +- Gestion des apprenants +- Consultation et modification des inscriptions +- Envoi d'emails groupés +- Export des données + +### Rôle Utilisateur + +Les apprenants qui s'inscrivent via le lien public. + +**Permissions :** +- Accès au formulaire d'inscription public +- Consultation des sessions disponibles +- Inscription à une session (dans les limites définies) + +## Contraintes et règles métier + +### Capacité des sessions + +Chaque session est limitée à **12 participants maximum**. Au-delà, les apprenants sont automatiquement placés en liste d'attente. + +### Blocage à J-15 + +Les inscriptions et désinscriptions sont automatiquement bloquées **15 jours avant le début de la session**. Cette règle garantit la stabilité des groupes et évite les désistements de dernière minute. + +### Prévention des doubles inscriptions + +Le système empêche un apprenant de s'inscrire plusieurs fois à la même session. Si l'apprenant existe déjà dans la base de données (même email), ses informations sont réutilisées. + +### Statuts des inscriptions + +- **Confirmée** : l'apprenant a une place garantie dans la session +- **Liste d'attente** : la session est complète, l'apprenant sera contacté si une place se libère +- **Annulée** : l'inscription a été annulée par l'apprenant ou l'administrateur + +## Architecture technique + +### Technologies utilisées + +L'application est construite avec une architecture moderne full-stack : + +**Frontend :** +- React 19 avec TypeScript +- Tailwind CSS 4 pour le design +- tRPC pour la communication type-safe avec le backend +- Wouter pour le routage +- shadcn/ui pour les composants d'interface + +**Backend :** +- Node.js avec Express +- tRPC pour les API type-safe +- Drizzle ORM pour l'accès à la base de données +- MySQL/TiDB comme base de données + +**Fonctionnalités :** +- Génération de fichiers ICS (iCalendar) pour Outlook +- Export Excel avec xlsx +- Export PDF avec jsPDF +- Système d'emails automatisés + +### Base de données + +Le schéma de base de données comprend 5 tables principales : + +1. **users** : utilisateurs authentifiés (administrateurs) +2. **formations** : catalogue des formations +3. **sessions** : instances de formations avec dates et lieux +4. **apprenants** : base de données des participants +5. **inscriptions** : relation entre apprenants et sessions avec statuts + +## Confidentialité et sécurité + +L'application respecte les principes de confidentialité des données : + +- **Authentification sécurisée** : via Manus OAuth pour les administrateurs +- **Contrôle d'accès** : seuls les administrateurs peuvent accéder aux données sensibles +- **Protection des données** : les informations personnelles des apprenants sont stockées de manière sécurisée +- **Emails professionnels** : seuls les emails professionnels sont collectés + +## Support et maintenance + +### Contact + +Pour toute question ou problème technique, contactez le service RH ou l'équipe de développement. + +### Évolutions futures + +L'application est conçue pour être évolutive. Des fonctionnalités supplémentaires peuvent être ajoutées selon les besoins : + +- Intégration avec un vrai service SMTP pour l'envoi d'emails +- Tableau de bord analytique avec statistiques de participation +- Système de notation et d'évaluation post-formation +- Gestion des formateurs et de leurs disponibilités +- Notifications push pour les rappels + +## Notes importantes + +### Emails simulés + +Dans la version actuelle, les emails sont **simulés** et affichés dans les logs du serveur. Pour une utilisation en production, il est nécessaire d'intégrer un service SMTP réel (comme SendGrid, Mailgun, ou un serveur SMTP d'entreprise). + +### Personnalisation + +Les templates d'emails peuvent être personnalisés dans le fichier `server/emailService.ts` pour correspondre à la charte graphique de l'entreprise. + +### Sauvegarde des données + +Il est recommandé de mettre en place une stratégie de sauvegarde régulière de la base de données pour prévenir toute perte de données. + +--- + +**Version de la documentation :** 1.0 +**Date de dernière mise à jour :** 2026-01-17 +**Auteur :** Manus AI diff --git a/client/src/App.tsx b/client/src/App.tsx index 5c7a610..f8d07b7 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,12 +5,23 @@ import { Route, Switch } from "wouter"; import ErrorBoundary from "./components/ErrorBoundary"; import { ThemeProvider } from "./contexts/ThemeContext"; import Home from "./pages/Home"; +import Admin from "./pages/Admin"; +import AdminFormations from "./pages/AdminFormations"; +import AdminSessions from "./pages/AdminSessions"; +import AdminApprenants from "./pages/AdminApprenants"; +import AdminSessionInscrits from "./pages/AdminSessionInscrits"; +import Inscription from "./pages/Inscription"; function Router() { - // make sure to consider if you need authentication for certain routes return ( + + + + + + {/* Final fallback route */} @@ -18,18 +29,10 @@ function Router() { ); } -// NOTE: About Theme -// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css -// to keep consistent foreground/background color across components -// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook - function App() { return ( - + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 11ed6d0..790419e 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -21,15 +21,17 @@ import { } from "@/components/ui/sidebar"; import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; -import { LayoutDashboard, LogOut, PanelLeft, Users } from "lucide-react"; +import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; import { Button } from "./ui/button"; const menuItems = [ - { icon: LayoutDashboard, label: "Page 1", path: "/" }, - { icon: Users, label: "Page 2", path: "/some-path" }, + { icon: LayoutDashboard, label: "Tableau de bord", path: "/admin" }, + { icon: GraduationCap, label: "Formations", path: "/admin/formations" }, + { icon: Calendar, label: "Sessions", path: "/admin/sessions" }, + { icon: Users, label: "Apprenants", path: "/admin/apprenants" }, ]; const SIDEBAR_WIDTH_KEY = "sidebar-width"; @@ -73,7 +75,7 @@ export default function DashboardLayout({

{APP_TITLE}

- Please sign in to continue + Veuillez vous connecter pour continuer

@@ -84,7 +86,7 @@ export default function DashboardLayout({ size="lg" className="w-full shadow-lg hover:shadow-xl transition-all" > - Sign in + Se connecter @@ -255,7 +257,7 @@ function DashboardLayoutContent({ className="cursor-pointer text-destructive focus:text-destructive" > - Sign out + Déconnexion diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx new file mode 100644 index 0000000..1d490e7 --- /dev/null +++ b/client/src/pages/Admin.tsx @@ -0,0 +1,92 @@ +import DashboardLayout from "@/components/DashboardLayout"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { trpc } from "@/lib/trpc"; +import { GraduationCap, Calendar, Users, CheckCircle } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function Admin() { + const { data: formations, isLoading: loadingFormations } = trpc.formations.list.useQuery(); + const { data: sessions, isLoading: loadingSessions } = trpc.sessions.list.useQuery(); + const { data: apprenants, isLoading: loadingApprenants } = trpc.apprenants.list.useQuery(); + + const stats = [ + { + title: "Formations actives", + value: formations?.filter(f => f.actif).length || 0, + icon: , + loading: loadingFormations, + }, + { + title: "Sessions ouvertes", + value: sessions?.filter(s => s.statut === "ouverte").length || 0, + icon: , + loading: loadingSessions, + }, + { + title: "Apprenants inscrits", + value: apprenants?.length || 0, + icon: , + loading: loadingApprenants, + }, + { + title: "Sessions terminées", + value: sessions?.filter(s => s.statut === "terminee").length || 0, + icon: , + loading: loadingSessions, + }, + ]; + + return ( + +
+
+

Tableau de bord

+

+ Gestion des formations Manager Itinova +

+
+ +
+ {stats.map((stat, index) => ( + + + + {stat.title} + + {stat.icon} + + + {stat.loading ? ( + + ) : ( +
{stat.value}
+ )} +
+
+ ))} +
+ + + + Bienvenue dans l'espace d'administration + + Gérez vos formations, sessions et apprenants depuis le menu de navigation. + + + +
+

Fonctionnalités principales :

+
    +
  • Créer et gérer les formations avec liens d'inscription uniques
  • +
  • Organiser les sessions avec limitation à 12 participants
  • +
  • Gérer les inscriptions avec blocage automatique à J-15
  • +
  • Exporter les listes d'inscrits en PDF ou Excel
  • +
  • Générer des invitations Outlook et envoyer des emails automatiques
  • +
+
+
+
+
+
+ ); +} diff --git a/client/src/pages/AdminApprenants.tsx b/client/src/pages/AdminApprenants.tsx new file mode 100644 index 0000000..66f2b6d --- /dev/null +++ b/client/src/pages/AdminApprenants.tsx @@ -0,0 +1,244 @@ +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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { trpc } from "@/lib/trpc"; +import { Plus, Pencil, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function AdminApprenants() { + const [open, setOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + const [formData, setFormData] = useState({ + nom: "", + prenom: "", + email: "", + codeEtablissement: "", + }); + + const utils = trpc.useUtils(); + const { data: apprenants, isLoading } = trpc.apprenants.list.useQuery(); + + const createMutation = trpc.apprenants.create.useMutation({ + onSuccess: () => { + utils.apprenants.list.invalidate(); + toast.success("Apprenant créé avec succès"); + setOpen(false); + resetForm(); + }, + onError: (error) => { + toast.error("Erreur lors de la création : " + error.message); + }, + }); + + const updateMutation = trpc.apprenants.update.useMutation({ + onSuccess: () => { + utils.apprenants.list.invalidate(); + toast.success("Apprenant mis à jour avec succès"); + setOpen(false); + resetForm(); + }, + onError: (error) => { + toast.error("Erreur lors de la mise à jour : " + error.message); + }, + }); + + const deleteMutation = trpc.apprenants.delete.useMutation({ + onSuccess: () => { + utils.apprenants.list.invalidate(); + toast.success("Apprenant supprimé avec succès"); + }, + onError: (error) => { + toast.error("Erreur lors de la suppression : " + error.message); + }, + }); + + const resetForm = () => { + setFormData({ nom: "", prenom: "", email: "", codeEtablissement: "" }); + setEditingId(null); + }; + + const handleEdit = (apprenant: any) => { + setFormData({ + nom: apprenant.nom, + prenom: apprenant.prenom, + email: apprenant.email, + codeEtablissement: apprenant.codeEtablissement, + }); + setEditingId(apprenant.id); + setOpen(true); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (editingId) { + updateMutation.mutate({ id: editingId, ...formData }); + } else { + createMutation.mutate(formData); + } + }; + + const handleDelete = (id: number) => { + if (confirm("Êtes-vous sûr de vouloir supprimer cet apprenant ?")) { + deleteMutation.mutate({ id }); + } + }; + + return ( + +
+
+
+

Apprenants

+

+ Gérez les apprenants et leurs informations +

+
+ { + setOpen(isOpen); + if (!isOpen) resetForm(); + }}> + + + + +
+ + + {editingId ? "Modifier l'apprenant" : "Nouvel apprenant"} + + + Ajoutez les informations de l'apprenant + + +
+
+
+ + setFormData({ ...formData, nom: e.target.value })} + placeholder="Dupont" + required + /> +
+
+ + setFormData({ ...formData, prenom: e.target.value })} + placeholder="Jean" + required + /> +
+
+
+ + setFormData({ ...formData, email: e.target.value })} + placeholder="jean.dupont@example.com" + required + /> +
+
+ + setFormData({ ...formData, codeEtablissement: e.target.value })} + placeholder="ETB001" + required + /> +
+
+ + + + +
+
+
+
+ + + + Liste des apprenants + + Gérez les apprenants inscrits aux formations + + + + {isLoading ? ( +
+ + + +
+ ) : apprenants && apprenants.length > 0 ? ( + + + + Nom + Prénom + Email + Code établissement + Actions + + + + {apprenants.map((apprenant) => ( + + {apprenant.nom} + {apprenant.prenom} + {apprenant.email} + {apprenant.codeEtablissement} + +
+ + +
+
+
+ ))} +
+
+ ) : ( +
+ Aucun apprenant pour le moment. Créez-en un pour commencer. +
+ )} +
+
+
+
+ ); +} diff --git a/client/src/pages/AdminFormations.tsx b/client/src/pages/AdminFormations.tsx new file mode 100644 index 0000000..19b3aee --- /dev/null +++ b/client/src/pages/AdminFormations.tsx @@ -0,0 +1,265 @@ +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 { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { trpc } from "@/lib/trpc"; +import { Plus, Pencil, Trash2, ExternalLink } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function AdminFormations() { + const [open, setOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + const [formData, setFormData] = useState({ + nom: "", + description: "", + lienUnique: "", + actif: true, + }); + + const utils = trpc.useUtils(); + const { data: formations, isLoading } = trpc.formations.list.useQuery(); + const createMutation = trpc.formations.create.useMutation({ + onSuccess: () => { + utils.formations.list.invalidate(); + toast.success("Formation créée avec succès"); + setOpen(false); + resetForm(); + }, + onError: (error) => { + toast.error("Erreur lors de la création : " + error.message); + }, + }); + + const updateMutation = trpc.formations.update.useMutation({ + onSuccess: () => { + utils.formations.list.invalidate(); + toast.success("Formation mise à jour avec succès"); + setOpen(false); + resetForm(); + }, + onError: (error) => { + toast.error("Erreur lors de la mise à jour : " + error.message); + }, + }); + + const deleteMutation = trpc.formations.delete.useMutation({ + onSuccess: () => { + utils.formations.list.invalidate(); + toast.success("Formation supprimée avec succès"); + }, + onError: (error) => { + toast.error("Erreur lors de la suppression : " + error.message); + }, + }); + + const resetForm = () => { + setFormData({ nom: "", description: "", lienUnique: "", actif: true }); + setEditingId(null); + }; + + const handleEdit = (formation: any) => { + setFormData({ + nom: formation.nom, + description: formation.description || "", + lienUnique: formation.lienUnique, + actif: formation.actif, + }); + setEditingId(formation.id); + setOpen(true); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (editingId) { + updateMutation.mutate({ id: editingId, ...formData }); + } else { + createMutation.mutate(formData); + } + }; + + const handleDelete = (id: number) => { + if (confirm("Êtes-vous sûr de vouloir supprimer cette formation ?")) { + deleteMutation.mutate({ id }); + } + }; + + const getInscriptionUrl = (lien: string) => { + return `${window.location.origin}/inscription/${lien}`; + }; + + return ( + +
+
+
+

Formations

+

+ Gérez les formations et leurs liens d'inscription +

+
+ { + setOpen(isOpen); + if (!isOpen) resetForm(); + }}> + + + + +
+ + + {editingId ? "Modifier la formation" : "Nouvelle formation"} + + + Créez une formation avec un lien d'inscription unique + + +
+
+ + setFormData({ ...formData, nom: e.target.value })} + placeholder="Manager Itinova" + required + /> +
+
+ +