From 023711df922c4bcbf6fc874e34e152c4ba0f17dd Mon Sep 17 00:00:00 2001 From: Manus Date: Wed, 7 Jan 2026 10:06:29 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Syst=C3=A8me=20d'attestations=20d?= =?UTF-8?q?e=20formation=20impl=C3=A9ment=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Infrastructure backend : - Tables attestations et configAttestation en base de données - Service de génération PDF avec PDFKit (attestationService.ts) - Génération automatique de PDF avec template personnalisable - Upload automatique vers S3 avec URLs publiques - Procédures tRPC pour générer, récupérer et configurer ✅ Interface utilisateur : - Page de configuration des attestations (/admin/attestations) - Personnalisation du texte avec variables dynamiques (nomComplet, nomFormation, dateDebut, dateFin) - Configuration du signataire (nom et fonction) - Bouton "Générer" dans la page de validation des présences formateur - Génération automatique avec ouverture du PDF dans un nouvel onglet ✅ Fonctionnalités : - Vérification des doublons (une seule attestation par apprenant/séquence) - Génération uniquement pour les apprenants marqués comme présents - PDF professionnel avec détails de la formation et dates - Stockage permanent sur S3 📋 Améliorations futures possibles : - Upload de logo et signature personnalisés - Envoi automatique par email aux apprenants - Espace apprenant pour télécharger les attestations --- .manus/db/db-query-1767798017521.json | 13 + client/src/App.tsx | 2 + client/src/components/DashboardLayout.tsx | 3 +- client/src/pages/admin/AdminAttestations.tsx | 155 ++ .../src/pages/formateur/FormateurSequence.tsx | 45 +- drizzle/meta/0024_snapshot.json | 2028 +++++++++++++++++ drizzle/meta/0025_snapshot.json | 1848 +++++++++++++++ drizzle/meta/0026_snapshot.json | 2028 +++++++++++++++++ drizzle/meta/_journal.json | 21 + drizzle/schema.ts | 55 + package.json | 2 + pnpm-lock.yaml | 124 + server/attestationService.ts | 269 +++ server/routers.ts | 92 + todo.md | 8 + 15 files changed, 6691 insertions(+), 2 deletions(-) create mode 100644 .manus/db/db-query-1767798017521.json create mode 100644 client/src/pages/admin/AdminAttestations.tsx create mode 100644 drizzle/meta/0024_snapshot.json create mode 100644 drizzle/meta/0025_snapshot.json create mode 100644 drizzle/meta/0026_snapshot.json create mode 100644 server/attestationService.ts diff --git a/.manus/db/db-query-1767798017521.json b/.manus/db/db-query-1767798017521.json new file mode 100644 index 0000000..1624295 --- /dev/null +++ b/.manus/db/db-query-1767798017521.json @@ -0,0 +1,13 @@ +{ + "query": "SHOW TABLES LIKE 'attestations';", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SHOW TABLES LIKE 'attestations';", + "rows": [ + { + "Tables_in_7PAT67UmWoxv8vwp8Bbcv6 (attestations)": "attestations" + } + ], + "messages": [], + "stdout": "Tables_in_7PAT67UmWoxv8vwp8Bbcv6 (attestations)\nattestations\n", + "stderr": "", + "execution_time_ms": 53 +} \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 561de3f..944af09 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -29,6 +29,7 @@ import AdminRappelsHistorique from "./pages/admin/AdminRappelsHistorique"; import AdminRappelsStats from "./pages/admin/AdminRappelsStats"; import AdminFormateurs from "./pages/admin/AdminFormateurs"; import AdminNotifications from "./pages/admin/AdminNotifications"; +import AdminAttestations from "./pages/admin/AdminAttestations"; import QuestionnaireReponse from "./pages/QuestionnaireReponse"; import Inscription from "./pages/Inscription"; import Login from "./pages/Login"; @@ -53,6 +54,7 @@ function Router() { + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index b9026e7..bbbb23a 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -21,7 +21,7 @@ import { } from "@/components/ui/sidebar"; import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; -import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings, TrendingUp, Building2, FileSpreadsheet, ClipboardList, ChevronDown, User } from "lucide-react"; +import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings, TrendingUp, Building2, FileSpreadsheet, ClipboardList, ChevronDown, User, FileText } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; @@ -57,6 +57,7 @@ const menuSections = [ { icon: Mail, label: "Templates d'emails", path: "/admin/email-templates" }, { icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" }, { icon: Mail, label: "Notifications", path: "/admin/notifications" }, + { icon: FileText, label: "Attestations", path: "/admin/attestations" }, ] }, { diff --git a/client/src/pages/admin/AdminAttestations.tsx b/client/src/pages/admin/AdminAttestations.tsx new file mode 100644 index 0000000..33bbfa6 --- /dev/null +++ b/client/src/pages/admin/AdminAttestations.tsx @@ -0,0 +1,155 @@ +import { useState, useEffect } from "react"; +import { trpc } from "@/lib/trpc"; +import DashboardLayout from "@/components/DashboardLayout"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { toast } from "sonner"; +import { FileText, Save } from "lucide-react"; + +export default function AdminAttestations() { + const { data: config, isLoading, refetch } = trpc.attestations.getConfig.useQuery(); + const updateConfig = trpc.attestations.updateConfig.useMutation(); + + const [formData, setFormData] = useState({ + nomSignataire: "", + fonctionSignataire: "", + texteAttestation: "", + }); + + // Mettre à jour le formulaire quand les données sont chargées + useEffect(() => { + if (config) { + setFormData({ + nomSignataire: config.nomSignataire || "", + fonctionSignataire: config.fonctionSignataire || "", + texteAttestation: config.texteAttestation || "", + }); + } + }, [config]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + await updateConfig.mutateAsync(formData); + toast.success("Configuration enregistrée avec succès"); + refetch(); + } catch (error) { + toast.error("Erreur lors de l'enregistrement"); + console.error(error); + } + }; + + if (isLoading) { + return ( + +
+

Chargement...

+
+
+ ); + } + + return ( + +
+
+ +
+

Configuration des attestations

+

+ Personnalisez le contenu et la signature des attestations de formation +

+
+
+ +
+ + + Informations du signataire + + Ces informations apparaîtront en bas de l'attestation + + + +
+ + + setFormData({ ...formData, nomSignataire: e.target.value }) + } + placeholder="Ex: Jean Dupont" + /> +
+ +
+ + + setFormData({ ...formData, fonctionSignataire: e.target.value }) + } + placeholder="Ex: Directeur de la formation" + /> +
+
+
+ + + + Texte de l'attestation + + Personnalisez le texte principal de l'attestation. Utilisez les variables suivantes : +
    +
  • {'{{nomComplet}}'} - Nom complet de l'apprenant
  • +
  • {'{{nomFormation}}'} - Nom de la formation
  • +
  • {'{{dateDebut}}'} - Date de début de la formation
  • +
  • {'{{dateFin}}'} - Date de fin de la formation
  • +
+
+
+ +