Rollback to 29b35fce
This commit is contained in:
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,6 @@ import AdminRappelsHistorique from "./pages/admin/AdminRappelsHistorique";
|
|||||||
import AdminRappelsStats from "./pages/admin/AdminRappelsStats";
|
import AdminRappelsStats from "./pages/admin/AdminRappelsStats";
|
||||||
import AdminFormateurs from "./pages/admin/AdminFormateurs";
|
import AdminFormateurs from "./pages/admin/AdminFormateurs";
|
||||||
import AdminNotifications from "./pages/admin/AdminNotifications";
|
import AdminNotifications from "./pages/admin/AdminNotifications";
|
||||||
import AdminAttestations from "./pages/admin/AdminAttestations";
|
|
||||||
import QuestionnaireReponse from "./pages/QuestionnaireReponse";
|
import QuestionnaireReponse from "./pages/QuestionnaireReponse";
|
||||||
import Inscription from "./pages/Inscription";
|
import Inscription from "./pages/Inscription";
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login";
|
||||||
@@ -54,7 +53,6 @@ function Router() {
|
|||||||
<Route path={"/admin/users"} component={AdminUsers} />
|
<Route path={"/admin/users"} component={AdminUsers} />
|
||||||
<Route path={"/admin/formateurs"} component={AdminFormateurs} />
|
<Route path={"/admin/formateurs"} component={AdminFormateurs} />
|
||||||
<Route path={"/admin/notifications"} component={AdminNotifications} />
|
<Route path={"/admin/notifications"} component={AdminNotifications} />
|
||||||
<Route path={"/admin/attestations"} component={AdminAttestations} />
|
|
||||||
<Route path={"/admin/email-templates"} component={AdminEmailTemplates} />
|
<Route path={"/admin/email-templates"} component={AdminEmailTemplates} />
|
||||||
<Route path={"/admin/email-config"} component={AdminEmailConfig} />
|
<Route path={"/admin/email-config"} component={AdminEmailConfig} />
|
||||||
<Route path={"/admin/calendrier"} component={AdminCalendrier} />
|
<Route path={"/admin/calendrier"} component={AdminCalendrier} />
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ const menuSections = [
|
|||||||
{ icon: Mail, label: "Templates d'emails", path: "/admin/email-templates" },
|
{ icon: Mail, label: "Templates d'emails", path: "/admin/email-templates" },
|
||||||
{ icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" },
|
{ icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" },
|
||||||
{ icon: Mail, label: "Notifications", path: "/admin/notifications" },
|
{ icon: Mail, label: "Notifications", path: "/admin/notifications" },
|
||||||
{ icon: FileText, label: "Attestations", path: "/admin/attestations" },
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="text-center py-8">Chargement...</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold">Configuration des Attestations</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Personnalisez le template des attestations de formation
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
{/* Logo et signature */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Upload className="h-5 w-5" />
|
|
||||||
Logo
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Logo affiché en haut de l'attestation
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{config?.logoUrl ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<img
|
|
||||||
src={config.logoUrl}
|
|
||||||
alt="Logo"
|
|
||||||
className="max-w-full h-auto border rounded"
|
|
||||||
/>
|
|
||||||
<Button type="button" variant="outline" size="sm">
|
|
||||||
Changer le logo
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="border-2 border-dashed rounded-lg p-8 text-center">
|
|
||||||
<Upload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
|
||||||
Aucun logo configuré
|
|
||||||
</p>
|
|
||||||
<Button type="button" variant="outline" size="sm">
|
|
||||||
Uploader un logo
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<FileText className="h-5 w-5" />
|
|
||||||
Signature
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Signature affichée en bas de l'attestation
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{config?.signatureUrl ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<img
|
|
||||||
src={config.signatureUrl}
|
|
||||||
alt="Signature"
|
|
||||||
className="max-w-full h-auto border rounded"
|
|
||||||
/>
|
|
||||||
<Button type="button" variant="outline" size="sm">
|
|
||||||
Changer la signature
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="border-2 border-dashed rounded-lg p-8 text-center">
|
|
||||||
<FileText className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
|
||||||
Aucune signature configurée
|
|
||||||
</p>
|
|
||||||
<Button type="button" variant="outline" size="sm">
|
|
||||||
Uploader une signature
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Informations du signataire */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Informations du signataire</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Nom et fonction affichés en bas de l'attestation
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="nomSignataire">Nom du signataire</Label>
|
|
||||||
<Input
|
|
||||||
id="nomSignataire"
|
|
||||||
value={formData.nomSignataire}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, nomSignataire: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="Ex: Jean Dupont"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="fonctionSignataire">Fonction du signataire</Label>
|
|
||||||
<Input
|
|
||||||
id="fonctionSignataire"
|
|
||||||
value={formData.fonctionSignataire}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, fonctionSignataire: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="Ex: Directeur de la formation"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Texte de l'attestation */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Texte de l'attestation</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Personnalisez le texte principal de l'attestation. Variables disponibles :
|
|
||||||
<code className="ml-2 text-xs bg-muted px-2 py-1 rounded">
|
|
||||||
{"{"}{"{"} nomComplet {"}"}{"}"}
|
|
||||||
</code>
|
|
||||||
<code className="ml-2 text-xs bg-muted px-2 py-1 rounded">
|
|
||||||
{"{"}{"{"} nomFormation {"}"}{"}"}
|
|
||||||
</code>
|
|
||||||
<code className="ml-2 text-xs bg-muted px-2 py-1 rounded">
|
|
||||||
{"{"}{"{"} dateDebut {"}"}{"}"}
|
|
||||||
</code>
|
|
||||||
<code className="ml-2 text-xs bg-muted px-2 py-1 rounded">
|
|
||||||
{"{"}{"{"} dateFin {"}"}{"}"}
|
|
||||||
</code>
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Textarea
|
|
||||||
value={formData.texteAttestation}
|
|
||||||
onChange={(e) =>
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Bouton de sauvegarde */}
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button type="submit" disabled={updateConfig.isPending}>
|
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
{updateConfig.isPending ? "Enregistrement..." : "Enregistrer la configuration"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -169,13 +169,6 @@
|
|||||||
"when": 1765830109012,
|
"when": 1765830109012,
|
||||||
"tag": "0023_jittery_gorgon",
|
"tag": "0023_jittery_gorgon",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 24,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1767770858914,
|
|
||||||
"tag": "0024_panoramic_goliath",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -520,58 +520,3 @@ export const logsNotifications = mysqlTable("logsNotifications", {
|
|||||||
|
|
||||||
export type LogNotification = typeof logsNotifications.$inferSelect;
|
export type LogNotification = typeof logsNotifications.$inferSelect;
|
||||||
export type InsertLogNotification = typeof logsNotifications.$inferInsert;
|
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;
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@
|
|||||||
"@trpc/client": "^11.6.0",
|
"@trpc/client": "^11.6.0",
|
||||||
"@trpc/react-query": "^11.6.0",
|
"@trpc/react-query": "^11.6.0",
|
||||||
"@trpc/server": "^11.6.0",
|
"@trpc/server": "^11.6.0",
|
||||||
"@types/pdfkit": "^0.17.4",
|
|
||||||
"axios": "^1.12.0",
|
"axios": "^1.12.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -75,7 +74,6 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nodemailer": "^7.0.11",
|
"nodemailer": "^7.0.11",
|
||||||
"openai": "^4.67.0",
|
"openai": "^4.67.0",
|
||||||
"pdfkit": "^0.17.2",
|
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|||||||
124
pnpm-lock.yaml
generated
124
pnpm-lock.yaml
generated
@@ -127,9 +127,6 @@ importers:
|
|||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.6.0
|
specifier: ^11.6.0
|
||||||
version: 11.6.0(typescript@5.9.3)
|
version: 11.6.0(typescript@5.9.3)
|
||||||
'@types/pdfkit':
|
|
||||||
specifier: ^0.17.4
|
|
||||||
version: 0.17.4
|
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.12.0
|
specifier: ^1.12.0
|
||||||
version: 1.12.2
|
version: 1.12.2
|
||||||
@@ -202,9 +199,6 @@ importers:
|
|||||||
openai:
|
openai:
|
||||||
specifier: ^4.67.0
|
specifier: ^4.67.0
|
||||||
version: 4.104.0(zod@4.1.12)
|
version: 4.104.0(zod@4.1.12)
|
||||||
pdfkit:
|
|
||||||
specifier: ^0.17.2
|
|
||||||
version: 0.17.2
|
|
||||||
react:
|
react:
|
||||||
specifier: ^19.1.1
|
specifier: ^19.1.1
|
||||||
version: 19.2.0
|
version: 19.2.0
|
||||||
@@ -2300,9 +2294,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
|
resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@swc/helpers@0.5.18':
|
|
||||||
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
|
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.14':
|
'@tailwindcss/node@4.1.14':
|
||||||
resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==}
|
resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==}
|
||||||
|
|
||||||
@@ -2770,9 +2761,6 @@ packages:
|
|||||||
'@types/pako@2.0.4':
|
'@types/pako@2.0.4':
|
||||||
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
||||||
|
|
||||||
'@types/pdfkit@0.17.4':
|
|
||||||
resolution: {integrity: sha512-odAmVuuguRxKh1X4pbMrJMp8ecwNqHRw6lweupvzK+wuyNmi6wzlUlGVZ9EqMvp3Bs2+L9Ty0sRlrvKL+gsQZg==}
|
|
||||||
|
|
||||||
'@types/qs@6.14.0':
|
'@types/qs@6.14.0':
|
||||||
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
||||||
|
|
||||||
@@ -2929,10 +2917,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||||
engines: {node: '>= 0.6.0'}
|
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:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
@@ -2970,9 +2954,6 @@ packages:
|
|||||||
brace-expansion@2.0.2:
|
brace-expansion@2.0.2:
|
||||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||||
|
|
||||||
brotli@1.3.3:
|
|
||||||
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
|
||||||
|
|
||||||
browserslist@4.26.3:
|
browserslist@4.26.3:
|
||||||
resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==}
|
resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -3066,10 +3047,6 @@ packages:
|
|||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
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:
|
clsx@2.1.1:
|
||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3162,9 +3139,6 @@ packages:
|
|||||||
crelt@1.0.6:
|
crelt@1.0.6:
|
||||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
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:
|
css-line-break@2.1.0:
|
||||||
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
||||||
|
|
||||||
@@ -3401,9 +3375,6 @@ packages:
|
|||||||
devlop@1.1.0:
|
devlop@1.1.0:
|
||||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||||
|
|
||||||
dfa@1.2.0:
|
|
||||||
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
|
|
||||||
|
|
||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||||
|
|
||||||
@@ -3697,9 +3668,6 @@ packages:
|
|||||||
debug:
|
debug:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
fontkit@2.0.4:
|
|
||||||
resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==}
|
|
||||||
|
|
||||||
form-data-encoder@1.7.2:
|
form-data-encoder@1.7.2:
|
||||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||||
|
|
||||||
@@ -3948,9 +3916,6 @@ packages:
|
|||||||
jose@6.1.0:
|
jose@6.1.0:
|
||||||
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
|
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:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -4076,9 +4041,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
linebreak@1.1.0:
|
|
||||||
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
|
|
||||||
|
|
||||||
linkify-it@5.0.0:
|
linkify-it@5.0.0:
|
||||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||||
|
|
||||||
@@ -4510,9 +4472,6 @@ packages:
|
|||||||
package-manager-detector@1.5.0:
|
package-manager-detector@1.5.0:
|
||||||
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
|
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:
|
pako@1.0.11:
|
||||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
|
||||||
@@ -4549,9 +4508,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||||
engines: {node: '>= 14.16'}
|
engines: {node: '>= 14.16'}
|
||||||
|
|
||||||
pdfkit@0.17.2:
|
|
||||||
resolution: {integrity: sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==}
|
|
||||||
|
|
||||||
performance-now@2.1.0:
|
performance-now@2.1.0:
|
||||||
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
||||||
|
|
||||||
@@ -4568,9 +4524,6 @@ packages:
|
|||||||
pkg-types@2.3.0:
|
pkg-types@2.3.0:
|
||||||
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
|
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
|
||||||
|
|
||||||
png-js@1.0.0:
|
|
||||||
resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==}
|
|
||||||
|
|
||||||
pnpm@10.18.0:
|
pnpm@10.18.0:
|
||||||
resolution: {integrity: sha512-6AT4ifHOzEDVctsITuw+SIFzn43sacD/ENLRvv+aTjCTg7ontbdQBZ1/TBSVNbbNDSyx7Trrc5I5pChKaPQM+g==}
|
resolution: {integrity: sha512-6AT4ifHOzEDVctsITuw+SIFzn43sacD/ENLRvv+aTjCTg7ontbdQBZ1/TBSVNbbNDSyx7Trrc5I5pChKaPQM+g==}
|
||||||
engines: {node: '>=18.12'}
|
engines: {node: '>=18.12'}
|
||||||
@@ -4845,9 +4798,6 @@ packages:
|
|||||||
resolve-pkg-maps@1.0.0:
|
resolve-pkg-maps@1.0.0:
|
||||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
restructure@3.0.2:
|
|
||||||
resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==}
|
|
||||||
|
|
||||||
rgbcolor@1.0.1:
|
rgbcolor@1.0.1:
|
||||||
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
|
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
|
||||||
engines: {node: '>= 0.8.15'}
|
engines: {node: '>= 0.8.15'}
|
||||||
@@ -5040,9 +4990,6 @@ packages:
|
|||||||
text-segmentation@1.0.3:
|
text-segmentation@1.0.3:
|
||||||
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
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:
|
tiny-invariant@1.3.3:
|
||||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
@@ -5127,12 +5074,6 @@ packages:
|
|||||||
undici-types@7.14.0:
|
undici-types@7.14.0:
|
||||||
resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==}
|
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:
|
unified@11.0.5:
|
||||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||||
|
|
||||||
@@ -8040,10 +7981,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@swc/helpers@0.5.18':
|
|
||||||
dependencies:
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.14':
|
'@tailwindcss/node@4.1.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
@@ -8563,10 +8500,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/pako@2.0.4': {}
|
'@types/pako@2.0.4': {}
|
||||||
|
|
||||||
'@types/pdfkit@0.17.4':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 24.7.0
|
|
||||||
|
|
||||||
'@types/qs@6.14.0': {}
|
'@types/qs@6.14.0': {}
|
||||||
|
|
||||||
'@types/raf@3.4.3':
|
'@types/raf@3.4.3':
|
||||||
@@ -8756,8 +8689,6 @@ snapshots:
|
|||||||
base64-arraybuffer@1.0.2:
|
base64-arraybuffer@1.0.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
base64-js@0.0.8: {}
|
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.8.12: {}
|
baseline-browser-mapping@2.8.12: {}
|
||||||
@@ -8807,10 +8738,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
brotli@1.3.3:
|
|
||||||
dependencies:
|
|
||||||
base64-js: 1.5.1
|
|
||||||
|
|
||||||
browserslist@4.26.3:
|
browserslist@4.26.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.8.12
|
baseline-browser-mapping: 2.8.12
|
||||||
@@ -8911,8 +8838,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
clone@2.1.2: {}
|
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
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):
|
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: {}
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
crypto-js@4.2.0: {}
|
|
||||||
|
|
||||||
css-line-break@2.1.0:
|
css-line-break@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
utrie: 1.0.2
|
utrie: 1.0.2
|
||||||
@@ -9231,8 +9154,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
|
|
||||||
dfa@1.2.0: {}
|
|
||||||
|
|
||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
@@ -9523,18 +9444,6 @@ snapshots:
|
|||||||
|
|
||||||
follow-redirects@1.15.11: {}
|
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-encoder@1.7.2: {}
|
||||||
|
|
||||||
form-data@4.0.4:
|
form-data@4.0.4:
|
||||||
@@ -9843,8 +9752,6 @@ snapshots:
|
|||||||
|
|
||||||
jose@6.1.0: {}
|
jose@6.1.0: {}
|
||||||
|
|
||||||
jpeg-exif@1.1.4: {}
|
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
@@ -9970,11 +9877,6 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.30.1
|
lightningcss-win32-arm64-msvc: 1.30.1
|
||||||
lightningcss-win32-x64-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:
|
linkify-it@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
uc.micro: 2.1.0
|
uc.micro: 2.1.0
|
||||||
@@ -10601,8 +10503,6 @@ snapshots:
|
|||||||
|
|
||||||
package-manager-detector@1.5.0: {}
|
package-manager-detector@1.5.0: {}
|
||||||
|
|
||||||
pako@0.2.9: {}
|
|
||||||
|
|
||||||
pako@1.0.11: {}
|
pako@1.0.11: {}
|
||||||
|
|
||||||
pako@2.1.0: {}
|
pako@2.1.0: {}
|
||||||
@@ -10635,14 +10535,6 @@ snapshots:
|
|||||||
|
|
||||||
pathval@2.0.1: {}
|
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:
|
performance-now@2.1.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -10662,8 +10554,6 @@ snapshots:
|
|||||||
exsolve: 1.0.7
|
exsolve: 1.0.7
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
png-js@1.0.0: {}
|
|
||||||
|
|
||||||
pnpm@10.18.0: {}
|
pnpm@10.18.0: {}
|
||||||
|
|
||||||
points-on-curve@0.2.0: {}
|
points-on-curve@0.2.0: {}
|
||||||
@@ -11038,8 +10928,6 @@ snapshots:
|
|||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
restructure@3.0.2: {}
|
|
||||||
|
|
||||||
rgbcolor@1.0.1:
|
rgbcolor@1.0.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -11292,8 +11180,6 @@ snapshots:
|
|||||||
utrie: 1.0.2
|
utrie: 1.0.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
tiny-inflate@1.0.3: {}
|
|
||||||
|
|
||||||
tiny-invariant@1.3.3: {}
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
tinybench@2.9.0: {}
|
tinybench@2.9.0: {}
|
||||||
@@ -11353,16 +11239,6 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.14.0: {}
|
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:
|
unified@11.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|||||||
@@ -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<number> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|||||||
20
todo.md
20
todo.md
@@ -442,23 +442,3 @@
|
|||||||
- [x] Interface de gestion des emails formateurs dans Configuration
|
- [x] Interface de gestion des emails formateurs dans Configuration
|
||||||
- [x] Tableau de bord des notifications avec historique des emails envoyés
|
- [x] Tableau de bord des notifications avec historique des emails envoyés
|
||||||
- [x] Lien vers questionnaire de satisfaction dans l'email de remerciement
|
- [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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user