diff --git a/.manus/db/db-query-1766057305408.json b/.manus/db/db-query-1766057305408.json deleted file mode 100644 index ea08da2..0000000 --- a/.manus/db/db-query-1766057305408.json +++ /dev/null @@ -1,226 +0,0 @@ -{ - "query": "SELECT \n type,\n emailDestinataire,\n sujet,\n dateEnvoi,\n statut,\n sequenceId,\n apprenantId\nFROM logsNotifications \nWHERE type = 'remerciement'\nORDER BY dateEnvoi DESC\nLIMIT 50;", - "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 SELECT \n type,\n emailDestinataire,\n sujet,\n dateEnvoi,\n statut,\n sequenceId,\n apprenantId\nFROM logsNotifications \nWHERE type = 'remerciement'\nORDER BY dateEnvoi DESC\nLIMIT 50;", - "rows": [ - { - "type": "remerciement", - "emailDestinataire": "alerte2.test@example.com", - "sujet": "Merci pour votre participation - Formation manager Itinova", - "dateEnvoi": "2025-12-18 11:23:55", - "statut": "success", - "sequenceId": "30001", - "apprenantId": "90002" - }, - { - "type": "remerciement", - "emailDestinataire": "test.alerte@example.com", - "sujet": "Merci pour votre participation - Formation manager Itinova", - "dateEnvoi": "2025-12-18 09:42:30", - "statut": "success", - "sequenceId": "30001", - "apprenantId": "90001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation manager Itinova", - "dateEnvoi": "2025-12-18 09:42:27", - "statut": "success", - "sequenceId": "30001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 21:16:59", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 21:16:32", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 21:14:32", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 21:14:25", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 21:00:43", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 21:00:40", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:34:28", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:34:00", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:33:28", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:33:21", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:26:15", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:26:13", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:25:51", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:25:49", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:25:31", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:25:28", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:25:14", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:25:12", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "o.pareige@itinova.org", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:24:58", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "240001" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:24:55", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - }, - { - "type": "remerciement", - "emailDestinataire": "opareige@gmail.com", - "sujet": "Merci pour votre participation - Formation Jetski", - "dateEnvoi": "2025-12-15 20:24:49", - "statut": "success", - "sequenceId": "330001", - "apprenantId": "1" - } - ], - "messages": [], - "stdout": "type\temailDestinataire\tsujet\tdateEnvoi\tstatut\tsequenceId\tapprenantId\nremerciement\talerte2.test@example.com\tMerci pour votre participation - Formation manager Itinova\t2025-12-18 11:23:55\tsuccess\t30001\t90002\nremerciement\ttest.alerte@example.com\tMerci pour votre participation - Formation manager Itinova\t2025-12-18 09:42:30\tsuccess\t30001\t90001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation manager Itinova\t2025-12-18 09:42:27\tsuccess\t30001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 21:16:59\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 21:16:32\tsuccess\t330001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 21:14:32\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 21:14:25\tsuccess\t330001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 21:00:43\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 21:00:40\tsuccess\t330001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:34:28\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:34:00\tsuccess\t330001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:33:28\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:33:21\tsuccess\t330001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:26:15\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:26:13\tsuccess\t330001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:25:51\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:25:49\tsuccess\t330001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:25:31\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:25:28\tsuccess\t330001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:25:14\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:25:12\tsuccess\t330001\t1\nremerciement\to.pareige@itinova.org\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:24:58\tsuccess\t330001\t240001\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:24:55\tsuccess\t330001\t1\nremerciement\topareige@gmail.com\tMerci pour votre participation - Formation Jetski\t2025-12-15 20:24:49\tsuccess\t330001\t1\n", - "stderr": "", - "execution_time_ms": 168 -} \ No newline at end of file diff --git a/.manus/db/db-query-1766057309842.json b/.manus/db/db-query-1766057309842.json deleted file mode 100644 index 97236b6..0000000 --- a/.manus/db/db-query-1766057309842.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "query": "SELECT \n emailDestinataire,\n sequenceId,\n COUNT(*) as nb_envois,\n MIN(dateEnvoi) as premier_envoi,\n MAX(dateEnvoi) as dernier_envoi\nFROM logsNotifications \nWHERE type = 'remerciement' AND statut = 'success'\nGROUP BY emailDestinataire, sequenceId\nHAVING COUNT(*) > 1\nORDER BY nb_envois DESC, dernier_envoi DESC;", - "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 SELECT \n emailDestinataire,\n sequenceId,\n COUNT(*) as nb_envois,\n MIN(dateEnvoi) as premier_envoi,\n MAX(dateEnvoi) as dernier_envoi\nFROM logsNotifications \nWHERE type = 'remerciement' AND statut = 'success'\nGROUP BY emailDestinataire, sequenceId\nHAVING COUNT(*) > 1\nORDER BY nb_envois DESC, dernier_envoi DESC;", - "rows": [ - { - "emailDestinataire": "opareige@gmail.com", - "sequenceId": "330001", - "nb_envois": "11", - "premier_envoi": "2025-12-15 20:24:49", - "dernier_envoi": "2025-12-15 21:16:32" - }, - { - "emailDestinataire": "o.pareige@itinova.org", - "sequenceId": "330001", - "nb_envois": "10", - "premier_envoi": "2025-12-15 20:24:58", - "dernier_envoi": "2025-12-15 21:16:59" - } - ], - "messages": [], - "stdout": "emailDestinataire\tsequenceId\tnb_envois\tpremier_envoi\tdernier_envoi\nopareige@gmail.com\t330001\t11\t2025-12-15 20:24:49\t2025-12-15 21:16:32\no.pareige@itinova.org\t330001\t10\t2025-12-15 20:24:58\t2025-12-15 21:16:59\n", - "stderr": "", - "execution_time_ms": 61 -} \ No newline at end of file diff --git a/.manus/db/db-query-1767770875139.json b/.manus/db/db-query-1767770875139.json deleted file mode 100644 index 3b7622f..0000000 --- a/.manus/db/db-query-1767770875139.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "query": "CREATE TABLE IF NOT EXISTS `attestations` (\n `id` int AUTO_INCREMENT NOT NULL,\n `inscriptionId` int NOT NULL,\n `apprenantId` int NOT NULL,\n `sequenceId` int NOT NULL,\n `s3Key` varchar(500) NOT NULL,\n `pdfUrl` varchar(1000) NOT NULL,\n `dateGeneration` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `emailEnvoye` boolean NOT NULL DEFAULT false,\n `dateEnvoiEmail` timestamp NULL,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n CONSTRAINT `attestations_id` PRIMARY KEY(`id`)\n);\n\nCREATE TABLE IF NOT EXISTS `configAttestation` (\n `id` int AUTO_INCREMENT NOT NULL,\n `logoS3Key` varchar(500),\n `logoUrl` varchar(1000),\n `signatureS3Key` varchar(500),\n `signatureUrl` varchar(1000),\n `nomSignataire` varchar(255),\n `fonctionSignataire` varchar(255),\n `texteAttestation` text,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n CONSTRAINT `configAttestation_id` PRIMARY KEY(`id`)\n);", - "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 CREATE TABLE IF NOT EXISTS `attestations` (\n `id` int AUTO_INCREMENT NOT NULL,\n `inscriptionId` int NOT NULL,\n `apprenantId` int NOT NULL,\n `sequenceId` int NOT NULL,\n `s3Key` varchar(500) NOT NULL,\n `pdfUrl` varchar(1000) NOT NULL,\n `dateGeneration` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `emailEnvoye` boolean NOT NULL DEFAULT false,\n `dateEnvoiEmail` timestamp NULL,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n CONSTRAINT `attestations_id` PRIMARY KEY(`id`)\n);\n\nCREATE TABLE IF NOT EXISTS `configAttestation` (\n `id` int AUTO_INCREMENT NOT NULL,\n `logoS3Key` varchar(500),\n `logoUrl` varchar(1000),\n `signatureS3Key` varchar(500),\n `signatureUrl` varchar(1000),\n `nomSignataire` varchar(255),\n `fonctionSignataire` varchar(255),\n `texteAttestation` text,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n CONSTRAINT `configAttestation_id` PRIMARY KEY(`id`)\n);", - "rows": [], - "messages": [], - "stdout": "", - "stderr": "", - "execution_time_ms": 665 -} \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 944af09..561de3f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -29,7 +29,6 @@ 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"; @@ -54,7 +53,6 @@ function Router() { - diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index bee1721..b9026e7 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -57,7 +57,6 @@ 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 deleted file mode 100644 index 8bdc118..0000000 --- a/client/src/pages/admin/AdminAttestations.tsx +++ /dev/null @@ -1,218 +0,0 @@ -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 { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { toast } from "sonner"; -import { Upload, 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: any) { - toast.error("Erreur lors de l'enregistrement : " + error.message); - } - }; - - if (isLoading) { - return ( - - Chargement... - - ); - } - - return ( - - - - Configuration des Attestations - - Personnalisez le template des attestations de formation - - - - - {/* Logo et signature */} - - - - - - Logo - - - Logo affiché en haut de l'attestation - - - - {config?.logoUrl ? ( - - - - Changer le logo - - - ) : ( - - - - Aucun logo configuré - - - Uploader un logo - - - )} - - - - - - - - Signature - - - Signature affichée en bas de l'attestation - - - - {config?.signatureUrl ? ( - - - - Changer la signature - - - ) : ( - - - - Aucune signature configurée - - - Uploader une signature - - - )} - - - - - {/* Informations du signataire */} - - - Informations du signataire - - Nom et fonction affichés en bas de l'attestation - - - - - - Nom du signataire - - setFormData({ ...formData, nomSignataire: e.target.value }) - } - placeholder="Ex: Jean Dupont" - /> - - - Fonction du signataire - - setFormData({ ...formData, fonctionSignataire: e.target.value }) - } - placeholder="Ex: Directeur de la formation" - /> - - - - - - {/* Texte de l'attestation */} - - - Texte de l'attestation - - Personnalisez le texte principal de l'attestation. Variables disponibles : - - {"{"}{"{"} nomComplet {"}"}{"}"} - - - {"{"}{"{"} nomFormation {"}"}{"}"} - - - {"{"}{"{"} dateDebut {"}"}{"}"} - - - {"{"}{"{"} dateFin {"}"}{"}"} - - - - - - setFormData({ ...formData, texteAttestation: e.target.value }) - } - placeholder="Nous attestons que {{nomComplet}} a suivi avec assiduité la formation "{{nomFormation}}" organisée du {{dateDebut}} au {{dateFin}}." - rows={6} - /> - - - - {/* Bouton de sauvegarde */} - - - - {updateConfig.isPending ? "Enregistrement..." : "Enregistrer la configuration"} - - - - - - ); -} diff --git a/drizzle/meta/0024_snapshot.json b/drizzle/meta/0024_snapshot.json deleted file mode 100644 index f936ae8..0000000 --- a/drizzle/meta/0024_snapshot.json +++ /dev/null @@ -1,2028 +0,0 @@ -{ - "version": "5", - "dialect": "mysql", - "id": "ba0ad665-1f5c-44ab-ab8c-b98211f33717", - "prevId": "e0c120cb-533e-435d-a5d7-a6a6c58698cb", - "tables": { - "alertes": { - "name": "alertes", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "nom": { - "name": "nom", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "enum('taux_remplissage','seuil_inscriptions')", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "condition": { - "name": "condition", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "valeurSeuil": { - "name": "valeurSeuil", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "codeEtablissement": { - "name": "codeEtablissement", - "type": "varchar(50)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "emailsDestinataires": { - "name": "emailsDestinataires", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actif": { - "name": "actif", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "derniereVerification": { - "name": "derniereVerification", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "dernierEnvoi": { - "name": "dernierEnvoi", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "alertes_id": { - "name": "alertes_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "apprenants": { - "name": "apprenants", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "nom": { - "name": "nom", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prenom": { - "name": "prenom", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "varchar(320)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "codeEtablissement": { - "name": "codeEtablissement", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fonction": { - "name": "fonction", - "type": "enum('directeur','chef_service','autre')", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "apprenants_id": { - "name": "apprenants_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": { - "apprenants_email_unique": { - "name": "apprenants_email_unique", - "columns": [ - "email" - ] - } - }, - "checkConstraint": {} - }, - "attestations": { - "name": "attestations", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "inscriptionId": { - "name": "inscriptionId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "apprenantId": { - "name": "apprenantId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sequenceId": { - "name": "sequenceId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "s3Key": { - "name": "s3Key", - "type": "varchar(500)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "pdfUrl": { - "name": "pdfUrl", - "type": "varchar(1000)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "dateGeneration": { - "name": "dateGeneration", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "emailEnvoye": { - "name": "emailEnvoye", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "dateEnvoiEmail": { - "name": "dateEnvoiEmail", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "attestations_id": { - "name": "attestations_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "configAttestation": { - "name": "configAttestation", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "logoS3Key": { - "name": "logoS3Key", - "type": "varchar(500)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "logoUrl": { - "name": "logoUrl", - "type": "varchar(1000)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "signatureS3Key": { - "name": "signatureS3Key", - "type": "varchar(500)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "signatureUrl": { - "name": "signatureUrl", - "type": "varchar(1000)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "nomSignataire": { - "name": "nomSignataire", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "fonctionSignataire": { - "name": "fonctionSignataire", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "texteAttestation": { - "name": "texteAttestation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "configAttestation_id": { - "name": "configAttestation_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "datesFormation": { - "name": "datesFormation", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "sequenceId": { - "name": "sequenceId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "dateDebut": { - "name": "dateDebut", - "type": "datetime", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "dateFin": { - "name": "dateFin", - "type": "datetime", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "ordre": { - "name": "ordre", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "datesFormation_id": { - "name": "datesFormation_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "emailConfig": { - "name": "emailConfig", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "provider": { - "name": "provider", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'resend'" - }, - "apiKey": { - "name": "apiKey", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "fromEmail": { - "name": "fromEmail", - "type": "varchar(320)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fromName": { - "name": "fromName", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'Formation Manager Itinova'" - }, - "mode": { - "name": "mode", - "type": "enum('simulation','production')", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'simulation'" - }, - "domainVerified": { - "name": "domainVerified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "active": { - "name": "active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "smtpHost": { - "name": "smtpHost", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "smtpPort": { - "name": "smtpPort", - "type": "int", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "smtpSecure": { - "name": "smtpSecure", - "type": "enum('none','tls','ssl')", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'tls'" - }, - "smtpUser": { - "name": "smtpUser", - "type": "varchar(320)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "smtpPassword": { - "name": "smtpPassword", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "emailConfig_id": { - "name": "emailConfig_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "emailTemplates": { - "name": "emailTemplates", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "type": { - "name": "type", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "logoUrl": { - "name": "logoUrl", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "primaryColor": { - "name": "primaryColor", - "type": "varchar(7)", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'#2563eb'" - }, - "headerBgColor": { - "name": "headerBgColor", - "type": "varchar(7)", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'#2563eb'" - }, - "headerTextColor": { - "name": "headerTextColor", - "type": "varchar(7)", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'#ffffff'" - }, - "headerTitle": { - "name": "headerTitle", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'Formation Manager Itinova'" - }, - "footerText": { - "name": "footerText", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "bodyContent": { - "name": "bodyContent", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "active": { - "name": "active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "emailTemplates_id": { - "name": "emailTemplates_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": { - "emailTemplates_type_unique": { - "name": "emailTemplates_type_unique", - "columns": [ - "type" - ] - } - }, - "checkConstraint": {} - }, - "envoisQuestionnaires": { - "name": "envoisQuestionnaires", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "questionnaireId": { - "name": "questionnaireId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "apprenantId": { - "name": "apprenantId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sequenceId": { - "name": "sequenceId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "dateEnvoi": { - "name": "dateEnvoi", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "token": { - "name": "token", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "dateReponse": { - "name": "dateReponse", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "envoisQuestionnaires_id": { - "name": "envoisQuestionnaires_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": { - "envoisQuestionnaires_token_unique": { - "name": "envoisQuestionnaires_token_unique", - "columns": [ - "token" - ] - } - }, - "checkConstraint": {} - }, - "formateurs": { - "name": "formateurs", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "nom": { - "name": "nom", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "varchar(320)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "formateurs_id": { - "name": "formateurs_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "formations": { - "name": "formations", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "nom": { - "name": "nom", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "lienUnique": { - "name": "lienUnique", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actif": { - "name": "actif", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "formations_id": { - "name": "formations_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": { - "formations_lienUnique_unique": { - "name": "formations_lienUnique_unique", - "columns": [ - "lienUnique" - ] - } - }, - "checkConstraint": {} - }, - "inscriptions": { - "name": "inscriptions", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "apprenantId": { - "name": "apprenantId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sequenceId": { - "name": "sequenceId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "statut": { - "name": "statut", - "type": "enum('confirmee','liste_attente','annulee')", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "dateInscription": { - "name": "dateInscription", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "inscriptions_id": { - "name": "inscriptions_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "logsNotifications": { - "name": "logsNotifications", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "type": { - "name": "type", - "type": "enum('remerciement','notification_formateur_inscription','notification_formateur_annulation','alerte_capacite','notification_liste_attente')", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sequenceId": { - "name": "sequenceId", - "type": "int", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "apprenantId": { - "name": "apprenantId", - "type": "int", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "formateurId": { - "name": "formateurId", - "type": "int", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "emailDestinataire": { - "name": "emailDestinataire", - "type": "varchar(320)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sujet": { - "name": "sujet", - "type": "varchar(500)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "dateEnvoi": { - "name": "dateEnvoi", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "statut": { - "name": "statut", - "type": "enum('success','failed')", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "messageErreur": { - "name": "messageErreur", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "logsNotifications_id": { - "name": "logsNotifications_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "logsRappels": { - "name": "logsRappels", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "rappelId": { - "name": "rappelId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sequenceId": { - "name": "sequenceId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "apprenantId": { - "name": "apprenantId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "emailDestinataire": { - "name": "emailDestinataire", - "type": "varchar(320)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "typeRappel": { - "name": "typeRappel", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "dateEnvoi": { - "name": "dateEnvoi", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "statut": { - "name": "statut", - "type": "enum('success','failed')", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "messageErreur": { - "name": "messageErreur", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "dateSequence": { - "name": "dateSequence", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "nbTentatives": { - "name": "nbTentatives", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "prochainEssai": { - "name": "prochainEssai", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "logsRappels_id": { - "name": "logsRappels_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "passwordResetTokens": { - "name": "passwordResetTokens", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "userId": { - "name": "userId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "token": { - "name": "token", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "used": { - "name": "used", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "passwordResetTokens_id": { - "name": "passwordResetTokens_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": { - "passwordResetTokens_token_unique": { - "name": "passwordResetTokens_token_unique", - "columns": [ - "token" - ] - } - }, - "checkConstraint": {} - }, - "questionnaires": { - "name": "questionnaires", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "nom": { - "name": "nom", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "enum('satisfaction','evaluation_pre','evaluation_post')", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "formationId": { - "name": "formationId", - "type": "int", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "actif": { - "name": "actif", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "envoiAutomatique": { - "name": "envoiAutomatique", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "delaiEnvoiJours": { - "name": "delaiEnvoiJours", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "questionnaires_id": { - "name": "questionnaires_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "questions": { - "name": "questions", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "questionnaireId": { - "name": "questionnaireId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "ordre": { - "name": "ordre", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "texte": { - "name": "texte", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "typeQuestion": { - "name": "typeQuestion", - "type": "enum('choix_multiple','echelle','texte_libre','oui_non')", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "options": { - "name": "options", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "echelleMin": { - "name": "echelleMin", - "type": "int", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "echelleMax": { - "name": "echelleMax", - "type": "int", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "echelleLabelMin": { - "name": "echelleLabelMin", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "echelleLabelMax": { - "name": "echelleLabelMax", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "obligatoire": { - "name": "obligatoire", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "questions_id": { - "name": "questions_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "rappels": { - "name": "rappels", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "nom": { - "name": "nom", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "templateType": { - "name": "templateType", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "joursAvant": { - "name": "joursAvant", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "heureEnvoi": { - "name": "heureEnvoi", - "type": "varchar(5)", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'09:00'" - }, - "actif": { - "name": "actif", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "derniereExecution": { - "name": "derniereExecution", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "rappels_id": { - "name": "rappels_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "reponsesQuestionnaires": { - "name": "reponsesQuestionnaires", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "questionnaireId": { - "name": "questionnaireId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "apprenantId": { - "name": "apprenantId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sequenceId": { - "name": "sequenceId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "dateReponse": { - "name": "dateReponse", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "complete": { - "name": "complete", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "reponsesQuestionnaires_id": { - "name": "reponsesQuestionnaires_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "reponsesQuestions": { - "name": "reponsesQuestions", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "reponseQuestionnaireId": { - "name": "reponseQuestionnaireId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "questionId": { - "name": "questionId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "reponseNumerique": { - "name": "reponseNumerique", - "type": "int", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "reponseTexte": { - "name": "reponseTexte", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "reponsesQuestions_id": { - "name": "reponsesQuestions_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "sequences": { - "name": "sequences", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "formationId": { - "name": "formationId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "nom": { - "name": "nom", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "lieu": { - "name": "lieu", - "type": "varchar(500)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "publicCible": { - "name": "publicCible", - "type": "enum('directeur','chef_service','tous','autre')", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "formateurId": { - "name": "formateurId", - "type": "int", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "capaciteMax": { - "name": "capaciteMax", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 12 - }, - "dateBlocage": { - "name": "dateBlocage", - "type": "datetime", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "statut": { - "name": "statut", - "type": "enum('ouverte','bloquee','terminee')", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'ouverte'" - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "sequences_id": { - "name": "sequences_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "supportsFormation": { - "name": "supportsFormation", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "sequenceId": { - "name": "sequenceId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "formateurId": { - "name": "formateurId", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "nomFichier": { - "name": "nomFichier", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "typeFichier": { - "name": "typeFichier", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "tailleFichier": { - "name": "tailleFichier", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "urlFichier": { - "name": "urlFichier", - "type": "varchar(500)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "s3Key": { - "name": "s3Key", - "type": "varchar(500)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "supportsFormation_id": { - "name": "supportsFormation_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "users": { - "name": "users", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "openId": { - "name": "openId", - "type": "varchar(64)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "varchar(320)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "username": { - "name": "username", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "loginMethod": { - "name": "loginMethod", - "type": "varchar(64)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "role": { - "name": "role", - "type": "enum('user','admin')", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'user'" - }, - "isActive": { - "name": "isActive", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "onUpdate": true, - "default": "(now())" - }, - "lastSignedIn": { - "name": "lastSignedIn", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(now())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "users_id": { - "name": "users_id", - "columns": [ - "id" - ] - } - }, - "uniqueConstraints": { - "users_openId_unique": { - "name": "users_openId_unique", - "columns": [ - "openId" - ] - }, - "users_username_unique": { - "name": "users_username_unique", - "columns": [ - "username" - ] - } - }, - "checkConstraint": {} - } - }, - "views": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "tables": {}, - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c0601e3..15855a7 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -169,13 +169,6 @@ "when": 1765830109012, "tag": "0023_jittery_gorgon", "breakpoints": true - }, - { - "idx": 24, - "version": "5", - "when": 1767770858914, - "tag": "0024_panoramic_goliath", - "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index b8f3083..844c290 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -520,58 +520,3 @@ export const logsNotifications = mysqlTable("logsNotifications", { export type LogNotification = typeof logsNotifications.$inferSelect; export type InsertLogNotification = typeof logsNotifications.$inferInsert; - -/** - * Table des attestations de formation - * Stocke les attestations générées pour chaque apprenant ayant terminé une formation - */ -export const attestations = mysqlTable("attestations", { - id: int("id").autoincrement().primaryKey(), - /** ID de l'inscription concernée */ - inscriptionId: int("inscriptionId").notNull(), - /** ID de l'apprenant */ - apprenantId: int("apprenantId").notNull(), - /** ID de la séquence */ - sequenceId: int("sequenceId").notNull(), - /** Clé S3 du PDF généré */ - s3Key: varchar("s3Key", { length: 500 }).notNull(), - /** URL publique du PDF */ - pdfUrl: varchar("pdfUrl", { length: 1000 }).notNull(), - /** Date de génération de l'attestation */ - dateGeneration: timestamp("dateGeneration").defaultNow().notNull(), - /** Statut de l'envoi par email */ - emailEnvoye: boolean("emailEnvoye").default(false).notNull(), - /** Date d'envoi de l'email */ - dateEnvoiEmail: timestamp("dateEnvoiEmail"), - createdAt: timestamp("createdAt").defaultNow().notNull(), -}); - -export type Attestation = typeof attestations.$inferSelect; -export type InsertAttestation = typeof attestations.$inferInsert; - -/** - * Table de configuration des attestations - * Stocke le logo, la signature et le template personnalisable - */ -export const configAttestation = mysqlTable("configAttestation", { - id: int("id").autoincrement().primaryKey(), - /** Clé S3 du logo */ - logoS3Key: varchar("logoS3Key", { length: 500 }), - /** URL du logo */ - logoUrl: varchar("logoUrl", { length: 1000 }), - /** Clé S3 de la signature */ - signatureS3Key: varchar("signatureS3Key", { length: 500 }), - /** URL de la signature */ - signatureUrl: varchar("signatureUrl", { length: 1000 }), - /** Nom du signataire */ - nomSignataire: varchar("nomSignataire", { length: 255 }), - /** Fonction du signataire */ - fonctionSignataire: varchar("fonctionSignataire", { length: 255 }), - /** Texte personnalisable de l'attestation */ - texteAttestation: text("texteAttestation"), - createdAt: timestamp("createdAt").defaultNow().notNull(), - updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), -}); - -export type ConfigAttestation = typeof configAttestation.$inferSelect; -export type InsertConfigAttestation = typeof configAttestation.$inferInsert; diff --git a/package.json b/package.json index f053d3a..f29cb81 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "@trpc/client": "^11.6.0", "@trpc/react-query": "^11.6.0", "@trpc/server": "^11.6.0", - "@types/pdfkit": "^0.17.4", "axios": "^1.12.0", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", @@ -75,7 +74,6 @@ "next-themes": "^0.4.6", "nodemailer": "^7.0.11", "openai": "^4.67.0", - "pdfkit": "^0.17.2", "react": "^19.1.1", "react-day-picker": "^9.11.1", "react-dom": "^19.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0018611..138e36a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,9 +127,6 @@ importers: '@trpc/server': specifier: ^11.6.0 version: 11.6.0(typescript@5.9.3) - '@types/pdfkit': - specifier: ^0.17.4 - version: 0.17.4 axios: specifier: ^1.12.0 version: 1.12.2 @@ -202,9 +199,6 @@ importers: openai: specifier: ^4.67.0 version: 4.104.0(zod@4.1.12) - pdfkit: - specifier: ^0.17.2 - version: 0.17.2 react: specifier: ^19.1.1 version: 19.2.0 @@ -2300,9 +2294,6 @@ packages: resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} engines: {node: '>=18.0.0'} - '@swc/helpers@0.5.18': - resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} - '@tailwindcss/node@4.1.14': resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==} @@ -2770,9 +2761,6 @@ packages: '@types/pako@2.0.4': resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} - '@types/pdfkit@0.17.4': - resolution: {integrity: sha512-odAmVuuguRxKh1X4pbMrJMp8ecwNqHRw6lweupvzK+wuyNmi6wzlUlGVZ9EqMvp3Bs2+L9Ty0sRlrvKL+gsQZg==} - '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -2929,10 +2917,6 @@ packages: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} engines: {node: '>= 0.6.0'} - base64-js@0.0.8: - resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} - engines: {node: '>= 0.4'} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2970,9 +2954,6 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brotli@1.3.3: - resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} - browserslist@4.26.3: resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -3066,10 +3047,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - clone@2.1.2: - resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} - engines: {node: '>=0.8'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -3162,9 +3139,6 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - css-line-break@2.1.0: resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} @@ -3401,9 +3375,6 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - dfa@1.2.0: - resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} - dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -3697,9 +3668,6 @@ packages: debug: optional: true - fontkit@2.0.4: - resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} - form-data-encoder@1.7.2: resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} @@ -3948,9 +3916,6 @@ packages: jose@6.1.0: resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} - jpeg-exif@1.1.4: - resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4076,9 +4041,6 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} - linebreak@1.1.0: - resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} - linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} @@ -4510,9 +4472,6 @@ packages: package-manager-detector@1.5.0: resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} - pako@0.2.9: - resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} - pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -4549,9 +4508,6 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} - pdfkit@0.17.2: - resolution: {integrity: sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==} - performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -4568,9 +4524,6 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - png-js@1.0.0: - resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} - pnpm@10.18.0: resolution: {integrity: sha512-6AT4ifHOzEDVctsITuw+SIFzn43sacD/ENLRvv+aTjCTg7ontbdQBZ1/TBSVNbbNDSyx7Trrc5I5pChKaPQM+g==} engines: {node: '>=18.12'} @@ -4845,9 +4798,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - restructure@3.0.2: - resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} - rgbcolor@1.0.1: resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} engines: {node: '>= 0.8.15'} @@ -5040,9 +4990,6 @@ packages: text-segmentation@1.0.3: resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} - tiny-inflate@1.0.3: - resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -5127,12 +5074,6 @@ packages: undici-types@7.14.0: resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} - unicode-properties@1.4.1: - resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} - - unicode-trie@2.0.0: - resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -8040,10 +7981,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/helpers@0.5.18': - dependencies: - tslib: 2.8.1 - '@tailwindcss/node@4.1.14': dependencies: '@jridgewell/remapping': 2.3.5 @@ -8563,10 +8500,6 @@ snapshots: '@types/pako@2.0.4': {} - '@types/pdfkit@0.17.4': - dependencies: - '@types/node': 24.7.0 - '@types/qs@6.14.0': {} '@types/raf@3.4.3': @@ -8756,8 +8689,6 @@ snapshots: base64-arraybuffer@1.0.2: optional: true - base64-js@0.0.8: {} - base64-js@1.5.1: {} baseline-browser-mapping@2.8.12: {} @@ -8807,10 +8738,6 @@ snapshots: dependencies: balanced-match: 1.0.2 - brotli@1.3.3: - dependencies: - base64-js: 1.5.1 - browserslist@4.26.3: dependencies: baseline-browser-mapping: 2.8.12 @@ -8911,8 +8838,6 @@ snapshots: dependencies: clsx: 2.1.1 - clone@2.1.2: {} - clsx@2.1.1: {} cmdk@1.1.1(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): @@ -8992,8 +8917,6 @@ snapshots: crelt@1.0.6: {} - crypto-js@4.2.0: {} - css-line-break@2.1.0: dependencies: utrie: 1.0.2 @@ -9231,8 +9154,6 @@ snapshots: dependencies: dequal: 2.0.3 - dfa@1.2.0: {} - dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.4 @@ -9523,18 +9444,6 @@ snapshots: follow-redirects@1.15.11: {} - fontkit@2.0.4: - dependencies: - '@swc/helpers': 0.5.18 - brotli: 1.3.3 - clone: 2.1.2 - dfa: 1.2.0 - fast-deep-equal: 3.1.3 - restructure: 3.0.2 - tiny-inflate: 1.0.3 - unicode-properties: 1.4.1 - unicode-trie: 2.0.0 - form-data-encoder@1.7.2: {} form-data@4.0.4: @@ -9843,8 +9752,6 @@ snapshots: jose@6.1.0: {} - jpeg-exif@1.1.4: {} - js-tokens@4.0.0: {} jsesc@3.1.0: {} @@ -9970,11 +9877,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 - linebreak@1.1.0: - dependencies: - base64-js: 0.0.8 - unicode-trie: 2.0.0 - linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 @@ -10601,8 +10503,6 @@ snapshots: package-manager-detector@1.5.0: {} - pako@0.2.9: {} - pako@1.0.11: {} pako@2.1.0: {} @@ -10635,14 +10535,6 @@ snapshots: pathval@2.0.1: {} - pdfkit@0.17.2: - dependencies: - crypto-js: 4.2.0 - fontkit: 2.0.4 - jpeg-exif: 1.1.4 - linebreak: 1.1.0 - png-js: 1.0.0 - performance-now@2.1.0: optional: true @@ -10662,8 +10554,6 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 - png-js@1.0.0: {} - pnpm@10.18.0: {} points-on-curve@0.2.0: {} @@ -11038,8 +10928,6 @@ snapshots: resolve-pkg-maps@1.0.0: {} - restructure@3.0.2: {} - rgbcolor@1.0.1: optional: true @@ -11292,8 +11180,6 @@ snapshots: utrie: 1.0.2 optional: true - tiny-inflate@1.0.3: {} - tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -11353,16 +11239,6 @@ snapshots: undici-types@7.14.0: {} - unicode-properties@1.4.1: - dependencies: - base64-js: 1.5.1 - unicode-trie: 2.0.0 - - unicode-trie@2.0.0: - dependencies: - pako: 0.2.9 - tiny-inflate: 1.0.3 - unified@11.0.5: dependencies: '@types/unist': 3.0.3 diff --git a/server/attestationService.ts b/server/attestationService.ts deleted file mode 100644 index 45f2e76..0000000 --- a/server/attestationService.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { getDb } from "./db"; -import { attestations, configAttestation, inscriptions, apprenants, sequences, formations, datesFormation } from "../drizzle/schema"; -import { eq, and } from "drizzle-orm"; -import { storagePut } from "./storage"; -import PDFDocument from "pdfkit"; -import { format } from "date-fns"; -import { fr } from "date-fns/locale"; -import crypto from "crypto"; - -/** - * Génère une attestation de formation en PDF - */ -export async function genererAttestationPDF(inscriptionId: number): Promise<{ s3Key: string; pdfUrl: string }> { - const db = await getDb(); - if (!db) { - throw new Error("Base de données non disponible"); - } - - // Récupérer les données de l'inscription - const [inscriptionData] = await db - .select({ - inscription: inscriptions, - apprenant: apprenants, - sequence: sequences, - formation: formations, - }) - .from(inscriptions) - .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) - .innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id)) - .innerJoin(formations, eq(sequences.formationId, formations.id)) - .where(eq(inscriptions.id, inscriptionId)) - .limit(1); - - if (!inscriptionData) { - throw new Error("Inscription introuvable"); - } - - const { inscription, apprenant, sequence, formation } = inscriptionData; - - // Récupérer les dates de la séquence - const dates = await db - .select() - .from(datesFormation) - .where(eq(datesFormation.sequenceId, sequence.id)) - .orderBy(datesFormation.dateDebut); - - // Récupérer la configuration de l'attestation - const [config] = await db - .select() - .from(configAttestation) - .limit(1); - - // Créer le PDF - const doc = new PDFDocument({ - size: "A4", - margins: { top: 50, bottom: 50, left: 50, right: 50 }, - }); - - const chunks: Buffer[] = []; - doc.on("data", (chunk: Buffer) => chunks.push(chunk)); - - return new Promise((resolve, reject) => { - doc.on("end", async () => { - try { - const pdfBuffer = Buffer.concat(chunks); - - // Générer une clé S3 unique - const randomSuffix = crypto.randomBytes(8).toString("hex"); - const s3Key = `attestations/${apprenant.id}-${sequence.id}-${randomSuffix}.pdf`; - - // Upload vers S3 - const { url } = await storagePut(s3Key, pdfBuffer, "application/pdf"); - - resolve({ s3Key, pdfUrl: url }); - } catch (error) { - reject(error); - } - }); - - doc.on("error", reject); - - // Construction du PDF - try { - // Logo (si configuré) - if (config?.logoUrl) { - // TODO: télécharger et insérer le logo - // doc.image(logoPath, 50, 45, { width: 100 }); - } - - // Titre - doc.fontSize(24).font("Helvetica-Bold").text("ATTESTATION DE FORMATION", { align: "center" }); - doc.moveDown(2); - - // Texte personnalisable ou texte par défaut - const texteAttestation = config?.texteAttestation || - "Nous attestons que {{nomComplet}} a suivi avec assiduité la formation \"{{nomFormation}}\" organisée du {{dateDebut}} au {{dateFin}}."; - - // Remplacer les variables - const nomComplet = `${apprenant.prenom} ${apprenant.nom}`; - const dateDebut = dates.length > 0 ? format(new Date(dates[0].dateDebut), "dd MMMM yyyy", { locale: fr }) : ""; - const dateFin = dates.length > 0 ? format(new Date(dates[dates.length - 1].dateFin), "dd MMMM yyyy", { locale: fr }) : ""; - - const texteRempli = texteAttestation - .replace(/\{\{nomComplet\}\}/g, nomComplet) - .replace(/\{\{nomFormation\}\}/g, formation.nom) - .replace(/\{\{dateDebut\}\}/g, dateDebut) - .replace(/\{\{dateFin\}\}/g, dateFin); - - doc.fontSize(12).font("Helvetica").text(texteRempli, { align: "justify" }); - doc.moveDown(2); - - // Détails de la formation - doc.fontSize(10).font("Helvetica-Bold").text("Détails de la formation :", { underline: true }); - doc.moveDown(0.5); - doc.font("Helvetica"); - doc.text(`Formation : ${formation.nom}`); - doc.text(`Séquence : ${sequence.nom}`); - doc.text(`Lieu : ${sequence.lieu}`); - doc.moveDown(0.5); - doc.text("Dates :"); - dates.forEach((date) => { - const dateStr = format(new Date(date.dateDebut), "dd/MM/yyyy", { locale: fr }); - const heureDebut = format(new Date(date.dateDebut), "HH:mm"); - const heureFin = format(new Date(date.dateFin), "HH:mm"); - doc.text(` • ${dateStr} de ${heureDebut} à ${heureFin}`); - }); - - doc.moveDown(3); - - // Signature - doc.fontSize(10); - doc.text(`Fait le ${format(new Date(), "dd MMMM yyyy", { locale: fr })}`, { align: "right" }); - doc.moveDown(1); - - if (config?.signatureUrl) { - // TODO: télécharger et insérer la signature - // doc.image(signaturePath, 400, doc.y, { width: 100 }); - doc.moveDown(3); - } - - if (config?.nomSignataire) { - doc.text(config.nomSignataire, { align: "right" }); - } - if (config?.fonctionSignataire) { - doc.text(config.fonctionSignataire, { align: "right" }); - } - - doc.end(); - } catch (error) { - doc.end(); - reject(error); - } - }); -} - -/** - * Enregistre une attestation dans la base de données - */ -export async function enregistrerAttestation( - inscriptionId: number, - apprenantId: number, - sequenceId: number, - s3Key: string, - pdfUrl: string -): Promise { - const db = await getDb(); - if (!db) { - throw new Error("Base de données non disponible"); - } - - const [result] = await db.insert(attestations).values({ - inscriptionId, - apprenantId, - sequenceId, - s3Key, - pdfUrl, - emailEnvoye: false, - }); - - return result.insertId; -} - -/** - * Récupère l'attestation d'un apprenant pour une séquence - */ -export async function getAttestation(apprenantId: number, sequenceId: number) { - const db = await getDb(); - if (!db) return null; - - const [attestation] = await db - .select() - .from(attestations) - .where( - and( - eq(attestations.apprenantId, apprenantId), - eq(attestations.sequenceId, sequenceId) - ) - ) - .limit(1); - - return attestation || null; -} - -/** - * Récupère toutes les attestations d'un apprenant - */ -export async function getAttestationsApprenant(apprenantId: number) { - const db = await getDb(); - if (!db) return []; - - return db - .select({ - attestation: attestations, - sequence: sequences, - formation: formations, - }) - .from(attestations) - .innerJoin(sequences, eq(attestations.sequenceId, sequences.id)) - .innerJoin(formations, eq(sequences.formationId, formations.id)) - .where(eq(attestations.apprenantId, apprenantId)) - .orderBy(attestations.dateGeneration); -} - -/** - * Marque une attestation comme envoyée par email - */ -export async function marquerAttestationEnvoyee(attestationId: number) { - const db = await getDb(); - if (!db) return; - - await db - .update(attestations) - .set({ - emailEnvoye: true, - dateEnvoiEmail: new Date(), - }) - .where(eq(attestations.id, attestationId)); -} - -/** - * Récupère ou crée la configuration des attestations - */ -export async function getOrCreateConfigAttestation() { - const db = await getDb(); - if (!db) return null; - - let [config] = await db.select().from(configAttestation).limit(1); - - if (!config) { - // Créer une configuration par défaut - await db.insert(configAttestation).values({ - texteAttestation: "Nous attestons que {{nomComplet}} a suivi avec assiduité la formation \"{{nomFormation}}\" organisée du {{dateDebut}} au {{dateFin}}.", - }); - - [config] = await db.select().from(configAttestation).limit(1); - } - - return config; -} - -/** - * Met à jour la configuration des attestations - */ -export async function updateConfigAttestation(data: { - logoS3Key?: string; - logoUrl?: string; - signatureS3Key?: string; - signatureUrl?: string; - nomSignataire?: string; - fonctionSignataire?: string; - texteAttestation?: string; -}) { - const db = await getDb(); - if (!db) return; - - const [existing] = await db.select().from(configAttestation).limit(1); - - if (existing) { - await db.update(configAttestation).set(data).where(eq(configAttestation.id, existing.id)); - } else { - await db.insert(configAttestation).values(data); - } -} diff --git a/server/routers.ts b/server/routers.ts index c8b4d40..d6dffb0 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -1736,105 +1736,6 @@ export const appRouter = router({ }); }), }), - - // ===== ATTESTATIONS ===== - attestations: router({ - // Générer une attestation pour une inscription - generer: adminProcedure - .input(z.object({ inscriptionId: z.number() })) - .mutation(async ({ input }) => { - const attestationService = await import("./attestationService"); - const { getDb } = await import("./db"); - const { inscriptions, apprenants, sequences } = await import("../drizzle/schema"); - const { eq } = await import("drizzle-orm"); - - const db = await getDb(); - if (!db) throw new Error("Base de données non disponible"); - - // Récupérer les infos de l'inscription - const [inscriptionData] = await db - .select({ - inscription: inscriptions, - apprenant: apprenants, - sequence: sequences, - }) - .from(inscriptions) - .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) - .innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id)) - .where(eq(inscriptions.id, input.inscriptionId)) - .limit(1); - - if (!inscriptionData) { - throw new Error("Inscription introuvable"); - } - - // Vérifier si une attestation existe déjà - const existingAttestation = await attestationService.getAttestation( - inscriptionData.apprenant.id, - inscriptionData.sequence.id - ); - - if (existingAttestation) { - return existingAttestation; - } - - // Générer le PDF - const { s3Key, pdfUrl } = await attestationService.genererAttestationPDF(input.inscriptionId); - - // Enregistrer dans la base - await attestationService.enregistrerAttestation( - input.inscriptionId, - inscriptionData.apprenant.id, - inscriptionData.sequence.id, - s3Key, - pdfUrl - ); - - return { s3Key, pdfUrl }; - }), - - // Récupérer l'attestation d'un apprenant pour une séquence - get: publicProcedure - .input(z.object({ - apprenantId: z.number(), - sequenceId: z.number(), - })) - .query(async ({ input }) => { - const attestationService = await import("./attestationService"); - return attestationService.getAttestation(input.apprenantId, input.sequenceId); - }), - - // Récupérer toutes les attestations d'un apprenant - listByApprenant: publicProcedure - .input(z.object({ apprenantId: z.number() })) - .query(async ({ input }) => { - const attestationService = await import("./attestationService"); - return attestationService.getAttestationsApprenant(input.apprenantId); - }), - - // Configuration des attestations - getConfig: adminProcedure - .query(async () => { - const attestationService = await import("./attestationService"); - return attestationService.getOrCreateConfigAttestation(); - }), - - updateConfig: adminProcedure - .input(z.object({ - logoS3Key: z.string().optional(), - logoUrl: z.string().optional(), - signatureS3Key: z.string().optional(), - signatureUrl: z.string().optional(), - nomSignataire: z.string().optional(), - fonctionSignataire: z.string().optional(), - texteAttestation: z.string().optional(), - })) - .mutation(async ({ input }) => { - const attestationService = await import("./attestationService"); - await attestationService.updateConfigAttestation(input); - return { success: true }; - }), - }), }); export type AppRouter = typeof appRouter; diff --git a/todo.md b/todo.md index a43ebf0..ffbb0f5 100644 --- a/todo.md +++ b/todo.md @@ -442,23 +442,3 @@ - [x] Interface de gestion des emails formateurs dans Configuration - [x] Tableau de bord des notifications avec historique des emails envoyés - [x] Lien vers questionnaire de satisfaction dans l'email de remerciement - -## Attestations de formation et Espace apprenant (16/12/2025) -- [x] Système d'attestations de formation automatiques (Phase 1) - - [x] Table attestations en base de données - - [x] Génération PDF avec template personnalisable - - [x] Service backend de génération PDF (attestationService.ts) - - [x] Procédures tRPC pour les attestations - - [x] Page de configuration des attestations - - [x] Stockage S3 des PDF générés - - [ ] Upload logo et signature pour les attestations (Phase 2) - - [ ] Génération automatique après validation de présence (Phase 2) - - [ ] Envoi automatique par email (Phase 2) -- [ ] Espace apprenant personnalisé (Phase 2) - - [ ] Système d'authentification apprenant (email/mot de passe) - - [ ] Page de connexion apprenant - - [ ] Tableau de bord apprenant - - [ ] Historique des formations suivies - - [ ] Téléchargement des attestations - - [ ] Téléchargement des supports de formation - - [ ] Réinscription rapide aux nouvelles sessions
- Personnalisez le template des attestations de formation -
- Aucun logo configuré -
- Aucune signature configurée -
- {"{"}{"{"} nomComplet {"}"}{"}"} -
- {"{"}{"{"} nomFormation {"}"}{"}"} -
- {"{"}{"{"} dateDebut {"}"}{"}"} -
- {"{"}{"{"} dateFin {"}"}{"}"} -