diff --git a/.manus/db/db-query-1768376558906.json b/.manus/db/db-query-1768376558906.json new file mode 100644 index 0000000..1b02d95 --- /dev/null +++ b/.manus/db/db-query-1768376558906.json @@ -0,0 +1,90 @@ +{ + "query": "DESCRIBE questionnaires;", + "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 DESCRIBE questionnaires;", + "rows": [ + { + "Field": "id", + "Type": "int(11)", + "Null": "NO", + "Key": "PRI", + "Default": "NULL", + "Extra": "auto_increment" + }, + { + "Field": "nom", + "Type": "varchar(255)", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "type", + "Type": "enum('satisfaction','evaluation_pre','evaluation_post')", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "description", + "Type": "text", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "formationId", + "Type": "int(11)", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "actif", + "Type": "tinyint(1)", + "Null": "NO", + "Key": "", + "Default": "1", + "Extra": "" + }, + { + "Field": "envoiAutomatique", + "Type": "tinyint(1)", + "Null": "NO", + "Key": "", + "Default": "0", + "Extra": "" + }, + { + "Field": "delaiEnvoiJours", + "Type": "int(11)", + "Null": "NO", + "Key": "", + "Default": "1", + "Extra": "" + }, + { + "Field": "createdAt", + "Type": "timestamp", + "Null": "NO", + "Key": "", + "Default": "CURRENT_TIMESTAMP", + "Extra": "" + }, + { + "Field": "updatedAt", + "Type": "timestamp", + "Null": "NO", + "Key": "", + "Default": "CURRENT_TIMESTAMP", + "Extra": "DEFAULT_GENERATED on update CURRENT_TIMESTAMP" + } + ], + "messages": [], + "stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint(11)\tNO\tPRI\tNULL\tauto_increment\nnom\tvarchar(255)\tNO\t\tNULL\t\ntype\tenum('satisfaction','evaluation_pre','evaluation_post')\tNO\t\tNULL\t\ndescription\ttext\tYES\t\tNULL\t\nformationId\tint(11)\tYES\t\tNULL\t\nactif\ttinyint(1)\tNO\t\t1\t\nenvoiAutomatique\ttinyint(1)\tNO\t\t0\t\ndelaiEnvoiJours\tint(11)\tNO\t\t1\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\nupdatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\tDEFAULT_GENERATED on update CURRENT_TIMESTAMP\n", + "stderr": "", + "execution_time_ms": 710 +} \ No newline at end of file diff --git a/.manus/db/db-query-1768376568790.json b/.manus/db/db-query-1768376568790.json new file mode 100644 index 0000000..2790218 --- /dev/null +++ b/.manus/db/db-query-1768376568790.json @@ -0,0 +1,40 @@ +{ + "query": "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'questionnaires';", + "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 COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'questionnaires';", + "rows": [ + { + "COLUMN_NAME": "id" + }, + { + "COLUMN_NAME": "nom" + }, + { + "COLUMN_NAME": "type" + }, + { + "COLUMN_NAME": "description" + }, + { + "COLUMN_NAME": "formationId" + }, + { + "COLUMN_NAME": "actif" + }, + { + "COLUMN_NAME": "envoiAutomatique" + }, + { + "COLUMN_NAME": "delaiEnvoiJours" + }, + { + "COLUMN_NAME": "createdAt" + }, + { + "COLUMN_NAME": "updatedAt" + } + ], + "messages": [], + "stdout": "COLUMN_NAME\nid\nnom\ntype\ndescription\nformationId\nactif\nenvoiAutomatique\ndelaiEnvoiJours\ncreatedAt\nupdatedAt\n", + "stderr": "", + "execution_time_ms": 61 +} \ No newline at end of file diff --git a/.manus/db/db-query-1768376575187.json b/.manus/db/db-query-1768376575187.json new file mode 100644 index 0000000..91d5f70 --- /dev/null +++ b/.manus/db/db-query-1768376575187.json @@ -0,0 +1,90 @@ +{ + "query": "SHOW COLUMNS FROM questionnaires;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SHOW COLUMNS FROM questionnaires;", + "rows": [ + { + "Field": "id", + "Type": "int(11)", + "Null": "NO", + "Key": "PRI", + "Default": "NULL", + "Extra": "auto_increment" + }, + { + "Field": "nom", + "Type": "varchar(255)", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "type", + "Type": "enum('satisfaction','evaluation_pre','evaluation_post')", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "description", + "Type": "text", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "formationId", + "Type": "int(11)", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "actif", + "Type": "tinyint(1)", + "Null": "NO", + "Key": "", + "Default": "1", + "Extra": "" + }, + { + "Field": "envoiAutomatique", + "Type": "tinyint(1)", + "Null": "NO", + "Key": "", + "Default": "0", + "Extra": "" + }, + { + "Field": "delaiEnvoiJours", + "Type": "int(11)", + "Null": "NO", + "Key": "", + "Default": "1", + "Extra": "" + }, + { + "Field": "createdAt", + "Type": "timestamp", + "Null": "NO", + "Key": "", + "Default": "CURRENT_TIMESTAMP", + "Extra": "" + }, + { + "Field": "updatedAt", + "Type": "timestamp", + "Null": "NO", + "Key": "", + "Default": "CURRENT_TIMESTAMP", + "Extra": "DEFAULT_GENERATED on update CURRENT_TIMESTAMP" + } + ], + "messages": [], + "stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint(11)\tNO\tPRI\tNULL\tauto_increment\nnom\tvarchar(255)\tNO\t\tNULL\t\ntype\tenum('satisfaction','evaluation_pre','evaluation_post')\tNO\t\tNULL\t\ndescription\ttext\tYES\t\tNULL\t\nformationId\tint(11)\tYES\t\tNULL\t\nactif\ttinyint(1)\tNO\t\t1\t\nenvoiAutomatique\ttinyint(1)\tNO\t\t0\t\ndelaiEnvoiJours\tint(11)\tNO\t\t1\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\nupdatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\tDEFAULT_GENERATED on update CURRENT_TIMESTAMP\n", + "stderr": "", + "execution_time_ms": 62 +} \ No newline at end of file diff --git a/.manus/db/db-query-1768376586515.json b/.manus/db/db-query-1768376586515.json new file mode 100644 index 0000000..2b0f649 --- /dev/null +++ b/.manus/db/db-query-1768376586515.json @@ -0,0 +1,9 @@ +{ + "query": "ALTER TABLE questionnaires CHANGE COLUMN nom titre VARCHAR(255) NOT NULL;", + "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 ALTER TABLE questionnaires CHANGE COLUMN nom titre VARCHAR(255) NOT NULL;", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 172 +} \ No newline at end of file diff --git a/.manus/db/db-query-1768376894838.json b/.manus/db/db-query-1768376894838.json new file mode 100644 index 0000000..062587c --- /dev/null +++ b/.manus/db/db-query-1768376894838.json @@ -0,0 +1,114 @@ +{ + "query": "SHOW COLUMNS FROM questions;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SHOW COLUMNS FROM questions;", + "rows": [ + { + "Field": "id", + "Type": "int(11)", + "Null": "NO", + "Key": "PRI", + "Default": "NULL", + "Extra": "auto_increment" + }, + { + "Field": "questionnaireId", + "Type": "int(11)", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "ordre", + "Type": "int(11)", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "texte", + "Type": "text", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "typeQuestion", + "Type": "enum('choix_multiple','echelle','texte_libre','oui_non')", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "options", + "Type": "text", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "echelleMin", + "Type": "int(11)", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "echelleMax", + "Type": "int(11)", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "echelleLabelMin", + "Type": "varchar(255)", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "echelleLabelMax", + "Type": "varchar(255)", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "obligatoire", + "Type": "tinyint(1)", + "Null": "NO", + "Key": "", + "Default": "0", + "Extra": "" + }, + { + "Field": "createdAt", + "Type": "timestamp", + "Null": "NO", + "Key": "", + "Default": "CURRENT_TIMESTAMP", + "Extra": "" + }, + { + "Field": "updatedAt", + "Type": "timestamp", + "Null": "NO", + "Key": "", + "Default": "CURRENT_TIMESTAMP", + "Extra": "DEFAULT_GENERATED on update CURRENT_TIMESTAMP" + } + ], + "messages": [], + "stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint(11)\tNO\tPRI\tNULL\tauto_increment\nquestionnaireId\tint(11)\tNO\t\tNULL\t\nordre\tint(11)\tNO\t\tNULL\t\ntexte\ttext\tNO\t\tNULL\t\ntypeQuestion\tenum('choix_multiple','echelle','texte_libre','oui_non')\tNO\t\tNULL\t\noptions\ttext\tYES\t\tNULL\t\nechelleMin\tint(11)\tYES\t\tNULL\t\nechelleMax\tint(11)\tYES\t\tNULL\t\nechelleLabelMin\tvarchar(255)\tYES\t\tNULL\t\nechelleLabelMax\tvarchar(255)\tYES\t\tNULL\t\nobligatoire\ttinyint(1)\tNO\t\t0\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\nupdatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\tDEFAULT_GENERATED on update CURRENT_TIMESTAMP\n", + "stderr": "", + "execution_time_ms": 57 +} \ No newline at end of file diff --git a/.manus/db/db-query-1768376902477.json b/.manus/db/db-query-1768376902477.json new file mode 100644 index 0000000..178bfb4 --- /dev/null +++ b/.manus/db/db-query-1768376902477.json @@ -0,0 +1,49 @@ +{ + "query": "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'questions';", + "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 COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'questions';", + "rows": [ + { + "COLUMN_NAME": "id" + }, + { + "COLUMN_NAME": "questionnaireId" + }, + { + "COLUMN_NAME": "ordre" + }, + { + "COLUMN_NAME": "texte" + }, + { + "COLUMN_NAME": "typeQuestion" + }, + { + "COLUMN_NAME": "options" + }, + { + "COLUMN_NAME": "echelleMin" + }, + { + "COLUMN_NAME": "echelleMax" + }, + { + "COLUMN_NAME": "echelleLabelMin" + }, + { + "COLUMN_NAME": "echelleLabelMax" + }, + { + "COLUMN_NAME": "obligatoire" + }, + { + "COLUMN_NAME": "createdAt" + }, + { + "COLUMN_NAME": "updatedAt" + } + ], + "messages": [], + "stdout": "COLUMN_NAME\nid\nquestionnaireId\nordre\ntexte\ntypeQuestion\noptions\nechelleMin\nechelleMax\nechelleLabelMin\nechelleLabelMax\nobligatoire\ncreatedAt\nupdatedAt\n", + "stderr": "", + "execution_time_ms": 62 +} \ No newline at end of file diff --git a/.manus/db/db-query-1768376942069.json b/.manus/db/db-query-1768376942069.json new file mode 100644 index 0000000..24ee7e2 --- /dev/null +++ b/.manus/db/db-query-1768376942069.json @@ -0,0 +1,9 @@ +{ + "query": "ALTER TABLE questions ADD COLUMN IF NOT EXISTS valeurMin INT DEFAULT NULL; ALTER TABLE questions ADD COLUMN IF NOT EXISTS valeurMax INT DEFAULT NULL;", + "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 ALTER TABLE questions ADD COLUMN IF NOT EXISTS valeurMin INT DEFAULT NULL; ALTER TABLE questions ADD COLUMN IF NOT EXISTS valeurMax INT DEFAULT NULL;", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 950 +} \ No newline at end of file diff --git a/client/src/pages/AdminQuestionnaireEdit.tsx b/client/src/pages/AdminQuestionnaireEdit.tsx index 35888a5..4cdcafe 100644 --- a/client/src/pages/AdminQuestionnaireEdit.tsx +++ b/client/src/pages/AdminQuestionnaireEdit.tsx @@ -436,7 +436,7 @@ export default function AdminQuestionnaireEdit() { - {nom || "Questionnaire sans titre"} + {titre || "Questionnaire sans titre"} {description &&

{description}

}
diff --git a/client/src/pages/QuestionnaireReponse.tsx b/client/src/pages/QuestionnaireReponse.tsx index 8aa9f50..c182f71 100644 --- a/client/src/pages/QuestionnaireReponse.tsx +++ b/client/src/pages/QuestionnaireReponse.tsx @@ -113,7 +113,7 @@ export default function QuestionnaireReponse() { {APP_LOGO && ( {APP_TITLE} )} -

{data.questionnaire?.nom || 'Questionnaire'}

+

{data.questionnaire?.titre || 'Questionnaire'}

{data.questionnaire?.description && (

{data.questionnaire.description}

)} diff --git a/create-deployment-package.sh b/create-deployment-package.sh new file mode 100755 index 0000000..62ccea3 --- /dev/null +++ b/create-deployment-package.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Script de création du package de déploiement +# Version: 1.0 +# Date: 13 janvier 2026 + +set -e + +echo "=== Création du package de déploiement ===" +echo "" + +# Variables +PROJECT_DIR="/home/ubuntu/formation-manager-itinova" +PACKAGE_DIR="$PROJECT_DIR/deployment-package" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +ARCHIVE_NAME="formation-manager-itinova-${TIMESTAMP}.tar.gz" + +# Nettoyer le répertoire de package +echo "Nettoyage du répertoire de package..." +rm -rf "$PACKAGE_DIR" +mkdir -p "$PACKAGE_DIR" + +# Créer la structure du package +echo "Création de la structure du package..." +mkdir -p "$PACKAGE_DIR/scripts" +mkdir -p "$PACKAGE_DIR/docs" +mkdir -p "$PACKAGE_DIR/source" + +# Copier les scripts de déploiement +echo "Copie des scripts de déploiement..." +cp "$PROJECT_DIR/deploy.sh" "$PACKAGE_DIR/scripts/" 2>/dev/null || true +cp "$PROJECT_DIR/deploy-update.sh" "$PACKAGE_DIR/scripts/" 2>/dev/null || true +cp "$PROJECT_DIR/deploy-update-manual.sh" "$PACKAGE_DIR/scripts/" 2>/dev/null || true +cp "$PROJECT_DIR/deploy-update-targz.sh" "$PACKAGE_DIR/scripts/" 2>/dev/null || true +cp "$PROJECT_DIR/setup-env.sh" "$PACKAGE_DIR/scripts/" 2>/dev/null || true +cp "$PROJECT_DIR/setup-https.sh" "$PACKAGE_DIR/scripts/" 2>/dev/null || true +cp "$PROJECT_DIR/create-admin.sh" "$PACKAGE_DIR/scripts/" 2>/dev/null || true +cp "$PROJECT_DIR/reset-admin.sh" "$PACKAGE_DIR/scripts/" 2>/dev/null || true +cp "$PROJECT_DIR/diagnose.sh" "$PACKAGE_DIR/scripts/" 2>/dev/null || true + +# Rendre les scripts exécutables +chmod +x "$PACKAGE_DIR/scripts/"*.sh + +# Copier la documentation +echo "Copie de la documentation..." +cp "$PROJECT_DIR/GUIDE_DEPLOIEMENT.md" "$PACKAGE_DIR/docs/" 2>/dev/null || true +cp "$PROJECT_DIR/DEPLOIEMENT-MANUEL.md" "$PACKAGE_DIR/docs/" 2>/dev/null || true +cp "$PROJECT_DIR/DEPLOIEMENT-TARGZ.md" "$PACKAGE_DIR/docs/" 2>/dev/null || true +cp "$PROJECT_DIR/INSTRUCTIONS-DEPLOIEMENT.md" "$PACKAGE_DIR/docs/" 2>/dev/null || true +cp "$PROJECT_DIR/GUIDE_CONFIGURATION_ATTESTATIONS.md" "$PACKAGE_DIR/docs/" 2>/dev/null || true +cp "$PROJECT_DIR/README.md" "$PACKAGE_DIR/docs/" 2>/dev/null || true + +# Copier les sources (sans node_modules et fichiers de build) +echo "Copie des sources de l'application..." +rsync -av --progress \ + --exclude 'node_modules' \ + --exclude 'dist' \ + --exclude '.git' \ + --exclude 'deployment-package' \ + --exclude '*.log' \ + --exclude '.env' \ + --exclude '.DS_Store' \ + "$PROJECT_DIR/" "$PACKAGE_DIR/source/" + +# Créer l'archive +echo "Création de l'archive..." +cd "$PROJECT_DIR" +tar -czf "$ARCHIVE_NAME" -C deployment-package . + +echo "" +echo "=== Package créé avec succès ===" +echo "Archive: $PROJECT_DIR/$ARCHIVE_NAME" +echo "Taille: $(du -h "$ARCHIVE_NAME" | cut -f1)" +echo "" +echo "Pour transférer vers votre serveur VPS:" +echo "scp $ARCHIVE_NAME user@your-server:/path/to/destination/" +echo "" diff --git a/deployment-package/CONFIGURATION.md b/deployment-package/CONFIGURATION.md deleted file mode 100644 index 22b961c..0000000 --- a/deployment-package/CONFIGURATION.md +++ /dev/null @@ -1,396 +0,0 @@ -# Documentation de Configuration - Formation Manager Itinova - -**Version:** 7a737c05 -**Date:** 24 novembre 2025 -**Auteur:** Manus AI - ---- - -## Vue d'ensemble - -Ce document détaille toutes les variables de configuration et les paramètres nécessaires pour personnaliser et sécuriser votre installation de Formation Manager Itinova. - ---- - -## Variables d'environnement - -Toutes les variables d'environnement doivent être définies dans le fichier `.env` à la racine du projet. Ce fichier ne doit **jamais** être commité dans le dépôt Git pour des raisons de sécurité. - -### Base de données - -#### DATABASE_URL - -**Type:** String (URL de connexion) -**Requis:** Oui -**Format:** `mysql://utilisateur:motdepasse@hote:port/nombase` - -La chaîne de connexion complète à votre base de données MySQL ou TiDB. Cette URL contient toutes les informations nécessaires pour établir la connexion : utilisateur, mot de passe, hôte, port et nom de la base de données. - -**Exemple:** -``` -DATABASE_URL=mysql://formation_user:SecureP@ssw0rd@localhost:3306/formation_manager -``` - -**Recommandations de sécurité:** -- Utilisez un mot de passe fort d'au moins 16 caractères -- Créez un utilisateur dédié avec uniquement les permissions nécessaires -- Si possible, utilisez une connexion SSL/TLS (ajoutez `?ssl=true` à la fin de l'URL) -- Pour TiDB Cloud, utilisez toujours SSL et les certificats fournis - ---- - -### Authentification et sécurité - -#### JWT_SECRET - -**Type:** String (clé secrète) -**Requis:** Oui -**Longueur minimale:** 32 caractères (64 recommandés) - -La clé secrète utilisée pour signer les tokens JWT (JSON Web Tokens) qui gèrent les sessions utilisateur. Cette clé est critique pour la sécurité de votre application. - -**Génération d'une clé sécurisée:** -```bash -openssl rand -hex 32 -``` - -**Exemple:** -``` -JWT_SECRET=a3f8b2c9d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0 -``` - -**Important:** Ne réutilisez jamais la même clé entre différents environnements (développement, staging, production). - -#### OAUTH_SERVER_URL - -**Type:** URL -**Requis:** Oui -**Valeur par défaut:** `https://api.manus.im` - -L'URL du serveur OAuth Manus utilisé pour l'authentification des utilisateurs. Cette valeur ne doit généralement pas être modifiée sauf si vous utilisez une instance Manus personnalisée. - -#### VITE_OAUTH_PORTAL_URL - -**Type:** URL -**Requis:** Oui -**Valeur par défaut:** `https://auth.manus.im` - -L'URL du portail de connexion Manus où les utilisateurs sont redirigés pour s'authentifier. Cette URL est utilisée côté client (frontend). - ---- - -### Identité du propriétaire - -#### OWNER_OPEN_ID - -**Type:** String -**Requis:** Oui - -L'identifiant OpenID unique du propriétaire de l'application. Cet utilisateur aura automatiquement le rôle `admin` lors de sa première connexion et recevra les notifications système. - -**Comment obtenir votre Open ID:** -1. Connectez-vous à votre compte Manus -2. Accédez à votre profil utilisateur -3. Copiez votre Open ID depuis les paramètres du compte - -#### OWNER_NAME - -**Type:** String -**Requis:** Oui - -Le nom complet du propriétaire de l'application, utilisé dans les notifications et les logs système. - -**Exemple:** -``` -OWNER_OPEN_ID=usr_abc123def456 -OWNER_NAME=Olivier Pareige -``` - ---- - -### Configuration de l'application - -#### VITE_APP_ID - -**Type:** String -**Requis:** Oui - -L'identifiant unique de votre application Manus. Cette valeur est fournie lors de la création de votre application sur la plateforme Manus. - -#### VITE_APP_TITLE - -**Type:** String -**Requis:** Non -**Valeur par défaut:** `Gestion des Formations Manager Itinova` - -Le titre de l'application affiché dans l'interface utilisateur, les onglets du navigateur et les emails. Vous pouvez personnaliser ce titre selon vos besoins. - -**Exemple:** -``` -VITE_APP_TITLE=Formation Manager - Itinova -``` - -#### VITE_APP_LOGO - -**Type:** String (chemin relatif) -**Requis:** Non -**Valeur par défaut:** `/logo.png` - -Le chemin vers le fichier logo de l'application. Le fichier doit être placé dans le répertoire `client/public/`. Le logo est affiché dans le header et les emails. - -**Formats supportés:** PNG, SVG, JPG -**Taille recommandée:** 200x50 pixels (ratio 4:1) - ---- - -### APIs Manus intégrées - -Ces variables sont généralement fournies automatiquement par la plateforme Manus lors du déploiement. Elles donnent accès aux services intégrés (LLM, stockage, notifications, etc.). - -#### BUILT_IN_FORGE_API_URL - -**Type:** URL -**Requis:** Oui (pour les fonctionnalités avancées) - -L'URL de base des APIs Manus Forge utilisées côté serveur. - -#### BUILT_IN_FORGE_API_KEY - -**Type:** String (clé API) -**Requis:** Oui (pour les fonctionnalités avancées) - -La clé d'authentification pour accéder aux APIs Manus côté serveur. Cette clé ne doit **jamais** être exposée côté client. - -#### VITE_FRONTEND_FORGE_API_KEY - -**Type:** String (clé API) -**Requis:** Non - -Une clé API distincte pour les appels côté client (frontend). Cette clé a des permissions limitées pour des raisons de sécurité. - -#### VITE_FRONTEND_FORGE_API_URL - -**Type:** URL -**Requis:** Non - -L'URL des APIs Manus accessibles depuis le frontend. - ---- - -### Analytics (optionnel) - -#### VITE_ANALYTICS_ENDPOINT - -**Type:** URL -**Requis:** Non - -L'URL de votre service d'analytics (compatible Plausible/Umami). Si défini, les statistiques de visite seront envoyées à ce endpoint. - -#### VITE_ANALYTICS_WEBSITE_ID - -**Type:** String -**Requis:** Non (sauf si VITE_ANALYTICS_ENDPOINT est défini) - -L'identifiant unique de votre site dans le système d'analytics. - -**Exemple:** -``` -VITE_ANALYTICS_ENDPOINT=https://analytics.example.com -VITE_ANALYTICS_WEBSITE_ID=formation-manager-prod -``` - ---- - -### Configuration de l'environnement - -#### NODE_ENV - -**Type:** String -**Requis:** Oui -**Valeurs possibles:** `development`, `production`, `test` - -Définit l'environnement d'exécution de l'application. En production, cette valeur **doit** être `production` pour activer les optimisations et désactiver les outils de développement. - -#### PORT - -**Type:** Number -**Requis:** Non -**Valeur par défaut:** `3000` - -Le port sur lequel le serveur Express écoute les connexions. Si vous utilisez Nginx comme reverse proxy, gardez la valeur par défaut. - ---- - -## Configuration SMTP (emails) - -L'application utilise un serveur SMTP pour envoyer les emails automatiques (confirmations, rappels, teasers). La configuration SMTP se fait via l'interface web dans la section "Configuration SMTP" du menu. - -### Paramètres SMTP requis - -| Paramètre | Description | Exemple | -|-----------|-------------|---------| -| Hôte SMTP | Adresse du serveur SMTP | `smtp.gmail.com` | -| Port | Port du serveur (25, 465, 587) | `587` | -| Sécurité | TLS ou SSL | `TLS` | -| Utilisateur | Adresse email d'envoi | `noreply@itinova.org` | -| Mot de passe | Mot de passe ou app password | `****************` | - -### Fournisseurs SMTP recommandés - -**Gmail** : Gratuit jusqu'à 500 emails/jour. Nécessite un "App Password" si 2FA activé. -**SendGrid** : 100 emails/jour gratuits, excellent pour la délivrabilité. -**Amazon SES** : Très économique, $0.10 pour 1000 emails. -**Mailgun** : 5000 emails/mois gratuits les 3 premiers mois. - -### Test de configuration - -Après avoir configuré SMTP, utilisez la fonction "Envoyer un email de test" dans l'interface pour vérifier que les emails sont bien envoyés et reçus. - ---- - -## Configuration Nginx - -### Certificat SSL - -Pour activer HTTPS, vous devez obtenir un certificat SSL. La méthode recommandée est d'utiliser Let's Encrypt (gratuit) : - -```bash -sudo apt install certbot python3-certbot-nginx -sudo certbot --nginx -d votre-domaine.com -``` - -Certbot configurera automatiquement Nginx et renouvellera le certificat avant expiration. - -### Optimisations recommandées - -**Compression Gzip** : Déjà activée dans la configuration fournie, réduit la bande passante de 70%. - -**Cache des assets statiques** : Les fichiers CSS, JS et images sont mis en cache 1 an côté client pour améliorer les performances. - -**HTTP/2** : Activé par défaut avec `http2` dans la directive `listen 443 ssl http2`. - -**Rate limiting** : Pour protéger contre les attaques DDoS, ajoutez dans votre configuration Nginx : - -```nginx -limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; - -location /api/ { - limit_req zone=api burst=20 nodelay; - proxy_pass http://formation_backend; -} -``` - ---- - -## Sécurité avancée - -### Rotation des secrets - -Les clés sensibles (JWT_SECRET, mots de passe DB) doivent être changées régulièrement : - -1. **Générer une nouvelle clé JWT** -2. **Mettre à jour `.env`** -3. **Redémarrer l'application** -4. **Tous les utilisateurs devront se reconnecter** - -### Sauvegarde des configurations - -Sauvegardez régulièrement votre fichier `.env` dans un gestionnaire de secrets sécurisé (pas dans Git) : - -- **Vault** (HashiCorp) -- **AWS Secrets Manager** -- **Azure Key Vault** -- **1Password** (pour les petites équipes) - -### Audit de sécurité - -Vérifiez régulièrement : - -- [ ] Les permissions de fichiers (`.env` doit être en 600) -- [ ] Les logs d'accès pour détecter des comportements suspects -- [ ] Les mises à jour de sécurité des dépendances (`pnpm audit`) -- [ ] La force des mots de passe de base de données -- [ ] L'expiration des certificats SSL - ---- - -## Personnalisation - -### Modification du logo - -1. Placez votre nouveau logo dans `client/public/` -2. Mettez à jour `VITE_APP_LOGO` dans `.env` -3. Modifiez également `APP_LOGO` dans `client/src/const.ts` -4. Rebuild l'application : `pnpm run build` -5. Mettez à jour le favicon via l'interface de gestion - -### Personnalisation des couleurs - -Les couleurs de l'interface sont définies dans `client/src/index.css`. Modifiez les variables CSS dans la section `:root` pour personnaliser le thème : - -```css -:root { - --primary: 221.2 83.2% 53.3%; /* Bleu principal */ - --secondary: 210 40% 96.1%; - --accent: 210 40% 96.1%; - /* ... */ -} -``` - -### Ajout de fonctions personnalisées - -Pour ajouter des fonctions spécifiques à votre organisation : - -1. **Schéma** : Ajoutez les tables dans `drizzle/schema.ts` -2. **Backend** : Créez les fonctions dans `server/db.ts` -3. **API** : Ajoutez les procédures dans `server/routers.ts` -4. **Frontend** : Créez les pages dans `client/src/pages/` -5. **Navigation** : Ajoutez les liens dans `client/src/components/DashboardLayout.tsx` - ---- - -## Troubleshooting - -### L'application ne démarre pas - -**Vérifiez le fichier `.env`** : Assurez-vous que toutes les variables requises sont définies. - -**Testez la connexion DB** : -```bash -mysql -h HOST -u USER -p DATABASE -``` - -**Consultez les logs** : -```bash -sudo journalctl -u formation-manager -n 100 -``` - -### Les emails ne sont pas envoyés - -**Vérifiez la configuration SMTP** : Utilisez la fonction de test dans l'interface. - -**Consultez les logs** : Les erreurs SMTP sont loguées dans les journaux système. - -**Vérifiez les ports** : Assurez-vous que les ports SMTP (587, 465) ne sont pas bloqués par le pare-feu. - -### Erreurs de base de données - -**Migrations non appliquées** : Exécutez `pnpm db:push` pour synchroniser le schéma. - -**Connexion refusée** : Vérifiez que MySQL est démarré et accessible. - -**Erreur d'authentification** : Vérifiez les identifiants dans `DATABASE_URL`. - ---- - -## Support - -Pour toute question sur la configuration : - -- **Documentation technique** : README.md du projet -- **Support Manus** : https://help.manus.im -- **Contact** : o.pareige@itinova.org - ---- - -**Fin de la documentation de configuration** diff --git a/deployment-package/CONFIGURATION.pdf b/deployment-package/CONFIGURATION.pdf deleted file mode 100644 index ec67f5e..0000000 Binary files a/deployment-package/CONFIGURATION.pdf and /dev/null differ diff --git a/deployment-package/GUIDE_DEPLOIEMENT.md b/deployment-package/GUIDE_DEPLOIEMENT.md deleted file mode 100644 index df41dd5..0000000 --- a/deployment-package/GUIDE_DEPLOIEMENT.md +++ /dev/null @@ -1,681 +0,0 @@ -# Guide de Déploiement - Formation Manager Itinova - -**Version:** 7a737c05 -**Date:** 24 novembre 2025 -**Auteur:** Manus AI - ---- - -## Vue d'ensemble - -Ce document fournit les instructions complètes pour déployer l'application **Formation Manager Itinova** sur un serveur de production. L'application est construite avec React 19, Express 4, tRPC 11 et utilise une base de données MySQL/TiDB. - ---- - -## Prérequis système - -Avant de commencer le déploiement, assurez-vous que votre serveur dispose des éléments suivants : - -### Configuration matérielle minimale - -| Composant | Spécification minimale | Recommandé | -|-----------|------------------------|------------| -| CPU | 2 cœurs | 4 cœurs | -| RAM | 2 GB | 4 GB | -| Stockage | 20 GB | 50 GB SSD | -| Bande passante | 100 Mbps | 1 Gbps | - -### Logiciels requis - -L'application nécessite les logiciels suivants installés sur le serveur : - -**Node.js version 22.13.0 ou supérieure** : Le runtime JavaScript est essentiel pour exécuter l'application côté serveur. La version 22.13.0 garantit la compatibilité avec toutes les dépendances du projet. - -**pnpm** : Le gestionnaire de paquets pnpm est utilisé pour installer les dépendances de manière efficace. Il offre de meilleures performances et une gestion optimisée de l'espace disque par rapport à npm. - -**MySQL 8.0 ou TiDB** : La base de données relationnelle stocke toutes les données de l'application (formations, séquences, apprenants, inscriptions). TiDB est compatible MySQL et offre une scalabilité horizontale pour les déploiements à grande échelle. - -**Nginx ou Apache** : Un serveur web reverse proxy est recommandé pour gérer le SSL/TLS, la compression et le cache statique. Nginx est préféré pour ses performances supérieures. - -**Systemd** : Le gestionnaire de services Linux permet de gérer l'application comme un service système, assurant le démarrage automatique et la supervision. - -**Git** : Le système de contrôle de version est nécessaire pour cloner le dépôt et effectuer les mises à jour. - ---- - -## Architecture de déploiement - -L'application suit une architecture client-serveur moderne avec les composants suivants : - -### Composants principaux - -**Frontend React** : L'interface utilisateur est construite avec React 19 et Tailwind CSS 4. Le build de production génère des fichiers statiques optimisés (HTML, CSS, JavaScript) qui sont servis par le serveur Express. - -**Backend Express + tRPC** : Le serveur Node.js gère les requêtes API via tRPC, offrant une communication type-safe entre le client et le serveur. Toutes les routes API sont préfixées par `/api/`. - -**Base de données MySQL/TiDB** : La couche de persistance utilise Drizzle ORM pour interagir avec la base de données. Le schéma est défini dans `drizzle/schema.ts` et les migrations sont gérées automatiquement. - -**Authentification OAuth** : Le système d'authentification utilise Manus OAuth pour la gestion des utilisateurs. Les sessions sont stockées dans des cookies HTTP-only sécurisés. - -### Flux de requêtes - -Les requêtes utilisateur suivent ce chemin : **Navigateur → Nginx (SSL/TLS) → Express (port 3000) → tRPC → Base de données**. Les fichiers statiques sont servis directement par Express depuis le répertoire `dist/public/`. - ---- - -## Étape 1 : Préparation du serveur - -### Installation de Node.js - -La première étape consiste à installer Node.js version 22.13.0 sur votre serveur Ubuntu. Utilisez les commandes suivantes pour installer Node.js via le gestionnaire de versions nvm : - -```bash -# Installer nvm (Node Version Manager) -curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash - -# Recharger le profil shell -source ~/.bashrc - -# Installer Node.js 22.13.0 -nvm install 22.13.0 - -# Définir comme version par défaut -nvm use 22.13.0 -nvm alias default 22.13.0 - -# Vérifier l'installation -node --version # Doit afficher v22.13.0 -``` - -### Installation de pnpm - -Une fois Node.js installé, installez pnpm globalement : - -```bash -npm install -g pnpm - -# Vérifier l'installation -pnpm --version -``` - -### Configuration de la base de données - -Créez une base de données MySQL dédiée pour l'application. Connectez-vous à MySQL et exécutez les commandes suivantes : - -```sql -CREATE DATABASE formation_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER 'formation_user'@'localhost' IDENTIFIED BY 'MOT_DE_PASSE_SECURISE'; -GRANT ALL PRIVILEGES ON formation_manager.* TO 'formation_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -Remplacez `MOT_DE_PASSE_SECURISE` par un mot de passe fort généré aléatoirement. Notez les informations de connexion pour la configuration ultérieure. - -### Création de l'utilisateur système - -Pour des raisons de sécurité, créez un utilisateur système dédié pour exécuter l'application : - -```bash -sudo useradd -r -s /bin/bash -d /opt/formation-manager formation-manager -sudo mkdir -p /opt/formation-manager -sudo chown formation-manager:formation-manager /opt/formation-manager -``` - ---- - -## Étape 2 : Clonage et configuration - -### Clonage du dépôt - -Connectez-vous en tant qu'utilisateur `formation-manager` et clonez le dépôt Git : - -```bash -sudo su - formation-manager -cd /opt/formation-manager -git clone app -cd app -git checkout 7a737c05 -``` - -Remplacez `` par l'URL réelle de votre dépôt Git. - -### Configuration des variables d'environnement - -Créez un fichier `.env` à la racine du projet avec les variables suivantes : - -```bash -# Base de données -DATABASE_URL=mysql://formation_user:MOT_DE_PASSE_SECURISE@localhost:3306/formation_manager - -# Authentification -JWT_SECRET= -OAUTH_SERVER_URL=https://api.manus.im -VITE_OAUTH_PORTAL_URL=https://auth.manus.im - -# Identifiants propriétaire -OWNER_OPEN_ID= -OWNER_NAME= - -# Configuration application -VITE_APP_ID= -VITE_APP_TITLE="Gestion des Formations Manager Itinova" -VITE_APP_LOGO=/logo.png - -# APIs Manus (fournis automatiquement par la plateforme) -BUILT_IN_FORGE_API_URL= -BUILT_IN_FORGE_API_KEY= -VITE_FRONTEND_FORGE_API_KEY= -VITE_FRONTEND_FORGE_API_URL= - -# Analytics (optionnel) -VITE_ANALYTICS_ENDPOINT= -VITE_ANALYTICS_WEBSITE_ID= - -# Production -NODE_ENV=production -PORT=3000 -``` - -**Important** : Générez une clé JWT sécurisée avec la commande suivante : - -```bash -openssl rand -hex 32 -``` - -### Installation des dépendances - -Installez toutes les dépendances du projet : - -```bash -pnpm install -``` - -Cette commande télécharge et installe toutes les bibliothèques nécessaires définies dans `package.json`. Le processus peut prendre plusieurs minutes selon votre connexion internet. - ---- - -## Étape 3 : Migration de la base de données - -### Application du schéma - -Appliquez le schéma de base de données en exécutant : - -```bash -pnpm db:push -``` - -Cette commande utilise Drizzle Kit pour créer automatiquement toutes les tables nécessaires dans la base de données. Les tables suivantes seront créées : - -| Table | Description | -|-------|-------------| -| `users` | Utilisateurs et administrateurs | -| `formations` | Formations disponibles | -| `sequences` | Séquences de formation | -| `apprenants` | Apprenants inscrits | -| `inscriptions` | Inscriptions aux séquences | -| `emailTemplates` | Templates d'emails personnalisés | -| `alertes` | Alertes et notifications | - -### Vérification du schéma - -Connectez-vous à MySQL et vérifiez que toutes les tables ont été créées correctement : - -```bash -mysql -u formation_user -p formation_manager -``` - -```sql -SHOW TABLES; -DESCRIBE users; -DESCRIBE formations; -``` - ---- - -## Étape 4 : Build de production - -### Compilation de l'application - -Compilez l'application pour la production : - -```bash -pnpm run build -``` - -Cette commande effectue les opérations suivantes : - -1. **Compilation TypeScript** : Transpile le code TypeScript du serveur en JavaScript -2. **Build Vite** : Compile et optimise le frontend React (minification, tree-shaking, code splitting) -3. **Génération des assets** : Crée les fichiers statiques dans `dist/public/` - -Le build de production génère un répertoire `dist/` contenant : - -- `dist/index.js` : Le serveur Express compilé -- `dist/public/` : Les fichiers statiques du frontend (HTML, CSS, JS, images) - -### Vérification du build - -Vérifiez que le build s'est terminé sans erreur et que les fichiers ont été générés : - -```bash -ls -lh dist/ -ls -lh dist/public/ -``` - ---- - -## Étape 5 : Configuration du service systemd - -### Création du fichier service - -Créez un fichier de service systemd pour gérer l'application : - -```bash -sudo nano /etc/systemd/system/formation-manager.service -``` - -Ajoutez le contenu suivant : - -```ini -[Unit] -Description=Formation Manager Itinova -After=network.target mysql.service -Wants=mysql.service - -[Service] -Type=simple -User=formation-manager -Group=formation-manager -WorkingDirectory=/opt/formation-manager/app -Environment="NODE_ENV=production" -Environment="PORT=3000" -EnvironmentFile=/opt/formation-manager/app/.env -ExecStart=/home/formation-manager/.nvm/versions/node/v22.13.0/bin/node dist/index.js -Restart=always -RestartSec=10 -StandardOutput=journal -StandardError=journal -SyslogIdentifier=formation-manager - -# Limites de sécurité -LimitNOFILE=65536 -PrivateTmp=true -NoNewPrivileges=true - -[Install] -WantedBy=multi-user.target -``` - -### Activation du service - -Rechargez systemd, activez et démarrez le service : - -```bash -sudo systemctl daemon-reload -sudo systemctl enable formation-manager -sudo systemctl start formation-manager -``` - -### Vérification du statut - -Vérifiez que le service fonctionne correctement : - -```bash -sudo systemctl status formation-manager -``` - -Vous devriez voir `active (running)` en vert. Consultez les logs en cas d'erreur : - -```bash -sudo journalctl -u formation-manager -f -``` - ---- - -## Étape 6 : Configuration Nginx (reverse proxy) - -### Installation de Nginx - -Si Nginx n'est pas déjà installé : - -```bash -sudo apt update -sudo apt install nginx -``` - -### Configuration du site - -Créez un fichier de configuration pour votre site : - -```bash -sudo nano /etc/nginx/sites-available/formation-manager -``` - -Ajoutez la configuration suivante : - -```nginx -upstream formation_backend { - server 127.0.0.1:3000; - keepalive 64; -} - -server { - listen 80; - server_name votre-domaine.com www.votre-domaine.com; - - # Redirection HTTP vers HTTPS - return 301 https://$server_name$request_uri; -} - -server { - listen 443 ssl http2; - server_name votre-domaine.com www.votre-domaine.com; - - # Certificats SSL (à configurer avec Let's Encrypt) - ssl_certificate /etc/letsencrypt/live/votre-domaine.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/votre-domaine.com/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - - # Logs - access_log /var/log/nginx/formation-manager-access.log; - error_log /var/log/nginx/formation-manager-error.log; - - # Taille maximale des uploads - client_max_body_size 10M; - - # Compression - gzip on; - gzip_vary on; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; - - # Proxy vers Node.js - location / { - proxy_pass http://formation_backend; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - proxy_read_timeout 300s; - proxy_connect_timeout 75s; - } - - # Cache des assets statiques - location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { - proxy_pass http://formation_backend; - expires 1y; - add_header Cache-Control "public, immutable"; - } -} -``` - -Remplacez `votre-domaine.com` par votre nom de domaine réel. - -### Activation du site - -Activez la configuration et redémarrez Nginx : - -```bash -sudo ln -s /etc/nginx/sites-available/formation-manager /etc/nginx/sites-enabled/ -sudo nginx -t # Tester la configuration -sudo systemctl restart nginx -``` - -### Configuration SSL avec Let's Encrypt - -Installez Certbot et obtenez un certificat SSL gratuit : - -```bash -sudo apt install certbot python3-certbot-nginx -sudo certbot --nginx -d votre-domaine.com -d www.votre-domaine.com -``` - -Suivez les instructions à l'écran. Certbot configurera automatiquement Nginx pour utiliser HTTPS. - ---- - -## Étape 7 : Configuration du pare-feu - -### Configuration UFW - -Si vous utilisez UFW (Uncomplicated Firewall), autorisez les ports nécessaires : - -```bash -sudo ufw allow 22/tcp # SSH -sudo ufw allow 80/tcp # HTTP -sudo ufw allow 443/tcp # HTTPS -sudo ufw enable -sudo ufw status -``` - -### Sécurisation SSH (recommandé) - -Désactivez l'authentification par mot de passe SSH et utilisez uniquement les clés : - -```bash -sudo nano /etc/ssh/sshd_config -``` - -Modifiez les lignes suivantes : - -``` -PasswordAuthentication no -PermitRootLogin no -``` - -Redémarrez SSH : - -```bash -sudo systemctl restart sshd -``` - ---- - -## Étape 8 : Vérification du déploiement - -### Tests fonctionnels - -Accédez à votre application via le navigateur à l'adresse `https://votre-domaine.com` et vérifiez les éléments suivants : - -| Test | Description | Résultat attendu | -|------|-------------|------------------| -| Page d'accueil | Chargement de la page d'accueil | Affichage correct sans erreur | -| Connexion | Authentification via Manus OAuth | Redirection vers le tableau de bord | -| Tableau de bord | Affichage des statistiques | Graphiques et cartes visibles | -| Formations | Liste des formations | Tableau avec données | -| Séquences | Liste des séquences | Tableau avec code couleur | -| Apprenants | Liste des apprenants | Tableau avec badges de statut | -| Templates d'emails | Modification d'un template | Éditeur TipTap fonctionnel | - -### Vérification des logs - -Surveillez les logs pour détecter d'éventuelles erreurs : - -```bash -# Logs de l'application -sudo journalctl -u formation-manager -f - -# Logs Nginx -sudo tail -f /var/log/nginx/formation-manager-error.log - -# Logs système -sudo tail -f /var/log/syslog -``` - -### Tests de performance - -Utilisez des outils pour tester les performances de votre application : - -```bash -# Test de charge avec Apache Bench -ab -n 1000 -c 10 https://votre-domaine.com/ - -# Analyse des temps de réponse -curl -w "@curl-format.txt" -o /dev/null -s https://votre-domaine.com/ -``` - ---- - -## Maintenance et mises à jour - -### Sauvegarde de la base de données - -Créez un script de sauvegarde automatique : - -```bash -sudo nano /opt/formation-manager/backup-db.sh -``` - -Contenu du script : - -```bash -#!/bin/bash -DATE=$(date +%Y%m%d_%H%M%S) -BACKUP_DIR="/opt/formation-manager/backups" -mkdir -p $BACKUP_DIR - -mysqldump -u formation_user -p'MOT_DE_PASSE' formation_manager | gzip > $BACKUP_DIR/formation_manager_$DATE.sql.gz - -# Garder seulement les 30 dernières sauvegardes -find $BACKUP_DIR -name "formation_manager_*.sql.gz" -mtime +30 -delete -``` - -Rendez le script exécutable et ajoutez-le au cron : - -```bash -chmod +x /opt/formation-manager/backup-db.sh -sudo crontab -e -``` - -Ajoutez la ligne suivante pour une sauvegarde quotidienne à 2h du matin : - -``` -0 2 * * * /opt/formation-manager/backup-db.sh -``` - -### Mise à jour de l'application - -Pour mettre à jour l'application vers une nouvelle version : - -```bash -cd /opt/formation-manager/app -sudo systemctl stop formation-manager -git pull origin main -pnpm install -pnpm db:push # Appliquer les migrations -pnpm run build -sudo systemctl start formation-manager -``` - -### Surveillance et monitoring - -Installez des outils de monitoring pour surveiller les performances : - -**PM2** : Alternative à systemd avec monitoring intégré -**Prometheus + Grafana** : Monitoring avancé et dashboards -**Uptime Kuma** : Surveillance de disponibilité -**New Relic / DataDog** : Solutions SaaS complètes - ---- - -## Dépannage - -### L'application ne démarre pas - -Vérifiez les logs pour identifier l'erreur : - -```bash -sudo journalctl -u formation-manager -n 100 -``` - -Causes courantes : - -- **Erreur de connexion à la base de données** : Vérifiez `DATABASE_URL` dans `.env` -- **Port déjà utilisé** : Changez le port dans `.env` ou arrêtez le processus conflictuel -- **Permissions insuffisantes** : Vérifiez les droits sur `/opt/formation-manager/app` - -### Erreur 502 Bad Gateway - -Cette erreur indique que Nginx ne peut pas se connecter au backend Node.js. Vérifications : - -```bash -# Vérifier que le service tourne -sudo systemctl status formation-manager - -# Vérifier que le port 3000 écoute -sudo netstat -tulpn | grep 3000 - -# Tester la connexion locale -curl http://localhost:3000 -``` - -### Problèmes de performance - -Si l'application est lente : - -1. **Optimiser les requêtes SQL** : Ajoutez des index sur les colonnes fréquemment interrogées -2. **Activer le cache Redis** : Mettez en cache les résultats des requêtes coûteuses -3. **Augmenter les ressources** : Ajoutez plus de RAM ou de CPU -4. **Activer la compression Nginx** : Déjà configurée dans l'exemple ci-dessus - -### Base de données corrompue - -En cas de corruption de la base de données, restaurez depuis une sauvegarde : - -```bash -gunzip < /opt/formation-manager/backups/formation_manager_YYYYMMDD_HHMMSS.sql.gz | mysql -u formation_user -p formation_manager -``` - ---- - -## Sécurité - -### Bonnes pratiques - -Suivez ces recommandations pour sécuriser votre déploiement : - -**Mettez à jour régulièrement** : Appliquez les mises à jour de sécurité du système d'exploitation et des dépendances Node.js. - -**Utilisez HTTPS uniquement** : Forcez la redirection HTTP vers HTTPS et activez HSTS (HTTP Strict Transport Security). - -**Limitez les accès SSH** : Utilisez des clés SSH au lieu de mots de passe et limitez l'accès par IP si possible. - -**Configurez un WAF** : Utilisez un Web Application Firewall comme ModSecurity ou Cloudflare pour bloquer les attaques courantes. - -**Surveillez les logs** : Configurez des alertes pour détecter les tentatives d'intrusion ou les comportements anormaux. - -**Sauvegardez régulièrement** : Automatisez les sauvegardes de la base de données et testez régulièrement la restauration. - -### Checklist de sécurité - -Avant de mettre en production, vérifiez les points suivants : - -- [ ] Certificat SSL valide configuré -- [ ] Pare-feu activé et configuré -- [ ] Authentification SSH par clé uniquement -- [ ] Variables d'environnement sécurisées (pas de valeurs par défaut) -- [ ] Sauvegardes automatiques configurées -- [ ] Monitoring et alertes en place -- [ ] Logs rotatifs configurés -- [ ] Permissions fichiers correctes (pas de 777) -- [ ] Base de données accessible uniquement en local -- [ ] Rate limiting configuré sur Nginx - ---- - -## Support et contact - -Pour toute question ou problème concernant le déploiement : - -- **Documentation technique** : Consultez le README.md du projet -- **Support Manus** : https://help.manus.im -- **Contact développeur** : o.pareige@itinova.org - ---- - -**Fin du guide de déploiement** diff --git a/deployment-package/GUIDE_DEPLOIEMENT.pdf b/deployment-package/GUIDE_DEPLOIEMENT.pdf deleted file mode 100644 index 8fe3ff5..0000000 Binary files a/deployment-package/GUIDE_DEPLOIEMENT.pdf and /dev/null differ diff --git a/deployment-package/PACKAGE_INFO.txt b/deployment-package/PACKAGE_INFO.txt deleted file mode 100644 index 9716c2c..0000000 --- a/deployment-package/PACKAGE_INFO.txt +++ /dev/null @@ -1,134 +0,0 @@ -================================================================================ -PACKAGE DE DÉPLOIEMENT - FORMATION MANAGER ITINOVA -================================================================================ - -Version: 7a737c05 -Date de création: 24 novembre 2025 -Créé par: Manus AI - -================================================================================ -FICHIERS GÉNÉRÉS -================================================================================ - -📦 Archive complète: - formation-manager-itinova-deployment-7a737c05.tar.gz (738 KB) - - Contient tous les fichiers nécessaires pour le déploiement: - - Guides de déploiement (PDF + Markdown) - - Documentation de configuration (PDF + Markdown) - - Notes de version - - Scripts d'installation et de mise à jour - - README avec instructions de démarrage rapide - -📄 Fichiers individuels disponibles dans deployment-package/: - - GUIDE_DEPLOIEMENT.pdf (425 KB) - Guide complet d'installation - - GUIDE_DEPLOIEMENT.md (19 KB) - Version Markdown du guide - - CONFIGURATION.pdf (376 KB) - Documentation des variables d'environnement - - CONFIGURATION.md (12 KB) - Version Markdown de la configuration - - RELEASE_NOTES.md (9.5 KB) - Notes de version détaillées - - deploy.sh (8.6 KB) - Script d'installation automatisé - - update.sh (2.5 KB) - Script de mise à jour - - README.txt - Instructions de démarrage rapide - -================================================================================ -UTILISATION -================================================================================ - -Option 1: Utiliser l'archive complète --------------------------------------- -1. Téléchargez: formation-manager-itinova-deployment-7a737c05.tar.gz -2. Transférez sur votre serveur: - scp formation-manager-itinova-deployment-7a737c05.tar.gz user@serveur:/home/user/ -3. Décompressez: tar -xzf formation-manager-itinova-deployment-7a737c05.tar.gz -4. Suivez les instructions dans deployment-package/README.txt - -Option 2: Télécharger les fichiers individuels ------------------------------------------------ -Téléchargez uniquement les fichiers dont vous avez besoin depuis le dossier -deployment-package/ - -================================================================================ -PRÉREQUIS SYSTÈME -================================================================================ - -Serveur: -- Ubuntu 22.04 LTS ou supérieur -- 2 CPU cores minimum (4 recommandés) -- 4 GB RAM minimum (8 GB recommandés) -- 20 GB espace disque minimum -- Accès SSH avec droits sudo - -Base de données: -- MySQL 8.0+ ou TiDB Cloud -- 1 GB espace disque minimum pour la base de données - -Réseau: -- Nom de domaine pointant vers le serveur -- Ports ouverts: 80 (HTTP), 443 (HTTPS), 22 (SSH) - -================================================================================ -FONCTIONNALITÉS DE CETTE VERSION -================================================================================ - -✅ Gestion complète des formations, séquences et apprenants -✅ Système d'inscription avec limitation de capacité -✅ Templates d'emails personnalisables avec éditeur WYSIWYG (TipTap) -✅ Variables dynamiques dans les emails -✅ Tableau de bord principal avec statistiques -✅ Tableau de bord analytique avancé avec graphiques (Recharts) -✅ Export Excel des rapports analytiques -✅ Vue calendrier des séquences avec code couleur -✅ Page Établissements avec statistiques de participation -✅ Badges de statut Actif/Inactif pour les apprenants -✅ Code couleur pour taux de remplissage (vert/orange/rouge) -✅ Icônes colorées (œil en bleu, corbeille en rouge) -✅ Menu réorganisé en 4 zones avec titres en bleu -✅ Authentification locale (sans redirection OAuth Manus) - -================================================================================ -ÉTAPES DE DÉPLOIEMENT RÉSUMÉES -================================================================================ - -1. Préparer le serveur (Ubuntu 22.04) -2. Installer les dépendances (Node.js, MySQL, Nginx) -3. Cloner le dépôt Git -4. Configurer les variables d'environnement (.env) -5. Installer les dépendances Node.js (pnpm install) -6. Appliquer les migrations de base de données (pnpm db:push) -7. Compiler l'application (pnpm run build) -8. Configurer Nginx comme reverse proxy -9. Obtenir un certificat SSL avec Let's Encrypt -10. Créer et démarrer le service systemd -11. Tester l'application - -Temps d'installation estimé: 30-45 minutes - -================================================================================ -IDENTIFIANTS PAR DÉFAUT -================================================================================ - -Après l'installation, connectez-vous avec: - -Identifiant: adminServFormation -Mot de passe: Itinova69! - -⚠️ IMPORTANT: Changez ce mot de passe immédiatement après la première connexion! - -================================================================================ -SUPPORT -================================================================================ - -Documentation: Consultez les PDF inclus dans le package -Support Manus: https://help.manus.im -Contact: o.pareige@itinova.org - -================================================================================ -CHECKSUM (pour vérification d'intégrité) -================================================================================ - -Pour vérifier l'intégrité de l'archive après téléchargement: -sha256sum formation-manager-itinova-deployment-7a737c05.tar.gz - -================================================================================ -FIN -================================================================================ diff --git a/deployment-package/README.txt b/deployment-package/README.txt deleted file mode 100644 index ed73673..0000000 --- a/deployment-package/README.txt +++ /dev/null @@ -1,193 +0,0 @@ -================================================================================ - PACKAGE DE DÉPLOIEMENT - FORMATION MANAGER ITINOVA - Version: 7a737c05 - Date: 24 novembre 2025 -================================================================================ - -Ce package contient tous les fichiers nécessaires pour déployer l'application -Formation Manager Itinova sur un serveur VPS de production. - -================================================================================ - CONTENU DU PACKAGE -================================================================================ - -1. GUIDE_DEPLOIEMENT.pdf (425 KB) - Guide complet de déploiement avec instructions pas à pas pour installer - l'application sur un VPS Ubuntu 22.04. Inclut la configuration de la base - de données, Nginx, SSL, et la mise en service systemd. - -2. GUIDE_DEPLOIEMENT.md (19 KB) - Version Markdown du guide de déploiement (même contenu que le PDF). - -3. CONFIGURATION.pdf (376 KB) - Documentation complète de toutes les variables d'environnement et - paramètres de configuration. Explique chaque variable, sa fonction, - et les valeurs recommandées. - -4. CONFIGURATION.md (12 KB) - Version Markdown de la documentation de configuration. - -5. RELEASE_NOTES.md (9.5 KB) - Notes de version détaillées listant toutes les fonctionnalités, - corrections de bugs et améliorations de cette version. - -6. deploy.sh (8.6 KB) - Script bash automatisé pour l'installation initiale complète de - l'application. Installe toutes les dépendances, configure la base - de données, compile l'application et crée le service systemd. - -7. update.sh (2.5 KB) - Script bash pour les mises à jour rapides de l'application. Récupère - les dernières modifications, recompile et redémarre le service. - -8. README.txt (ce fichier) - Instructions de démarrage rapide. - -================================================================================ - DÉMARRAGE RAPIDE -================================================================================ - -ÉTAPE 1 : Prérequis -------------------- -- Serveur VPS Ubuntu 22.04 ou supérieur -- Accès SSH avec droits sudo -- Nom de domaine pointant vers le serveur (pour SSL) -- Base de données MySQL ou TiDB accessible - -ÉTAPE 2 : Transfert des fichiers ---------------------------------- -Transférez le contenu de ce package sur votre serveur : - - scp -r deployment-package/* user@votre-serveur.com:/home/user/ - -ÉTAPE 3 : Lecture de la documentation --------------------------------------- -Avant de commencer, lisez attentivement : - - 1. GUIDE_DEPLOIEMENT.pdf - Pour comprendre le processus complet - 2. CONFIGURATION.pdf - Pour préparer vos variables d'environnement - -ÉTAPE 4 : Exécution du script de déploiement ---------------------------------------------- -Sur le serveur, exécutez le script d'installation : - - chmod +x deploy.sh - ./deploy.sh - -Le script vous guidera à travers toutes les étapes d'installation. - -ÉTAPE 5 : Configuration post-installation ------------------------------------------- -Après l'installation : - - 1. Configurez SMTP dans l'interface web (Configuration > Configuration SMTP) - 2. Créez vos premiers utilisateurs (Gestion > Utilisateurs) - 3. Personnalisez les templates d'emails (Configuration > Templates d'emails) - 4. Testez l'envoi d'emails - -================================================================================ - MISES À JOUR FUTURES -================================================================================ - -Pour mettre à jour l'application vers une version plus récente : - - 1. Transférez le nouveau fichier update.sh sur le serveur - 2. Exécutez : chmod +x update.sh && ./update.sh - 3. Le script sauvegarde automatiquement la base de données avant la mise à jour - -================================================================================ - STRUCTURE DE L'APPLICATION -================================================================================ - -Après installation, l'application sera installée dans : - /opt/formation-manager/ - -Fichiers de configuration : - /opt/formation-manager/.env - -Logs de l'application : - sudo journalctl -u formation-manager -f - -Service systemd : - sudo systemctl status formation-manager - sudo systemctl restart formation-manager - sudo systemctl stop formation-manager - -Configuration Nginx : - /etc/nginx/sites-available/formation-manager - -================================================================================ - IDENTIFIANTS PAR DÉFAUT -================================================================================ - -Après l'installation, connectez-vous avec : - - Identifiant : adminServFormation - Mot de passe : Itinova69! - -⚠️ IMPORTANT : Changez ce mot de passe immédiatement après la première connexion - via le menu Gestion > Utilisateurs > Modifier l'utilisateur adminServFormation - -================================================================================ - SUPPORT ET ASSISTANCE -================================================================================ - -Documentation technique : - - README.md du projet (dans le dépôt Git) - - GUIDE_DEPLOIEMENT.pdf (ce package) - - CONFIGURATION.pdf (ce package) - -Support Manus : - https://help.manus.im - -Contact développeur : - o.pareige@itinova.org - -================================================================================ - SÉCURITÉ -================================================================================ - -Recommandations de sécurité importantes : - - ✓ Changez le mot de passe administrateur par défaut - ✓ Utilisez HTTPS (SSL/TLS) avec Let's Encrypt - ✓ Configurez un pare-feu (ufw) pour limiter les ports ouverts - ✓ Sauvegardez régulièrement la base de données - ✓ Gardez le système à jour (apt update && apt upgrade) - ✓ Utilisez des mots de passe forts pour la base de données - ✓ Ne commitez jamais le fichier .env dans Git - ✓ Limitez les accès SSH (clés SSH uniquement) - -================================================================================ - DÉPANNAGE -================================================================================ - -L'application ne démarre pas : - - Vérifiez les logs : sudo journalctl -u formation-manager -n 100 - - Vérifiez le fichier .env : toutes les variables sont-elles définies ? - - Testez la connexion DB : mysql -h HOST -u USER -p DATABASE - -Les emails ne sont pas envoyés : - - Vérifiez la configuration SMTP dans l'interface - - Utilisez la fonction "Test d'envoi" dans Configuration > Configuration SMTP - - Consultez les logs pour les erreurs SMTP - -Erreur 502 Bad Gateway : - - Le service formation-manager est-il démarré ? sudo systemctl status formation-manager - - Vérifiez les logs Nginx : sudo tail -f /var/log/nginx/error.log - -================================================================================ - LICENCE ET COPYRIGHT -================================================================================ - -Formation Manager Itinova -Version 7a737c05 - -Développé par Manus AI pour Itinova -© 2025 Itinova - Tous droits réservés - -Ce logiciel est fourni "tel quel", sans garantie d'aucune sorte. - -================================================================================ - FIN DU README -================================================================================ diff --git a/deployment-package/RELEASE_NOTES.md b/deployment-package/RELEASE_NOTES.md deleted file mode 100644 index 7f6a8b2..0000000 --- a/deployment-package/RELEASE_NOTES.md +++ /dev/null @@ -1,245 +0,0 @@ -# Notes de version - Formation Manager Itinova - -## Version 7a737c05 (24 novembre 2025) - -### 🎉 Nouvelles fonctionnalités - -#### Gestion des templates d'emails -- **Éditeur de texte riche (TipTap)** : Interface WYSIWYG complète pour formater les emails (gras, italique, titres, listes) -- **Variables dynamiques** : 7 boutons pour insérer rapidement des variables ({{nomApprenant}}, {{nomFormation}}, {{dateDebut}}, etc.) -- **Prévisualisation en temps réel** : Visualisation instantanée du rendu final pendant la modification -- **Bouton "Aperçu email"** : Modal affichant l'email complet avec variables remplacées par des données d'exemple -- **Remplacement automatique** : Les variables sont remplacées par les données réelles lors de l'envoi des emails - -#### Tableau de bord analytique avancé -- **Graphiques d'évolution temporelle** : Visualisation des inscriptions par mois sur 12 mois -- **Taux de remplissage mensuel** : Graphique linéaire montrant l'évolution du taux de remplissage -- **Top 10 établissements** : Graphique à barres des établissements les plus actifs -- **Répartition par fonction** : Graphique circulaire (Directeurs, Chefs de service, Autre) -- **Filtres de période** : Sélection rapide (dernier mois, trimestre, année, toutes les données) -- **Export Excel** : Téléchargement d'un rapport complet avec 5 feuilles détaillées - -#### Gestion des établissements -- **Page dédiée** : Liste complète de tous les établissements avec statistiques -- **Statistiques par établissement** : Nombre d'apprenants, apprenants actifs, taux de participation -- **Code couleur** : Indicateur visuel du taux de participation (vert >70%, orange 40-70%, rouge <40%) -- **Tri et recherche** : Filtrage rapide par nom d'établissement - -#### Badges de statut des apprenants -- **Badge "Actif"** (vert) : Apprenant inscrit à au moins une séquence -- **Badge "Inactif"** (gris) : Apprenant sans inscription active -- **Colonne dédiée** : Nouvelle colonne dans le tableau des apprenants - -### 🎨 Améliorations visuelles - -#### Interface générale -- **Menu réorganisé** : 4 zones distinctes avec séparateurs et titres en bleu - - Zone 1 : Tableau de bord - - Zone 2 : Gestion (Formations, Séquences, Calendrier, Apprenants) - - Zone 3 : Configuration (Utilisateurs, Rappels, Templates d'emails, Configuration SMTP) - - Zone 4 : Analyses (Établissements, Tableau de bord analytique, Rapport public cible) - -#### Gestion des séquences -- **Taux de remplissage coloré** : Vert (<70%), Orange (70-90%), Rouge (>90%) -- **Formations en couleur** : Chaque formation a une couleur unique (10 couleurs disponibles) -- **Icônes colorées** : Œil en bleu (visualisation), Corbeille en rouge (suppression) - -#### Gestion des apprenants -- **Fonction en couleur** : Directeurs (bleu), Chefs de service (vert), Autre (gris) -- **Icônes colorées** : Œil en bleu, Corbeille en rouge -- **Badge de statut** : Actif/Inactif visible immédiatement - -#### Tableau de bord principal -- **Graphique par établissement** : Ajout d'un second graphique à côté de la répartition par fonction -- **Cohérence des données** : Uniformisation des calculs entre tableau de bord principal et analytique - -### 🐛 Corrections de bugs - -- **Erreur calendrier** : Correction de l'erreur React #31 au clic sur un événement (objets Date convertis en string) -- **Menu invisible** : Ajout du wrapper DashboardLayout sur la page Établissements -- **Incohérence statistiques** : Uniformisation du calcul de répartition par fonction (comptage des apprenants uniques) -- **Couleurs formations** : Correction de l'algorithme pour assigner une couleur unique à chaque formation - -### 📚 Documentation - -- **Guide de déploiement complet** : Instructions détaillées pour déployer sur VPS (GUIDE_DEPLOIEMENT.md) -- **Script de déploiement** : Script bash automatisé pour installation initiale (deploy.sh) -- **Script de mise à jour** : Script simplifié pour les mises à jour rapides (update.sh) -- **Documentation de configuration** : Guide complet des variables d'environnement et paramètres (CONFIGURATION.md) - -### 🔧 Améliorations techniques - -- **React Quill remplacé par TipTap** : Compatibilité avec React 19 -- **Recharts intégré** : Bibliothèque de graphiques pour le tableau de bord analytique -- **ExcelJS intégré** : Export Excel des rapports analytiques -- **Requêtes analytiques optimisées** : Nouvelles fonctions dans analyticsDb.ts et etablissementsDb.ts - -### 📊 Statistiques de cette version - -- **Nouvelles pages** : 2 (AdminAnalytics, AdminEtablissements) -- **Nouveaux composants** : 1 (CapacityProgressBar amélioré) -- **Nouvelles fonctions backend** : 15+ (analytics, établissements, templates) -- **Lignes de code ajoutées** : ~3000 -- **Dépendances ajoutées** : 4 (@tiptap/react, @tiptap/starter-kit, recharts, exceljs) - ---- - -## Versions précédentes - -### Version 65a2d003 (23 novembre 2025) - -#### Corrections -- Correction de l'erreur d'affichage au clic sur un événement du calendrier -- Correction du problème de menu invisible sur la page Établissements - -### Version a0049436 (23 novembre 2025) - -#### Nouvelles fonctionnalités -- Badges de statut Actif/Inactif pour les apprenants -- Page Établissements avec statistiques de participation - -### Version 193ffd7e (23 novembre 2025) - -#### Corrections -- Correction de l'algorithme de couleur des formations (mapping basé sur l'ordre) - -### Version 9a38fc9d (23 novembre 2025) - -#### Améliorations visuelles -- Icône corbeille en rouge et icône œil en bleu dans la page des apprenants -- Fonction colorée (Directeurs, Chefs de service) dans le tableau des apprenants - -### Version d2bb185e (23 novembre 2025) - -#### Améliorations visuelles -- Icône œil en bleu dans la gestion des séquences -- Texte des formations coloré avec une couleur unique par formation - -### Version 41fc4874 (23 novembre 2025) - -#### Améliorations visuelles -- Taux de remplissage coloré dans la gestion des séquences (vert/orange/rouge) -- Icône corbeille en rouge - -### Version 3ec648f8 (23 novembre 2025) - -#### Nouvelles fonctionnalités -- Graphique de répartition par établissement sur le tableau de bord principal - -### Version a807183e (23 novembre 2025) - -#### Corrections -- Correction de l'incohérence des statistiques de répartition par fonction - -### Version 03efb59c (23 novembre 2025) - -#### Nouvelles fonctionnalités -- Filtres de période dans le tableau de bord analytique -- Export Excel des rapports analytiques - -### Version 64db3f1a (23 novembre 2025) - -#### Nouvelles fonctionnalités -- Tableau de bord analytique avancé avec graphiques Recharts - -### Version 33161d1a (23 novembre 2025) - -#### Nouvelles fonctionnalités -- Remplacement automatique des variables dans les emails -- Bouton "Aperçu email" avec modal de prévisualisation - -### Version 4e825dbf (23 novembre 2025) - -#### Corrections -- Remplacement de React Quill par TipTap (compatibilité React 19) - -### Version e16bdb22 (23 novembre 2025) - -#### Nouvelles fonctionnalités -- Éditeur WYSIWYG (React Quill) pour les templates d'emails -- Boutons de variables dynamiques - -### Version 3de0e393 (23 novembre 2025) - -#### Améliorations -- Prévisualisation en temps réel du contenu des templates d'emails - -### Version e4abeb5d (23 novembre 2025) - -#### Nouvelles fonctionnalités -- Champ "Corps du message" dans les templates d'emails avec validation backend - -### Version 62644d54 (23 novembre 2025) - -#### Nouvelles fonctionnalités -- Champ "Corps du message" dans l'interface de gestion des templates d'emails - ---- - -## Roadmap future - -### Fonctionnalités planifiées - -#### Court terme (1-2 mois) -- [ ] Système d'alertes automatiques configurables -- [ ] Export PDF des rapports analytiques -- [ ] Vue calendrier des séquences avec code couleur -- [ ] Filtre par formation dans le calendrier -- [ ] Recherche globale dans le header -- [ ] Badges de notification dans le menu - -#### Moyen terme (3-6 mois) -- [ ] Système de tags pour formations et séquences -- [ ] Rapports mensuels automatiques par email -- [ ] Vue détaillée par établissement avec liste des apprenants -- [ ] Statistiques d'emails envoyés -- [ ] Templates d'emails prédéfinis -- [ ] Comparaison de périodes dans les analytics - -#### Long terme (6-12 mois) -- [ ] API REST publique pour intégrations tierces -- [ ] Application mobile (React Native) -- [ ] Système de notifications push -- [ ] Intégration avec calendriers externes (Google Calendar, Outlook) -- [ ] Module de facturation et paiements -- [ ] Gestion multi-organisations - ---- - -## Migration depuis une version antérieure - -### Depuis la version 65a2d003 ou antérieure - -1. Sauvegardez votre base de données -2. Mettez à jour le code : `git pull origin main && git checkout 7a737c05` -3. Installez les nouvelles dépendances : `pnpm install` -4. Appliquez les migrations : `pnpm db:push` -5. Recompilez : `pnpm run build` -6. Redémarrez le service : `sudo systemctl restart formation-manager` - -**Nouvelles tables créées** : `alertes` -**Colonnes ajoutées** : `emailTemplates.bodyContent` - -### Vérification post-migration - -- [ ] Accès à la page Tableau de bord analytique -- [ ] Accès à la page Établissements -- [ ] Modification d'un template d'email avec le nouvel éditeur -- [ ] Export Excel depuis le tableau de bord analytique -- [ ] Vérification des badges de statut dans la liste des apprenants - ---- - -## Support et contact - -Pour toute question concernant cette version : - -- **Documentation** : Consultez GUIDE_DEPLOIEMENT.md et CONFIGURATION.md -- **Support Manus** : https://help.manus.im -- **Contact développeur** : o.pareige@itinova.org - ---- - -**Équipe de développement** -Manus AI - Formation Manager Itinova -© 2025 Itinova diff --git a/deployment-package/deploy.sh b/deployment-package/deploy.sh deleted file mode 100755 index bfa7d9a..0000000 --- a/deployment-package/deploy.sh +++ /dev/null @@ -1,273 +0,0 @@ -#!/bin/bash - -############################################################################### -# Script de déploiement - Gestion des Formations Manager Itinova -# Version: 1.0.0 -# Description: Script automatisé pour déployer l'application en production -############################################################################### - -set -e # Arrêter en cas d'erreur - -# Couleurs pour les messages -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Fonction pour afficher les messages -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Fonction pour vérifier les prérequis -check_prerequisites() { - log_info "Vérification des prérequis..." - - # Vérifier Node.js - if ! command -v node &> /dev/null; then - log_error "Node.js n'est pas installé" - exit 1 - fi - - NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) - if [ "$NODE_VERSION" -lt 18 ]; then - log_error "Node.js version 18+ est requis (version actuelle: $(node -v))" - exit 1 - fi - log_info "Node.js version: $(node -v) ✓" - - # Vérifier pnpm - if ! command -v pnpm &> /dev/null; then - log_warn "pnpm n'est pas installé. Installation..." - npm install -g pnpm - fi - log_info "pnpm version: $(pnpm -v) ✓" - - # Vérifier MySQL - if ! command -v mysql &> /dev/null; then - log_warn "MySQL client n'est pas installé" - else - log_info "MySQL client: ✓" - fi -} - -# Fonction pour créer le fichier .env si nécessaire -setup_env() { - log_info "Configuration des variables d'environnement..." - - if [ ! -f .env ]; then - log_warn "Fichier .env non trouvé. Création à partir du template..." - - cat > .env << 'EOF' -# Base de données -DATABASE_URL=mysql://user:password@localhost:3306/formation_manager - -# JWT Secret (IMPORTANT: Générer une clé aléatoire sécurisée) -JWT_SECRET=changez_cette_cle_secrete_par_une_valeur_aleatoire_tres_longue - -# OAuth Manus -OAUTH_SERVER_URL=https://api.manus.im -VITE_OAUTH_PORTAL_URL=https://login.manus.im -VITE_APP_ID= - -# Propriétaire -OWNER_OPEN_ID= -OWNER_NAME= - -# Application -VITE_APP_TITLE=Gestion des Formations Manager Itinova -VITE_APP_LOGO=/logo.svg - -# APIs Manus (optionnel) -BUILT_IN_FORGE_API_URL=https://forge.manus.im -BUILT_IN_FORGE_API_KEY= -VITE_FRONTEND_FORGE_API_KEY= -VITE_FRONTEND_FORGE_API_URL=https://forge.manus.im - -# Analytics (optionnel) -VITE_ANALYTICS_ENDPOINT= -VITE_ANALYTICS_WEBSITE_ID= - -# Environnement -NODE_ENV=production -EOF - - log_warn "Fichier .env créé. IMPORTANT: Modifiez les valeurs avant de continuer!" - log_warn "Éditez le fichier .env avec vos paramètres de production" - read -p "Appuyez sur Entrée après avoir configuré le fichier .env..." - else - log_info "Fichier .env trouvé ✓" - fi -} - -# Fonction pour installer les dépendances -install_dependencies() { - log_info "Installation des dépendances..." - pnpm install --frozen-lockfile - log_info "Dépendances installées ✓" -} - -# Fonction pour sauvegarder la base de données -backup_database() { - if [ -z "$DATABASE_URL" ]; then - log_warn "DATABASE_URL non définie, sauvegarde de la base de données ignorée" - return - fi - - log_info "Sauvegarde de la base de données..." - - BACKUP_DIR="./backups" - mkdir -p $BACKUP_DIR - - TIMESTAMP=$(date +%Y%m%d_%H%M%S) - BACKUP_FILE="$BACKUP_DIR/db_backup_$TIMESTAMP.sql" - - # Extraire les informations de connexion de DATABASE_URL - # Format: mysql://user:password@host:port/database - DB_USER=$(echo $DATABASE_URL | sed -n 's/.*:\/\/\([^:]*\):.*/\1/p') - DB_PASS=$(echo $DATABASE_URL | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p') - DB_HOST=$(echo $DATABASE_URL | sed -n 's/.*@\([^:]*\):.*/\1/p') - DB_PORT=$(echo $DATABASE_URL | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') - DB_NAME=$(echo $DATABASE_URL | sed -n 's/.*\/\([^?]*\).*/\1/p') - - if command -v mysqldump &> /dev/null; then - mysqldump -h $DB_HOST -P $DB_PORT -u $DB_USER -p$DB_PASS $DB_NAME > $BACKUP_FILE 2>/dev/null || { - log_warn "Impossible de créer la sauvegarde de la base de données" - return - } - - gzip $BACKUP_FILE - log_info "Sauvegarde créée: $BACKUP_FILE.gz ✓" - - # Supprimer les sauvegardes de plus de 30 jours - find $BACKUP_DIR -name "db_backup_*.sql.gz" -mtime +30 -delete - else - log_warn "mysqldump non disponible, sauvegarde ignorée" - fi -} - -# Fonction pour appliquer les migrations -run_migrations() { - log_info "Application des migrations de base de données..." - pnpm db:push - log_info "Migrations appliquées ✓" -} - -# Fonction pour build l'application -build_app() { - log_info "Build de l'application..." - pnpm build - log_info "Build terminé ✓" -} - -# Fonction pour redémarrer le service -restart_service() { - if command -v systemctl &> /dev/null; then - SERVICE_NAME="formation-manager" - - if systemctl is-active --quiet $SERVICE_NAME; then - log_info "Redémarrage du service $SERVICE_NAME..." - sudo systemctl restart $SERVICE_NAME - - # Attendre que le service soit actif - sleep 3 - - if systemctl is-active --quiet $SERVICE_NAME; then - log_info "Service redémarré avec succès ✓" - else - log_error "Échec du redémarrage du service" - sudo systemctl status $SERVICE_NAME - exit 1 - fi - else - log_warn "Le service $SERVICE_NAME n'est pas actif" - log_info "Démarrage du service..." - sudo systemctl start $SERVICE_NAME - fi - else - log_warn "systemctl non disponible. Redémarrage manuel requis." - fi -} - -# Fonction pour vérifier le déploiement -verify_deployment() { - log_info "Vérification du déploiement..." - - # Vérifier que le serveur répond - if command -v curl &> /dev/null; then - sleep 2 - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 || echo "000") - - if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then - log_info "Application accessible ✓" - else - log_warn "L'application ne répond pas correctement (HTTP $HTTP_CODE)" - fi - fi -} - -# Fonction principale -main() { - echo "╔════════════════════════════════════════════════════════════════╗" - echo "║ Déploiement - Gestion des Formations Manager Itinova ║" - echo "╚════════════════════════════════════════════════════════════════╝" - echo "" - - # Vérifier que nous sommes dans le bon répertoire - if [ ! -f "package.json" ]; then - log_error "Ce script doit être exécuté depuis la racine du projet" - exit 1 - fi - - # Étapes de déploiement - check_prerequisites - setup_env - - # Charger les variables d'environnement - if [ -f .env ]; then - export $(cat .env | grep -v '^#' | xargs) - fi - - # Demander confirmation avant de continuer - echo "" - log_warn "Le déploiement va:" - echo " 1. Sauvegarder la base de données" - echo " 2. Installer les dépendances" - echo " 3. Appliquer les migrations" - echo " 4. Builder l'application" - echo " 5. Redémarrer le service" - echo "" - read -p "Continuer? (y/N) " -n 1 -r - echo "" - - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - log_info "Déploiement annulé" - exit 0 - fi - - backup_database - install_dependencies - run_migrations - build_app - restart_service - verify_deployment - - echo "" - echo "╔════════════════════════════════════════════════════════════════╗" - echo "║ Déploiement terminé avec succès! ✓ ║" - echo "╚════════════════════════════════════════════════════════════════╝" - echo "" - log_info "L'application est maintenant accessible" - log_info "Vérifiez les logs avec: sudo journalctl -u formation-manager -f" -} - -# Exécuter le script -main "$@" diff --git a/deployment-package/drizzle/meta/0000_snapshot.json b/deployment-package/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..d0ae91e --- /dev/null +++ b/deployment-package/drizzle/meta/0000_snapshot.json @@ -0,0 +1,2123 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "aa771876-44ab-4786-a5db-87b71c6e5674", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "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": false, + "autoincrement": false + }, + "fonction": { + "name": "fonction", + "type": "varchar(100)", + "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": { + "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 + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inscriptionId": { + "name": "inscriptionId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "urlPdf": { + "name": "urlPdf", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "s3Key": { + "name": "s3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "documentUrl": { + "name": "documentUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "documentS3Key": { + "name": "documentS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uploadedBy": { + "name": "uploadedBy", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uploadedAt": { + "name": "uploadedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailEnvoye": { + "name": "emailEnvoye", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "dateEnvoiEmail": { + "name": "dateEnvoiEmail", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateGeneration": { + "name": "dateGeneration", + "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 + }, + "texteAttestation": { + "name": "texteAttestation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nomSignataire": { + "name": "nomSignataire", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fonctionSignataire": { + "name": "fonctionSignataire", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logoUrl": { + "name": "logoUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logoS3Key": { + "name": "logoS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureUrl": { + "name": "signatureUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureS3Key": { + "name": "signatureS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "ordre": { + "name": "ordre", + "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 + }, + "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": "enum('resend','smtp','simulation')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'simulation'" + }, + "resendApiKey": { + "name": "resendApiKey", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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('tls','ssl','none')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "smtpUser": { + "name": "smtpUser", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "smtpPassword": { + "name": "smtpPassword", + "type": "varchar(255)", + "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 + }, + "mode": { + "name": "mode", + "type": "enum('simulation','production')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'simulation'" + }, + "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": "enum('inscription','teaser','rappel','rappelJ1','rappel3','rappel4','rappel5','rappel6')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "titre": { + "name": "titre", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bodyContent": { + "name": "bodyContent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "couleurPrincipale": { + "name": "couleurPrincipale", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#283581'" + }, + "couleurSecondaire": { + "name": "couleurSecondaire", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#0578BE'" + }, + "piedDePage": { + "name": "piedDePage", + "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": { + "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 + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "dateReponse": { + "name": "dateReponse", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('envoye','repondu')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'envoye'" + } + }, + "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 + }, + "modeAttestation": { + "name": "modeAttestation", + "type": "enum('auto','manuel')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "modeEnvoi": { + "name": "modeEnvoi", + "type": "enum('auto','manuel')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manuel'" + }, + "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": {} + }, + "historiqueAttestations": { + "name": "historiqueAttestations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "statut": { + "name": "statut", + "type": "enum('envoye','erreur')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messageErreur": { + "name": "messageErreur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "urlAttestation": { + "name": "urlAttestation", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "historiqueAttestations_id": { + "name": "historiqueAttestations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "historiqueParametres": { + "name": "historiqueParametres", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userName": { + "name": "userName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "champModifie": { + "name": "champModifie", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ancienneValeur": { + "name": "ancienneValeur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nouvelleValeur": { + "name": "nouvelleValeur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateModification": { + "name": "dateModification", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "historiqueParametres_id": { + "name": "historiqueParametres_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "inscriptions": { + "name": "inscriptions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('confirmee','en_attente','annulee')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'confirmee'" + }, + "dateInscription": { + "name": "dateInscription", + "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('inscription','annulation','capacite_atteinte','place_disponible','remerciement','formateur_inscription','formateur_annulation')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destinataire": { + "name": "destinataire", + "type": "varchar(320)", + "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 + }, + "statut": { + "name": "statut", + "type": "enum('succes','echec')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messageErreur": { + "name": "messageErreur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "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 + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('rappel','rappelJ1','rappel3','rappel4','rappel5','rappel6')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('succes','echec')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messageErreur": { + "name": "messageErreur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nbTentatives": { + "name": "nbTentatives", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "prochainEssai": { + "name": "prochainEssai", + "type": "datetime", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "logsRappels_id": { + "name": "logsRappels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "parametres": { + "name": "parametres", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "urlPublique": { + "name": "urlPublique", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https://formations.itinova.org'" + }, + "delaiExpirationQR": { + "name": "delaiExpirationQR", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "dureeValiditeToken": { + "name": "dureeValiditeToken", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 24 + }, + "notificationsActives": { + "name": "notificationsActives", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "parametres_id": { + "name": "parametres_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 + }, + "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": {} + }, + "presences": { + "name": "presences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "inscriptionId": { + "name": "inscriptionId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateFormationId": { + "name": "dateFormationId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heurePresence": { + "name": "heurePresence", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "modeValidation": { + "name": "modeValidation", + "type": "enum('qrcode','manuel')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validateurId": { + "name": "validateurId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "presences_id": { + "name": "presences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "questionnaires": { + "name": "questionnaires", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "titre": { + "name": "titre", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('satisfaction','evaluation_pre','evaluation_post')", + "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": { + "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 + }, + "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 + }, + "valeurMin": { + "name": "valeurMin", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "valeurMax": { + "name": "valeurMax", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "obligatoire": { + "name": "obligatoire", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 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": { + "questions_id": { + "name": "questions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rappelDates": { + "name": "rappelDates", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rappelId": { + "name": "rappelId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateFormationId": { + "name": "dateFormationId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rappelDates_id": { + "name": "rappelDates_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": "enum('rappel','rappelJ1','rappel3','rappel4','rappel5','rappel6')", + "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'" + }, + "timing": { + "name": "timing", + "type": "enum('pre_formation','post_formation')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pre_formation'" + }, + "nomFichier": { + "name": "nomFichier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "urlFichier": { + "name": "urlFichier", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "s3Key": { + "name": "s3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeFichier": { + "name": "typeFichier", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tailleFichier": { + "name": "tailleFichier", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actif": { + "name": "actif", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "derniereExecution": { + "name": "derniereExecution", + "type": "datetime", + "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 + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateReponse": { + "name": "dateReponse", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "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 + }, + "reponseTexte": { + "name": "reponseTexte", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reponseNumerique": { + "name": "reponseNumerique", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "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(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capaciteMax": { + "name": "capaciteMax", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('ouverte','bloquee','terminee','fermee','annulee')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ouverte'" + }, + "publicCible": { + "name": "publicCible", + "type": "enum('directeur','chef_service','autre','tous')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'tous'" + }, + "formateurId": { + "name": "formateurId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qrCodeToken": { + "name": "qrCodeToken", + "type": "varchar(100)", + "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": { + "sequences_id": { + "name": "sequences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "sequences_qrCodeToken_unique": { + "name": "sequences_qrCodeToken_unique", + "columns": [ + "qrCodeToken" + ] + } + }, + "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(100)", + "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())" + } + }, + "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','formateur')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "formateurId": { + "name": "formateurId", + "type": "int", + "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())" + }, + "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/deployment-package/drizzle/meta/_journal.json b/deployment-package/drizzle/meta/_journal.json new file mode 100644 index 0000000..6f7b99a --- /dev/null +++ b/deployment-package/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1768296151204, + "tag": "0000_clean_justice", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/deployment-package/drizzle/relations.ts b/deployment-package/drizzle/relations.ts new file mode 100644 index 0000000..941a268 --- /dev/null +++ b/deployment-package/drizzle/relations.ts @@ -0,0 +1 @@ +import {} from "./schema"; diff --git a/deployment-package/drizzle/schema.ts b/deployment-package/drizzle/schema.ts new file mode 100644 index 0000000..e0d9b2d --- /dev/null +++ b/deployment-package/drizzle/schema.ts @@ -0,0 +1,549 @@ +import { boolean, int, mysqlEnum, mysqlTable, text, timestamp, varchar, datetime, decimal } from "drizzle-orm/mysql-core"; + +/** + * Core user table backing auth flow. + * Extend this file with additional tables as your product grows. + * Columns use camelCase to match both database fields and generated types. + */ +export const users = mysqlTable("users", { + /** + * Surrogate primary key. Auto-incremented numeric value managed by the database. + * Use this for relations between tables. + */ + id: int("id").autoincrement().primaryKey(), + /** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */ + openId: varchar("openId", { length: 64 }).notNull().unique(), + name: text("name"), + email: varchar("email", { length: 320 }), + /** Identifiant de connexion unique pour l'utilisateur */ + username: varchar("username", { length: 100 }).unique(), + /** Mot de passe hashé (bcrypt) */ + password: varchar("password", { length: 255 }), + loginMethod: varchar("loginMethod", { length: 64 }), + role: mysqlEnum("role", ["user", "admin", "formateur"]).default("user").notNull(), + /** Statut du compte (actif/inactif) */ + isActive: boolean("isActive").default(true).notNull(), + /** ID du formateur associé (si role = formateur) */ + formateurId: int("formateurId"), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), + lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(), +}); + +export type User = typeof users.$inferSelect; +export type InsertUser = typeof users.$inferInsert; + +/** + * Table des tokens de réinitialisation de mot de passe + */ +export const passwordResetTokens = mysqlTable("passwordResetTokens", { + id: int("id").autoincrement().primaryKey(), + userId: int("userId").notNull(), + token: varchar("token", { length: 255 }).notNull().unique(), + expiresAt: timestamp("expiresAt").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); + +export type PasswordResetToken = typeof passwordResetTokens.$inferSelect; +export type InsertPasswordResetToken = typeof passwordResetTokens.$inferInsert; + +/** + * Table des formations (ex: Manager Itinova) + */ +export const formations = mysqlTable("formations", { + id: int("id").autoincrement().primaryKey(), + nom: varchar("nom", { length: 255 }).notNull(), + description: text("description"), + /** Lien unique pour l'inscription à cette formation */ + lienUnique: varchar("lienUnique", { length: 100 }).notNull().unique(), + actif: boolean("actif").default(true).notNull(), + /** Mode de génération des attestations (auto = génération automatique, manuel = import de documents) */ + modeAttestation: mysqlEnum("modeAttestation", ["auto", "manuel"]).default("auto").notNull(), + /** Mode d'envoi des attestations (auto = envoi automatique, manuel = envoi manuel) */ + modeEnvoi: mysqlEnum("modeEnvoi", ["auto", "manuel"]).default("manuel").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type Formation = typeof formations.$inferSelect; +export type InsertFormation = typeof formations.$inferInsert; + +/** + * Table des séquences de formation (ex: Groupe A, Groupe B) + * Chaque séquence peut avoir jusqu'à 4 dates de formation + */ +export const sequences = mysqlTable("sequences", { + id: int("id").autoincrement().primaryKey(), + formationId: int("formationId").notNull(), + nom: varchar("nom", { length: 255 }).notNull(), + lieu: varchar("lieu", { length: 255 }).notNull(), + capaciteMax: int("capaciteMax").notNull(), + statut: mysqlEnum("statut", ["ouverte", "bloquee", "terminee", "fermee", "annulee"]).default("ouverte").notNull(), + /** Public cible de la séquence (Directeur, Chef de service, Autre, Tous) */ + publicCible: mysqlEnum("publicCible", ["directeur", "chef_service", "autre", "tous"]).default("tous").notNull(), + /** ID du formateur assigné à cette séquence */ + formateurId: int("formateurId"), + /** Token unique pour le QR code d'émargement */ + qrCodeToken: varchar("qrCodeToken", { length: 100 }).unique(), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type Sequence = typeof sequences.$inferSelect; +export type InsertSequence = typeof sequences.$inferInsert; + +/** + * Table des dates de formation pour chaque séquence + * Une séquence peut avoir jusqu'à 4 dates + */ +export const datesFormation = mysqlTable("datesFormation", { + id: int("id").autoincrement().primaryKey(), + sequenceId: int("sequenceId").notNull(), + /** Ordre de la date (1, 2, 3 ou 4) */ + ordre: int("ordre").notNull(), + dateDebut: datetime("dateDebut").notNull(), + dateFin: datetime("dateFin").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); + +export type DateFormation = typeof datesFormation.$inferSelect; +export type InsertDateFormation = typeof datesFormation.$inferInsert; + +/** + * Table des apprenants + */ +export const apprenants = mysqlTable("apprenants", { + id: int("id").autoincrement().primaryKey(), + nom: varchar("nom", { length: 255 }).notNull(), + prenom: varchar("prenom", { length: 255 }).notNull(), + email: varchar("email", { length: 320 }).notNull().unique(), + codeEtablissement: varchar("codeEtablissement", { length: 50 }), + /** Fonction de l'apprenant (Directeur, Chef de service, Autre) */ + fonction: varchar("fonction", { length: 100 }), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type Apprenant = typeof apprenants.$inferSelect; +export type InsertApprenant = typeof apprenants.$inferInsert; + +/** + * Table des inscriptions + */ +export const inscriptions = mysqlTable("inscriptions", { + id: int("id").autoincrement().primaryKey(), + sequenceId: int("sequenceId").notNull(), + apprenantId: int("apprenantId").notNull(), + statut: mysqlEnum("statut", ["confirmee", "en_attente", "annulee"]).default("confirmee").notNull(), + dateInscription: timestamp("dateInscription").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type Inscription = typeof inscriptions.$inferSelect; +export type InsertInscription = typeof inscriptions.$inferInsert; + +/** + * Table de configuration des emails + */ +export const emailConfig = mysqlTable("emailConfig", { + id: int("id").autoincrement().primaryKey(), + /** Provider d'envoi d'emails (resend, smtp, simulation) */ + provider: mysqlEnum("provider", ["resend", "smtp", "simulation"]).default("simulation").notNull(), + /** Clé API Resend */ + resendApiKey: varchar("resendApiKey", { length: 255 }), + /** Hôte SMTP */ + smtpHost: varchar("smtpHost", { length: 255 }), + /** Port SMTP */ + smtpPort: int("smtpPort"), + /** Sécurité SMTP (TLS, SSL, None) */ + smtpSecure: mysqlEnum("smtpSecure", ["tls", "ssl", "none"]), + /** Utilisateur SMTP */ + smtpUser: varchar("smtpUser", { length: 255 }), + /** Mot de passe SMTP */ + smtpPassword: varchar("smtpPassword", { length: 255 }), + /** Email expéditeur */ + fromEmail: varchar("fromEmail", { length: 320 }).notNull(), + /** Nom de l'expéditeur */ + fromName: varchar("fromName", { length: 255 }).notNull(), + /** Mode de fonctionnement (simulation ou production) */ + mode: mysqlEnum("mode", ["simulation", "production"]).default("simulation").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type EmailConfig = typeof emailConfig.$inferSelect; +export type InsertEmailConfig = typeof emailConfig.$inferInsert; + +/** + * Table des templates d'emails + */ +export const emailTemplates = mysqlTable("emailTemplates", { + id: int("id").autoincrement().primaryKey(), + /** Type de template (inscription, teaser, rappel, rappelJ1, rappel3, rappel4, rappel5, rappel6) */ + type: mysqlEnum("type", ["inscription", "teaser", "rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull().unique(), + titre: varchar("titre", { length: 255 }).notNull(), + /** Corps du message avec variables dynamiques */ + bodyContent: text("bodyContent").notNull(), + couleurPrincipale: varchar("couleurPrincipale", { length: 7 }).default("#283581").notNull(), + couleurSecondaire: varchar("couleurSecondaire", { length: 7 }).default("#0578BE").notNull(), + piedDePage: text("piedDePage"), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type EmailTemplate = typeof emailTemplates.$inferSelect; +export type InsertEmailTemplate = typeof emailTemplates.$inferInsert; + +/** + * Table des formateurs + */ +export const formateurs = mysqlTable("formateurs", { + id: int("id").autoincrement().primaryKey(), + nom: varchar("nom", { length: 255 }).notNull(), + email: varchar("email", { length: 320 }), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type Formateur = typeof formateurs.$inferSelect; +export type InsertFormateur = typeof formateurs.$inferInsert; + +/** + * Table des rappels automatiques + */ +export const rappels = mysqlTable("rappels", { + id: int("id").autoincrement().primaryKey(), + nom: varchar("nom", { length: 255 }).notNull(), + /** Type de template d'email à utiliser (rappel, rappelJ1, rappel3, rappel4, rappel5, rappel6) */ + templateType: mysqlEnum("templateType", ["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull(), + /** Nombre de jours avant la séquence */ + joursAvant: int("joursAvant").notNull(), + /** Heure d'envoi (format HH:mm) */ + heureEnvoi: varchar("heureEnvoi", { length: 5 }).default("09:00").notNull(), + /** Timing du rappel (pre_formation ou post_formation) */ + timing: mysqlEnum("timing", ["pre_formation", "post_formation"]).default("pre_formation").notNull(), + /** Nom du fichier joint (optionnel) */ + nomFichier: varchar("nomFichier", { length: 255 }), + /** URL du fichier sur S3 (optionnel) */ + urlFichier: varchar("urlFichier", { length: 500 }), + /** Clé S3 du fichier (optionnel) */ + s3Key: varchar("s3Key", { length: 500 }), + /** Type MIME du fichier (optionnel) */ + typeFichier: varchar("typeFichier", { length: 100 }), + /** Taille du fichier en octets (optionnel) */ + tailleFichier: int("tailleFichier"), + actif: boolean("actif").default(true).notNull(), + derniereExecution: datetime("derniereExecution"), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type Rappel = typeof rappels.$inferSelect; +export type InsertRappel = typeof rappels.$inferInsert; + +/** + * Table de liaison entre rappels et dates de formation + * Si aucune date n'est associée, le rappel s'applique à toutes les dates + */ +export const rappelDates = mysqlTable("rappelDates", { + id: int("id").autoincrement().primaryKey(), + rappelId: int("rappelId").notNull(), + dateFormationId: int("dateFormationId").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); + +export type RappelDate = typeof rappelDates.$inferSelect; +export type InsertRappelDate = typeof rappelDates.$inferInsert; + +/** + * Table des logs de rappels envoyés + */ +export const logsRappels = mysqlTable("logsRappels", { + id: int("id").autoincrement().primaryKey(), + rappelId: int("rappelId").notNull(), + sequenceId: int("sequenceId").notNull(), + apprenantId: int("apprenantId").notNull(), + email: varchar("email", { length: 320 }).notNull(), + /** Type de rappel (rappel, rappelJ1, rappel3, rappel4, rappel5, rappel6) */ + type: mysqlEnum("type", ["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).notNull(), + statut: mysqlEnum("statut", ["succes", "echec"]).notNull(), + messageErreur: text("messageErreur"), + /** Nombre de tentatives d'envoi */ + nbTentatives: int("nbTentatives").default(1).notNull(), + /** Date du prochain essai en cas d'échec */ + prochainEssai: datetime("prochainEssai"), + dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(), +}); + +export type LogRappel = typeof logsRappels.$inferSelect; +export type InsertLogRappel = typeof logsRappels.$inferInsert; + +/** + * Table des questionnaires de satisfaction et d'évaluation + */ +export const questionnaires = mysqlTable("questionnaires", { + id: int("id").autoincrement().primaryKey(), + titre: varchar("titre", { length: 255 }).notNull(), + description: text("description"), + /** Type de questionnaire (satisfaction, evaluation_pre, evaluation_post) */ + type: mysqlEnum("type", ["satisfaction", "evaluation_pre", "evaluation_post"]).notNull(), + actif: boolean("actif").default(true).notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type Questionnaire = typeof questionnaires.$inferSelect; +export type InsertQuestionnaire = typeof questionnaires.$inferInsert; + +/** + * Table des questions d'un questionnaire + */ +export const questions = mysqlTable("questions", { + id: int("id").autoincrement().primaryKey(), + questionnaireId: int("questionnaireId").notNull(), + texte: text("texte").notNull(), + /** Type de question (choix_multiple, echelle, texte_libre, oui_non) */ + typeQuestion: mysqlEnum("typeQuestion", ["choix_multiple", "echelle", "texte_libre", "oui_non"]).notNull(), + /** Options pour les questions à choix multiples (JSON) */ + options: text("options"), + /** Valeur minimale pour les échelles */ + valeurMin: int("valeurMin"), + /** Valeur maximale pour les échelles */ + valeurMax: int("valeurMax"), + obligatoire: boolean("obligatoire").default(false).notNull(), + ordre: int("ordre").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); + +export type Question = typeof questions.$inferSelect; +export type InsertQuestion = typeof questions.$inferInsert; + +/** + * Table des réponses aux questionnaires + */ +export const reponsesQuestionnaires = mysqlTable("reponsesQuestionnaires", { + id: int("id").autoincrement().primaryKey(), + questionnaireId: int("questionnaireId").notNull(), + sequenceId: int("sequenceId").notNull(), + apprenantId: int("apprenantId").notNull(), + dateReponse: timestamp("dateReponse").defaultNow().notNull(), +}); + +export type ReponseQuestionnaire = typeof reponsesQuestionnaires.$inferSelect; +export type InsertReponseQuestionnaire = typeof reponsesQuestionnaires.$inferInsert; + +/** + * Table des réponses individuelles aux questions + */ +export const reponsesQuestions = mysqlTable("reponsesQuestions", { + id: int("id").autoincrement().primaryKey(), + reponseQuestionnaireId: int("reponseQuestionnaireId").notNull(), + questionId: int("questionId").notNull(), + /** Réponse textuelle */ + reponseTexte: text("reponseTexte"), + /** Réponse numérique (pour échelles et oui/non) */ + reponseNumerique: int("reponseNumerique"), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); + +export type ReponseQuestion = typeof reponsesQuestions.$inferSelect; +export type InsertReponseQuestion = typeof reponsesQuestions.$inferInsert; + +/** + * Table des envois de questionnaires + */ +export const envoisQuestionnaires = mysqlTable("envoisQuestionnaires", { + id: int("id").autoincrement().primaryKey(), + questionnaireId: int("questionnaireId").notNull(), + sequenceId: int("sequenceId").notNull(), + apprenantId: int("apprenantId").notNull(), + /** Token unique pour accéder au questionnaire */ + token: varchar("token", { length: 100 }).notNull().unique(), + dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(), + dateReponse: timestamp("dateReponse"), + statut: mysqlEnum("statut", ["envoye", "repondu"]).default("envoye").notNull(), +}); + +export type EnvoiQuestionnaire = typeof envoisQuestionnaires.$inferSelect; +export type InsertEnvoiQuestionnaire = typeof envoisQuestionnaires.$inferInsert; + +/** + * Table des supports de formation uploadés par les formateurs + */ +export const supportsFormation = mysqlTable("supportsFormation", { + id: int("id").autoincrement().primaryKey(), + sequenceId: int("sequenceId").notNull(), + formateurId: int("formateurId").notNull(), + nomFichier: varchar("nomFichier", { length: 255 }).notNull(), + typeFichier: varchar("typeFichier", { length: 100 }).notNull(), + tailleFichier: int("tailleFichier").notNull(), + urlFichier: varchar("urlFichier", { length: 500 }).notNull(), + s3Key: varchar("s3Key", { length: 500 }).notNull(), + description: text("description"), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); + +export type SupportFormation = typeof supportsFormation.$inferSelect; +export type InsertSupportFormation = typeof supportsFormation.$inferInsert; + +/** + * Table des présences (émargement) + */ +export const presences = mysqlTable("presences", { + id: int("id").autoincrement().primaryKey(), + inscriptionId: int("inscriptionId").notNull(), + dateFormationId: int("dateFormationId").notNull(), + heurePresence: timestamp("heurePresence").defaultNow().notNull(), + /** Mode de validation (qrcode, manuel) */ + modeValidation: mysqlEnum("modeValidation", ["qrcode", "manuel"]).notNull(), + /** ID de l'utilisateur qui a validé (formateur ou admin) */ + validateurId: int("validateurId"), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); + +export type Presence = typeof presences.$inferSelect; +export type InsertPresence = typeof presences.$inferInsert; + +/** + * Table des attestations de formation + */ +export const attestations = mysqlTable("attestations", { + id: int("id").autoincrement().primaryKey(), + apprenantId: int("apprenantId").notNull(), + sequenceId: int("sequenceId").notNull(), + inscriptionId: int("inscriptionId").notNull(), + urlPdf: varchar("urlPdf", { length: 500 }), + s3Key: varchar("s3Key", { length: 500 }), + /** URL du document uploadé manuellement */ + documentUrl: varchar("documentUrl", { length: 500 }), + /** Clé S3 du document uploadé manuellement */ + documentS3Key: varchar("documentS3Key", { length: 500 }), + /** ID de l'utilisateur qui a uploadé le document */ + uploadedBy: int("uploadedBy"), + /** Date d'upload du document */ + uploadedAt: timestamp("uploadedAt"), + /** Email envoyé ou non */ + emailEnvoye: boolean("emailEnvoye").default(false).notNull(), + /** Date d'envoi de l'email */ + dateEnvoiEmail: timestamp("dateEnvoiEmail"), + dateGeneration: timestamp("dateGeneration").defaultNow().notNull(), +}); + +export type Attestation = typeof attestations.$inferSelect; +export type InsertAttestation = typeof attestations.$inferInsert; + +/** + * Table de configuration des attestations + */ +export const configAttestation = mysqlTable("configAttestation", { + id: int("id").autoincrement().primaryKey(), + /** Texte personnalisable de l'attestation avec variables dynamiques */ + texteAttestation: text("texteAttestation").notNull(), + /** Nom du signataire */ + nomSignataire: varchar("nomSignataire", { length: 255 }).notNull(), + /** Fonction du signataire */ + fonctionSignataire: varchar("fonctionSignataire", { length: 255 }).notNull(), + /** URL du logo sur S3 */ + logoUrl: varchar("logoUrl", { length: 500 }), + /** Clé S3 du logo */ + logoS3Key: varchar("logoS3Key", { length: 500 }), + /** URL de la signature sur S3 */ + signatureUrl: varchar("signatureUrl", { length: 500 }), + /** Clé S3 de la signature */ + signatureS3Key: varchar("signatureS3Key", { length: 500 }), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type ConfigAttestation = typeof configAttestation.$inferSelect; +export type InsertConfigAttestation = typeof configAttestation.$inferInsert; + +/** + * Table des notifications envoyées + */ +export const logsNotifications = mysqlTable("logsNotifications", { + id: int("id").autoincrement().primaryKey(), + /** Type de notification */ + type: mysqlEnum("type", [ + "inscription", + "annulation", + "capacite_atteinte", + "place_disponible", + "remerciement", + "formateur_inscription", + "formateur_annulation" + ]).notNull(), + /** Email du destinataire */ + destinataire: varchar("destinataire", { length: 320 }).notNull(), + /** ID de la séquence concernée */ + sequenceId: int("sequenceId"), + /** ID de l'apprenant concerné */ + apprenantId: int("apprenantId"), + /** Statut de l'envoi */ + statut: mysqlEnum("statut", ["succes", "echec"]).notNull(), + /** Message d'erreur en cas d'échec */ + messageErreur: text("messageErreur"), + dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(), +}); + +export type LogNotification = typeof logsNotifications.$inferSelect; +export type InsertLogNotification = typeof logsNotifications.$inferInsert; + +/** + * Table des paramètres de l'application + * Stocke les paramètres globaux de configuration + */ +export const parametres = mysqlTable("parametres", { + id: int("id").autoincrement().primaryKey(), + /** URL publique de l'application pour les QR codes d'émargement */ + urlPublique: varchar("urlPublique", { length: 500 }).notNull().default("https://formations.itinova.org"), + /** Délai d'expiration des QR codes en minutes (par défaut 30 minutes) */ + delaiExpirationQR: int("delaiExpirationQR").default(30).notNull(), + /** Durée de validité des tokens d'émargement en heures (par défaut 24 heures) */ + dureeValiditeToken: int("dureeValiditeToken").default(24).notNull(), + /** Activer ou désactiver les notifications automatiques */ + notificationsActives: boolean("notificationsActives").default(true).notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type Parametre = typeof parametres.$inferSelect; +export type InsertParametre = typeof parametres.$inferInsert; + +/** + * Table de l'historique des modifications des paramètres + * Permet de tracer qui a modifié quoi et quand + */ +export const historiqueParametres = mysqlTable("historiqueParametres", { + id: int("id").autoincrement().primaryKey(), + /** ID de l'utilisateur qui a effectué la modification */ + userId: int("userId").notNull(), + /** Nom de l'utilisateur (pour historique même si user supprimé) */ + userName: varchar("userName", { length: 255 }).notNull(), + /** Champ modifié */ + champModifie: varchar("champModifie", { length: 100 }).notNull(), + /** Ancienne valeur (JSON) */ + ancienneValeur: text("ancienneValeur"), + /** Nouvelle valeur (JSON) */ + nouvelleValeur: text("nouvelleValeur"), + dateModification: timestamp("dateModification").defaultNow().notNull(), +}); + +export type HistoriqueParametre = typeof historiqueParametres.$inferSelect; +export type InsertHistoriqueParametre = typeof historiqueParametres.$inferInsert; + +/** + * Table de l'historique des envois d'attestations de formation + * Permet de tracer tous les envois d'attestations + */ +export const historiqueAttestations = mysqlTable("historiqueAttestations", { + id: int("id").autoincrement().primaryKey(), + sequenceId: int("sequenceId").notNull(), + apprenantId: int("apprenantId").notNull(), + dateEnvoi: timestamp("dateEnvoi").defaultNow().notNull(), + statut: mysqlEnum("statut", ["envoye", "erreur"]).notNull(), + messageErreur: text("messageErreur"), + urlAttestation: varchar("urlAttestation", { length: 500 }), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); + +export type HistoriqueAttestation = typeof historiqueAttestations.$inferSelect; +export type InsertHistoriqueAttestation = typeof historiqueAttestations.$inferInsert; diff --git a/deployment-package/package.json b/deployment-package/package.json new file mode 100644 index 0000000..6ed978c --- /dev/null +++ b/deployment-package/package.json @@ -0,0 +1,136 @@ +{ + "name": "formation-manager-itinova", + "version": "1.0.0", + "type": "module", + "license": "MIT", + "scripts": { + "dev": "NODE_ENV=development tsx watch server/_core/index.ts", + "build": "vite build && esbuild server/_core/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist", + "start": "NODE_ENV=production node dist/index.js", + "check": "tsc --noEmit", + "format": "prettier --write .", + "test": "vitest run", + "db:push": "drizzle-kit generate && drizzle-kit migrate" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.693.0", + "@aws-sdk/s3-request-presigner": "^3.693.0", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.90.2", + "@tiptap/extension-color": "^3.11.0", + "@tiptap/extension-link": "^3.11.0", + "@tiptap/extension-text-style": "^3.11.0", + "@tiptap/react": "^3.11.0", + "@tiptap/starter-kit": "^3.11.0", + "@trpc/client": "^11.6.0", + "@trpc/react-query": "^11.6.0", + "@trpc/server": "^11.6.0", + "@types/multer": "^2.0.0", + "@types/pdfkit": "^0.17.4", + "axios": "^1.12.0", + "bcryptjs": "^3.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "cookie": "^1.0.2", + "date-fns": "^4.1.0", + "dotenv": "^17.2.2", + "drizzle-orm": "^0.44.5", + "embla-carousel-react": "^8.6.0", + "exceljs": "^4.4.0", + "express": "^4.21.2", + "framer-motion": "^12.23.22", + "input-otp": "^1.4.2", + "jose": "6.1.0", + "jsonwebtoken": "^9.0.2", + "jspdf": "^3.0.3", + "jspdf-autotable": "^5.0.2", + "lucide-react": "^0.453.0", + "multer": "^2.0.2", + "mysql2": "^3.15.0", + "nanoid": "^5.1.5", + "next-themes": "^0.4.6", + "nodemailer": "^7.0.11", + "openai": "^4.67.0", + "pdfkit": "^0.17.2", + "qrcode": "^1.5.4", + "react": "^19.1.1", + "react-day-picker": "^9.11.1", + "react-dom": "^19.1.1", + "react-hook-form": "^7.64.0", + "react-resizable-panels": "^3.0.6", + "recharts": "^2.15.4", + "sonner": "^2.0.7", + "streamdown": "^1.4.0", + "superjson": "^1.13.3", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", + "wouter": "^3.3.5", + "xlsx": "^0.18.5", + "zod": "^4.1.12" + }, + "devDependencies": { + "@builder.io/vite-plugin-jsx-loc": "^0.1.1", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "^4.1.3", + "@types/bcryptjs": "^3.0.0", + "@types/express": "4.17.21", + "@types/google.maps": "^3.58.1", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^24.7.0", + "@types/nodemailer": "^7.0.4", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "add": "^2.0.6", + "autoprefixer": "^10.4.20", + "drizzle-kit": "^0.31.4", + "esbuild": "^0.25.0", + "pnpm": "^10.15.1", + "postcss": "^8.4.47", + "prettier": "^3.6.2", + "tailwindcss": "^4.1.14", + "tsx": "^4.19.1", + "tw-animate-css": "^1.4.0", + "typescript": "5.9.3", + "vite": "^7.1.7", + "vite-plugin-manus-runtime": "^0.0.56", + "vitest": "^2.1.4" + }, + "packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af", + "pnpm": { + "patchedDependencies": { + "wouter@3.7.1": "patches/wouter@3.7.1.patch" + }, + "overrides": { + "tailwindcss>nanoid": "3.3.7" + } + } +} \ No newline at end of file diff --git a/deployment-package/pnpm-lock.yaml b/deployment-package/pnpm-lock.yaml new file mode 100644 index 0000000..5bf8408 --- /dev/null +++ b/deployment-package/pnpm-lock.yaml @@ -0,0 +1,11959 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + tailwindcss>nanoid: 3.3.7 + +patchedDependencies: + wouter@3.7.1: + hash: 4e16e6ff3fde7d6c1024d3e0c8605dc9eb6afb690d0d49958c2f449091813072 + path: patches/wouter@3.7.1.patch + +importers: + + .: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.693.0 + version: 3.907.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.693.0 + version: 3.907.0 + '@radix-ui/react-accordion': + specifier: ^1.2.12 + version: 1.2.12(@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) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.15 + version: 1.1.15(@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) + '@radix-ui/react-aspect-ratio': + specifier: ^1.1.7 + version: 1.1.7(@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) + '@radix-ui/react-avatar': + specifier: ^1.1.10 + version: 1.1.10(@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) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@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) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.12(@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) + '@radix-ui/react-context-menu': + specifier: ^2.2.16 + version: 2.2.16(@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) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@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) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.16(@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) + '@radix-ui/react-hover-card': + specifier: ^1.1.15 + version: 1.1.15(@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) + '@radix-ui/react-label': + specifier: ^2.1.7 + version: 2.1.7(@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) + '@radix-ui/react-menubar': + specifier: ^1.1.16 + version: 1.1.16(@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) + '@radix-ui/react-navigation-menu': + specifier: ^1.2.14 + version: 1.2.14(@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) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@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) + '@radix-ui/react-progress': + specifier: ^1.1.7 + version: 1.1.7(@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) + '@radix-ui/react-radio-group': + specifier: ^1.3.8 + version: 1.3.8(@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) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@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) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@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) + '@radix-ui/react-separator': + specifier: ^1.1.7 + version: 1.1.7(@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) + '@radix-ui/react-slider': + specifier: ^1.3.6 + version: 1.3.6(@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) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@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) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@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) + '@radix-ui/react-toggle': + specifier: ^1.1.10 + version: 1.1.10(@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) + '@radix-ui/react-toggle-group': + specifier: ^1.1.11 + version: 1.1.11(@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) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@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) + '@tanstack/react-query': + specifier: ^5.90.2 + version: 5.90.2(react@19.2.0) + '@tiptap/extension-color': + specifier: ^3.11.0 + version: 3.11.0(@tiptap/extension-text-style@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))) + '@tiptap/extension-link': + specifier: ^3.11.0 + version: 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-text-style': + specifier: ^3.11.0 + version: 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/react': + specifier: ^3.11.0 + version: 3.11.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)(@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) + '@tiptap/starter-kit': + specifier: ^3.11.0 + version: 3.11.0 + '@trpc/client': + specifier: ^11.6.0 + version: 11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3) + '@trpc/react-query': + specifier: ^11.6.0 + version: 11.6.0(@tanstack/react-query@5.90.2(react@19.2.0))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + '@trpc/server': + specifier: ^11.6.0 + version: 11.6.0(typescript@5.9.3) + '@types/multer': + specifier: ^2.0.0 + version: 2.0.0 + '@types/pdfkit': + specifier: ^0.17.4 + version: 0.17.4 + axios: + specifier: ^1.12.0 + version: 1.12.2 + bcryptjs: + specifier: ^3.0.3 + version: 3.0.3 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 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) + cookie: + specifier: ^1.0.2 + version: 1.0.2 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + dotenv: + specifier: ^17.2.2 + version: 17.2.3 + drizzle-orm: + specifier: ^0.44.5 + version: 0.44.6(mysql2@3.15.1) + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@19.2.0) + exceljs: + specifier: ^4.4.0 + version: 4.4.0 + express: + specifier: ^4.21.2 + version: 4.21.2 + framer-motion: + specifier: ^12.23.22 + version: 12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + input-otp: + specifier: ^1.4.2 + version: 1.4.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + jose: + specifier: 6.1.0 + version: 6.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + jspdf: + specifier: ^3.0.3 + version: 3.0.3 + jspdf-autotable: + specifier: ^5.0.2 + version: 5.0.2(jspdf@3.0.3) + lucide-react: + specifier: ^0.453.0 + version: 0.453.0(react@19.2.0) + multer: + specifier: ^2.0.2 + version: 2.0.2 + mysql2: + specifier: ^3.15.0 + version: 3.15.1 + nanoid: + specifier: ^5.1.5 + version: 5.1.6 + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + nodemailer: + specifier: ^7.0.11 + version: 7.0.11 + openai: + specifier: ^4.67.0 + version: 4.104.0(zod@4.1.12) + pdfkit: + specifier: ^0.17.2 + version: 0.17.2 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + react: + specifier: ^19.1.1 + version: 19.2.0 + react-day-picker: + specifier: ^9.11.1 + version: 9.11.1(react@19.2.0) + react-dom: + specifier: ^19.1.1 + version: 19.2.0(react@19.2.0) + react-hook-form: + specifier: ^7.64.0 + version: 7.64.0(react@19.2.0) + react-resizable-panels: + specifier: ^3.0.6 + version: 3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + recharts: + specifier: ^2.15.4 + version: 2.15.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + streamdown: + specifier: ^1.4.0 + version: 1.4.0(@types/react@19.2.2)(react@19.2.0) + superjson: + specifier: ^1.13.3 + version: 1.13.3 + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@4.1.14) + vaul: + specifier: ^1.1.2 + version: 1.1.2(@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) + wouter: + specifier: ^3.3.5 + version: 3.7.1(patch_hash=4e16e6ff3fde7d6c1024d3e0c8605dc9eb6afb690d0d49958c2f449091813072)(react@19.2.0) + xlsx: + specifier: ^0.18.5 + version: 0.18.5 + zod: + specifier: ^4.1.12 + version: 4.1.12 + devDependencies: + '@builder.io/vite-plugin-jsx-loc': + specifier: ^0.1.1 + version: 0.1.1(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)) + '@tailwindcss/typography': + specifier: ^0.5.15 + version: 0.5.19(tailwindcss@4.1.14) + '@tailwindcss/vite': + specifier: ^4.1.3 + version: 4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)) + '@types/bcryptjs': + specifier: ^3.0.0 + version: 3.0.0 + '@types/express': + specifier: 4.17.21 + version: 4.17.21 + '@types/google.maps': + specifier: ^3.58.1 + version: 3.58.1 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + '@types/node': + specifier: ^24.7.0 + version: 24.7.0 + '@types/nodemailer': + specifier: ^7.0.4 + version: 7.0.4 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + '@types/react': + specifier: ^19.1.16 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.1.9 + version: 19.2.1(@types/react@19.2.2) + '@vitejs/plugin-react': + specifier: ^5.0.4 + version: 5.0.4(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)) + add: + specifier: ^2.0.6 + version: 2.0.6 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.21(postcss@8.5.6) + drizzle-kit: + specifier: ^0.31.4 + version: 0.31.5 + esbuild: + specifier: ^0.25.0 + version: 0.25.10 + pnpm: + specifier: ^10.15.1 + version: 10.18.0 + postcss: + specifier: ^8.4.47 + version: 8.5.6 + prettier: + specifier: ^3.6.2 + version: 3.6.2 + tailwindcss: + specifier: ^4.1.14 + version: 4.1.14 + tsx: + specifier: ^4.19.1 + version: 4.20.6 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^7.1.7 + version: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) + vite-plugin-manus-runtime: + specifier: ^0.0.56 + version: 0.0.56 + vitest: + specifier: ^2.1.4 + version: 2.1.9(@types/node@24.7.0)(lightningcss@1.30.1) + +packages: + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@antfu/utils@9.3.0': + resolution: {integrity: sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==} + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.907.0': + resolution: {integrity: sha512-A606SYZtnrVDuuQTsG5fEurHMUeJeqh5TFLx0m3t2x27USuLH0dlw9s2ygFFdAcyzVG7u+my0CIMNQPWida/NA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sesv2@3.940.0': + resolution: {integrity: sha512-jDQ4x2HwB2/UXBS7CTeSDiIb+sVsYGDyxTeXdrRAtqNdGv8kC54fbwokDiJ/mnMyB2gyXWw57BqeDJNkZuLmsw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.907.0': + resolution: {integrity: sha512-ANuu0duNTcQHv0g5YrEuWImT8o9t6li3A+MtAaKxIbTA3eFQnl6xHDxyrbsrU19FtKPg3CWhvfY04j6DaDvR8g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.940.0': + resolution: {integrity: sha512-SdqJGWVhmIURvCSgkDditHRO+ozubwZk9aCX9MK8qxyOndhobCndW1ozl3hX9psvMAo9Q4bppjuqy/GHWpjB+A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.907.0': + resolution: {integrity: sha512-vuIHL8qUcA5oNi7IWSZauCMaXstWTcSsnK1iHcvg92ddGDo1LMd2kQNo0G9UANa8vOfc908+8xKO40gfL8+M7w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.940.0': + resolution: {integrity: sha512-KsGD2FLaX5ngJao1mHxodIVU9VYd1E8810fcYiGwO1PFHDzf5BEkp6D9IdMeQwT8Q6JLYtiiT1Y/o3UCScnGoA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.907.0': + resolution: {integrity: sha512-orqT6djon57y09Ci5q0kezisrEvr78Z+7WvZbq0ZC0Ncul4RgJfCmhcgmzNPaWA18NEI0wGytaxYh3YFE7kIBQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.940.0': + resolution: {integrity: sha512-/G3l5/wbZYP2XEQiOoIkRJmlv15f1P3MSd1a0gz27lHEMrOJOGq66rF1Ca4OJLzapWt3Fy9BPrZAepoAX11kMw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.907.0': + resolution: {integrity: sha512-CKG/0hT4o8K2aQKOe+xwGP3keSNOyryhZNmKuHPuMRVlsJfO6wNxlu37HcUPzihJ+S2pOmTVGUbeVMCxJVUJmw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.940.0': + resolution: {integrity: sha512-dOrc03DHElNBD6N9Okt4U0zhrG4Wix5QUBSZPr5VN8SvmjD9dkrrxOkkJaMCl/bzrW7kbQEp7LuBdbxArMmOZQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.907.0': + resolution: {integrity: sha512-Clz1YdXrgQ5WIlcRE7odHbgM/INBxy49EA3csDITafHaDPtPRL39zkQtB5+Lwrrt/Gg0xBlyTbvP5Snan+0lqA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.940.0': + resolution: {integrity: sha512-gn7PJQEzb/cnInNFTOaDoCN/hOKqMejNmLof1W5VW95Qk0TPO52lH8R4RmJPnRrwFMswOWswTOpR1roKNLIrcw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-login@3.940.0': + resolution: {integrity: sha512-fOKC3VZkwa9T2l2VFKWRtfHQPQuISqqNl35ZhcXjWKVwRwl/o7THPMkqI4XwgT2noGa7LLYVbWMwnsgSsBqglg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.907.0': + resolution: {integrity: sha512-w6Hhc4rV/CFaBliIh9Ph/T59xdGcTF6WmPGzzpykjl68+jcJyUem82hbTVIGaMCpvhx8VRqEr5AEXCXdbDbojw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.940.0': + resolution: {integrity: sha512-M8NFAvgvO6xZjiti5kztFiAYmSmSlG3eUfr4ZHSfXYZUA/KUdZU/D6xJyaLnU8cYRWBludb6K9XPKKVwKfqm4g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.907.0': + resolution: {integrity: sha512-MBWpZqZtKkpM/LOGD5quXvlHJJN8YIP4GKo2ad8y1fEEVydwI8cggyXuauMPV7GllW8d0u3kQUs+4rxm1VaS4w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.940.0': + resolution: {integrity: sha512-pILBzt5/TYCqRsJb7vZlxmRIe0/T+FZPeml417EK75060ajDGnVJjHcuVdLVIeKoTKm9gmJc9l45gon6PbHyUQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.907.0': + resolution: {integrity: sha512-F8I7xwIt0mhdg8NrC70HDmhDRx3ValBvmWH3YkWsjZltWIFozhQCCDISRPhanMkXVhSFmZY0FJ5Lo+B/SZvAAA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.940.0': + resolution: {integrity: sha512-q6JMHIkBlDCOMnA3RAzf8cGfup+8ukhhb50fNpghMs1SNBGhanmaMbZSgLigBRsPQW7fOk2l8jnzdVLS+BB9Uw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.907.0': + resolution: {integrity: sha512-1CmRE/M8LJ/joXm5vUsKkQS35MoWA4xvUH9J1jyCuL3J9A8M+bnTe6ER8fnNLgmEs6ikdmYEIdfijPpBjBpFig==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.940.0': + resolution: {integrity: sha512-9QLTIkDJHHaYL0nyymO41H8g3ui1yz6Y3GmAN1gYQa6plXisuFBnGAbmKVj7zNvjWaOKdF0dV3dd3AFKEDoJ/w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.901.0': + resolution: {integrity: sha512-mPF3N6eZlVs9G8aBSzvtoxR1RZqMo1aIwR+X8BAZSkhfj55fVF2no4IfPXfdFO3I66N+zEQ8nKoB0uTATWrogQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-expect-continue@3.901.0': + resolution: {integrity: sha512-bwq9nj6MH38hlJwOY9QXIDwa6lI48UsaZpaXbdD71BljEIRlxDzfB4JaYb+ZNNK7RIAdzsP/K05mJty6KJAQHw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.907.0': + resolution: {integrity: sha512-rrJFVsaeGr+bYOzUO2ph9LihFPNq9wcdg/9gcDI3oC9LtOgKeidFgU4t9603WGKGc2eXf4MxzYnaf7LXDOnVvQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.901.0': + resolution: {integrity: sha512-yWX7GvRmqBtbNnUW7qbre3GvZmyYwU0WHefpZzDTYDoNgatuYq6LgUIQ+z5C04/kCRoFkAFrHag8a3BXqFzq5A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.936.0': + resolution: {integrity: sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-location-constraint@3.901.0': + resolution: {integrity: sha512-MuCS5R2ngNoYifkVt05CTULvYVWX0dvRT0/Md4jE3a0u0yMygYy31C1zorwfE/SUgAQXyLmUx8ATmPp9PppImQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.901.0': + resolution: {integrity: sha512-UoHebjE7el/tfRo8/CQTj91oNUm+5Heus5/a4ECdmWaSCHCS/hXTsU3PTTHAY67oAQR8wBLFPfp3mMvXjB+L2A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.936.0': + resolution: {integrity: sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.901.0': + resolution: {integrity: sha512-Wd2t8qa/4OL0v/oDpCHHYkgsXJr8/ttCxrvCKAt0H1zZe2LlRhY9gpDVKqdertfHrHDj786fOvEQA28G1L75Dg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.936.0': + resolution: {integrity: sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.907.0': + resolution: {integrity: sha512-8VVxcZPmJOKI8P08v5ARvoXbLV41abpAIIkt388fp/lwtfzbnXt6sWhhAk/pHgvCR1NnuvkbGuXGVcux59648Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.940.0': + resolution: {integrity: sha512-JYkLjgS1wLoKHJ40G63+afM1ehmsPsjcmrHirKh8+kSCx4ip7+nL1e/twV4Zicxr8RJi9Y0Ahq5mDvneilDDKQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-ssec@3.901.0': + resolution: {integrity: sha512-YiLLJmA3RvjL38mFLuu8fhTTGWtp2qT24VqpucgfoyziYcTgIQkJJmKi90Xp6R6/3VcArqilyRgM1+x8i/em+Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.907.0': + resolution: {integrity: sha512-j/h3lk4X6AAXvusx/h8rr0zlo7G0l0quZM4k4rS/9jzatI53HCsrMaiGu6YXbxuVqtfMqv0MAj0MVhaMsAIs4A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.940.0': + resolution: {integrity: sha512-nJbLrUj6fY+l2W2rIB9P4Qvpiy0tnTdg/dmixRxrU1z3e8wBdspJlyE+AZN4fuVbeL6rrRrO/zxQC1bB3cw5IA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/nested-clients@3.907.0': + resolution: {integrity: sha512-LycXsdC5sMIc+Az5z1Mo2eYShr2kLo2gUgx7Rja3udG0GdqgdR/NNJ6ArmDCeKk2O5RFS5EgEg89bT55ecl5Uw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/nested-clients@3.940.0': + resolution: {integrity: sha512-x0mdv6DkjXqXEcQj3URbCltEzW6hoy/1uIL+i8gExP6YKrnhiZ7SzuB4gPls2UOpK5UqLiqXjhRLfBb1C9i4Dw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.901.0': + resolution: {integrity: sha512-7F0N888qVLHo4CSQOsnkZ4QAp8uHLKJ4v3u09Ly5k4AEStrSlFpckTPyUx6elwGL+fxGjNE2aakK8vEgzzCV0A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.936.0': + resolution: {integrity: sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/s3-request-presigner@3.907.0': + resolution: {integrity: sha512-7RZH4fhCRVq8bacAZd6x+Ko04cmV+Zq4Axk1HZqL3epZ+r4HLR/0NklYZjr/dqMlMx9VbexzNkpBY1VgWxOSyQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.907.0': + resolution: {integrity: sha512-f5XHRu6MTbjB/ud5RwBZzntYMgThRaDur5PJRZ1CaYAL8gZLNuEpJLzUA7o3queeSfE9JboO+cm1gZnOfnYJkg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.940.0': + resolution: {integrity: sha512-ugHZEoktD/bG6mdgmhzLDjMP2VrYRAUPRPF1DpCyiZexkH7DCU7XrSJyXMvkcf0DHV+URk0q2sLf/oqn1D2uYw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.907.0': + resolution: {integrity: sha512-HjPbNft1Ad8X1lHQG21QXy9pitdXA+OKH6NtcXg57A31002tM+SkyUmU6ty1jbsRBEScxziIVe5doI1NmkHheA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.940.0': + resolution: {integrity: sha512-k5qbRe/ZFjW9oWEdzLIa2twRVIEx7p/9rutofyrRysrtEnYh3HAWCngAnwbgKMoiwa806UzcTRx0TjyEpnKcCg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.901.0': + resolution: {integrity: sha512-FfEM25hLEs4LoXsLXQ/q6X6L4JmKkKkbVFpKD4mwfVHtRVQG6QxJiCPcrkcPISquiy6esbwK2eh64TWbiD60cg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.936.0': + resolution: {integrity: sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-arn-parser@3.893.0': + resolution: {integrity: sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-endpoints@3.901.0': + resolution: {integrity: sha512-5nZP3hGA8FHEtKvEQf4Aww5QZOkjLW1Z+NixSd+0XKfHvA39Ah5sZboScjLx0C9kti/K3OGW1RCx5K9Zc3bZqg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-endpoints@3.936.0': + resolution: {integrity: sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-format-url@3.901.0': + resolution: {integrity: sha512-GGUnJKrh3OF1F3YRSWtwPLbN904Fcfxf03gujyq1rcrDRPEkzoZB+2BzNkB27SsU6lAlwNq+4aRlZRVUloPiag==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-locate-window@3.893.0': + resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-user-agent-browser@3.907.0': + resolution: {integrity: sha512-Hus/2YCQmtCEfr4Ls88d07Q99Ex59uvtktiPTV963Q7w7LHuIT/JBjrbwNxtSm2KlJR9PHNdqxwN+fSuNsMGMQ==} + + '@aws-sdk/util-user-agent-browser@3.936.0': + resolution: {integrity: sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==} + + '@aws-sdk/util-user-agent-node@3.907.0': + resolution: {integrity: sha512-r2Bc8VCU6ymkuem+QWT6oDdGvaYnK0YHg77SGUF47k+JsztSt1kZR0Y0q8jRH97bOsXldThyEcYsNbqDERa1Uw==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/util-user-agent-node@3.940.0': + resolution: {integrity: sha512-dlD/F+L/jN26I8Zg5x0oDGJiA+/WEQmnSE27fi5ydvYnpfQLwThtQo9SsNS47XSR/SOULaaoC9qx929rZuo74A==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.901.0': + resolution: {integrity: sha512-pxFCkuAP7Q94wMTNPAwi6hEtNrp/BdFf+HOrIEeFQsk4EoOmpKY3I6S+u6A9Wg295J80Kh74LqDWM22ux3z6Aw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/xml-builder@3.930.0': + resolution: {integrity: sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==} + engines: {node: '>=18.0.0'} + + '@aws/lambda-invoke-store@0.0.1': + resolution: {integrity: sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==} + engines: {node: '>=18.0.0'} + + '@aws/lambda-invoke-store@0.2.1': + resolution: {integrity: sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==} + engines: {node: '>=18.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@7.1.1': + resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + + '@builder.io/jsx-loc-internals@0.0.1': + resolution: {integrity: sha512-cSADapVCi07DDhcuDmcAVItqSVmji7DNyD3xxYTHyNCwhWMNnTpZjyvDIWwYFJLleyDCJ9VUtbaXtUjjqBiRqw==} + + '@builder.io/vite-plugin-jsx-loc@0.1.1': + resolution: {integrity: sha512-iAHFkaLBDJBC+EkGO1hF7hnIW2+oKKYVOl8NFAQH//3xeNEzvGdS9tOALRPR+JjR/M5NLyj+FG0VV7WFb1aJmw==} + peerDependencies: + vite: ^4.0.0 || ^5.0.0 + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fast-csv/format@4.3.5': + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + + '@fast-csv/parse@4.3.6': + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.0.2': + resolution: {integrity: sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@medv/finder@4.0.2': + resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==} + + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + + '@rolldown/pluginutils@1.0.0-beta.38': + resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} + + '@rollup/rollup-android-arm-eabi@4.52.4': + resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.4': + resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.4': + resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.4': + resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.4': + resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.4': + resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.4': + resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.4': + resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.4': + resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.4': + resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.4': + resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.4': + resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} + cpu: [x64] + os: [win32] + + '@shikijs/core@3.14.0': + resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==} + + '@shikijs/engine-javascript@3.14.0': + resolution: {integrity: sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ==} + + '@shikijs/engine-oniguruma@3.14.0': + resolution: {integrity: sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==} + + '@shikijs/langs@3.14.0': + resolution: {integrity: sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==} + + '@shikijs/themes@3.14.0': + resolution: {integrity: sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==} + + '@shikijs/types@3.14.0': + resolution: {integrity: sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@smithy/abort-controller@4.2.0': + resolution: {integrity: sha512-PLUYa+SUKOEZtXFURBu/CNxlsxfaFGxSBPcStL13KpVeVWIfdezWyDqkz7iDLmwnxojXD0s5KzuB5HGHvt4Aeg==} + engines: {node: '>=18.0.0'} + + '@smithy/abort-controller@4.2.5': + resolution: {integrity: sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.2.1': + resolution: {integrity: sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.0': + resolution: {integrity: sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.3.0': + resolution: {integrity: sha512-9oH+n8AVNiLPK/iK/agOsoWfrKZ3FGP3502tkksd6SRsKMYiu7AFX0YXo6YBADdsAj7C+G/aLKdsafIJHxuCkQ==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.3': + resolution: {integrity: sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.15.0': + resolution: {integrity: sha512-VJWncXgt+ExNn0U2+Y7UywuATtRYaodGQKFo9mDyh70q+fJGedfrqi2XuKU1BhiLeXgg6RZrW7VEKfeqFhHAJA==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.18.5': + resolution: {integrity: sha512-6gnIz3h+PEPQGDj8MnRSjDvKBah042jEoPgjFGJ4iJLBE78L4lY/n98x14XyPF4u3lN179Ub/ZKFY5za9GeLQw==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.0': + resolution: {integrity: sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.5': + resolution: {integrity: sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.0': + resolution: {integrity: sha512-XE7CtKfyxYiNZ5vz7OvyTf1osrdbJfmUy+rbh+NLQmZumMGvY0mT0Cq1qKSfhrvLtRYzMsOBuRpi10dyI0EBPg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.0': + resolution: {integrity: sha512-U53p7fcrk27k8irLhOwUu+UYnBqsXNLKl1XevOpsxK3y1Lndk8R7CSiZV6FN3fYFuTPuJy5pP6qa/bjDzEkRvA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.0': + resolution: {integrity: sha512-uwx54t8W2Yo9Jr3nVF5cNnkAAnMCJ8Wrm+wDlQY6rY/IrEgZS3OqagtCu/9ceIcZFQ1zVW/zbN9dxb5esuojfA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.0': + resolution: {integrity: sha512-yjM2L6QGmWgJjVu/IgYd6hMzwm/tf4VFX0lm8/SvGbGBwc+aFl3hOzvO/e9IJ2XI+22Tx1Zg3vRpFRs04SWFcg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.0': + resolution: {integrity: sha512-C3jxz6GeRzNyGKhU7oV656ZbuHY93mrfkT12rmjDdZch142ykjn8do+VOkeRNjSGKw01p4g+hdalPYPhmMwk1g==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.1': + resolution: {integrity: sha512-3AvYYbB+Dv5EPLqnJIAgYw/9+WzeBiUYS8B+rU0pHq5NMQMvrZmevUROS4V2GAt0jEOn9viBzPLrZE+riTNd5Q==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.6': + resolution: {integrity: sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.2.1': + resolution: {integrity: sha512-Os9cg1fTXMwuqbvjemELlf+HB5oEeVyZmYsTbAtDQBmjGyibjmbeeqcaw7xOJLIHrkH/u0wAYabNcN6FRTqMRg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.0': + resolution: {integrity: sha512-ugv93gOhZGysTctZh9qdgng8B+xO0cj+zN0qAZ+Sgh7qTQGPOJbMdIuyP89KNfUyfAqFSNh5tMvC+h2uCpmTtA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.5': + resolution: {integrity: sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.2.0': + resolution: {integrity: sha512-8dELAuGv+UEjtzrpMeNBZc1sJhO8GxFVV/Yh21wE35oX4lOE697+lsMHBoUIFAUuYkTMIeu0EuJSEsH7/8Y+UQ==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.0': + resolution: {integrity: sha512-ZmK5X5fUPAbtvRcUPtk28aqIClVhbfcmfoS4M7UQBTnDdrNxhsrxYVv0ZEl5NaPSyExsPWqL4GsPlRvtlwg+2A==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.5': + resolution: {integrity: sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.0': + resolution: {integrity: sha512-LFEPniXGKRQArFmDQ3MgArXlClFJMsXDteuQQY8WG1/zzv6gVSo96+qpkuu1oJp4MZsKrwchY0cuAoPKzEbaNA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.0': + resolution: {integrity: sha512-6ZAnwrXFecrA4kIDOcz6aLBhU5ih2is2NdcZtobBDSdSHtE9a+MThB5uqyK4XXesdOCvOcbCm2IGB95birTSOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.5': + resolution: {integrity: sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.3.1': + resolution: {integrity: sha512-JtM4SjEgImLEJVXdsbvWHYiJ9dtuKE8bqLlvkvGi96LbejDL6qnVpVxEFUximFodoQbg0Gnkyff9EKUhFhVJFw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.3.12': + resolution: {integrity: sha512-9pAX/H+VQPzNbouhDhkW723igBMLgrI8OtX+++M7iKJgg/zY/Ig3i1e6seCcx22FWhE6Q/S61BRdi2wXBORT+A==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.1': + resolution: {integrity: sha512-wXxS4ex8cJJteL0PPQmWYkNi9QKDWZIpsndr0wZI2EL+pSSvA/qqxXU60gBOJoIc2YgtZSWY/PE86qhKCCKP1w==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.12': + resolution: {integrity: sha512-S4kWNKFowYd0lID7/DBqWHOQxmxlsf0jBaos9chQZUWTVOjSW1Ogyh8/ib5tM+agFDJ/TCxuCTvrnlc+9cIBcQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.0': + resolution: {integrity: sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.6': + resolution: {integrity: sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.0': + resolution: {integrity: sha512-G5CJ//eqRd9OARrQu9MK1H8fNm2sMtqFh6j8/rPozhEL+Dokpvi1Og+aCixTuwDAGZUkJPk6hJT5jchbk/WCyg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.5': + resolution: {integrity: sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.0': + resolution: {integrity: sha512-5QgHNuWdT9j9GwMPPJCKxy2KDxZ3E5l4M3/5TatSZrqYVoEiqQrDfAq8I6KWZw7RZOHtVtCzEPdYz7rHZixwcA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.5': + resolution: {integrity: sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.3.0': + resolution: {integrity: sha512-RHZ/uWCmSNZ8cneoWEVsVwMZBKy/8123hEpm57vgGXA3Irf/Ja4v9TVshHK2ML5/IqzAZn0WhINHOP9xl+Qy6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.5': + resolution: {integrity: sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.0': + resolution: {integrity: sha512-rV6wFre0BU6n/tx2Ztn5LdvEdNZ2FasQbPQmDOPfV9QQyDmsCkOAB0osQjotRCQg+nSKFmINhyda0D3AnjSBJw==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.5': + resolution: {integrity: sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.0': + resolution: {integrity: sha512-6POSYlmDnsLKb7r1D3SVm7RaYW6H1vcNcTWGWrF7s9+2noNYvUsm7E4tz5ZQ9HXPmKn6Hb67pBDRIjrT4w/d7Q==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.5': + resolution: {integrity: sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.0': + resolution: {integrity: sha512-Q4oFD0ZmI8yJkiPPeGUITZj++4HHYCW3pYBYfIobUCkYpI6mbkzmG1MAQQ3lJYYWj3iNqfzOenUZu+jqdPQ16A==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.5': + resolution: {integrity: sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.0': + resolution: {integrity: sha512-BjATSNNyvVbQxOOlKse0b0pSezTWGMvA87SvoFoFlkRsKXVsN3bEtjCxvsNXJXfnAzlWFPaT9DmhWy1vn0sNEA==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.5': + resolution: {integrity: sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.0': + resolution: {integrity: sha512-Ylv1ttUeKatpR0wEOMnHf1hXMktPUMObDClSWl2TpCVT4DwtJhCeighLzSLbgH3jr5pBNM0LDXT5yYxUvZ9WpA==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.5': + resolution: {integrity: sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.3.0': + resolution: {integrity: sha512-VCUPPtNs+rKWlqqntX0CbVvWyjhmX30JCtzO+s5dlzzxrvSfRh5SY0yxnkirvc1c80vdKQttahL71a9EsdolSQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.0': + resolution: {integrity: sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.0': + resolution: {integrity: sha512-MKNyhXEs99xAZaFhm88h+3/V+tCRDQ+PrDzRqL0xdDpq4gjxcMmf5rBA3YXgqZqMZ/XwemZEurCBQMfxZOWq/g==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.5': + resolution: {integrity: sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.7.1': + resolution: {integrity: sha512-WXVbiyNf/WOS/RHUoFMkJ6leEVpln5ojCjNBnzoZeMsnCg3A0BRhLK3WYc4V7PmYcYPZh9IYzzAg9XcNSzYxYQ==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.9.8': + resolution: {integrity: sha512-8xgq3LgKDEFoIrLWBho/oYKyWByw9/corz7vuh1upv7ZBm0ZMjGYBhbn6v643WoIqA9UTcx5A5htEp/YatUwMA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.6.0': + resolution: {integrity: sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.9.0': + resolution: {integrity: sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.0': + resolution: {integrity: sha512-AlBmD6Idav2ugmoAL6UtR6ItS7jU5h5RNqLMZC7QrLCoITA9NzIN3nx9GWi8g4z1pfWh2r9r96SX/jHiNwPJ9A==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.5': + resolution: {integrity: sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.0': + resolution: {integrity: sha512-H4MAj8j8Yp19Mr7vVtGgi7noJjvjJbsKQJkvNnLlrIFduRFT5jq5Eri1k838YW7rN2g5FTnXpz5ktKVr1KVgPQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.11': + resolution: {integrity: sha512-yHv+r6wSQXEXTPVCIQTNmXVWs7ekBTpMVErjqZoWkYN75HIFN5y9+/+sYOejfAuvxWGvgzgxbTHa/oz61YTbKw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.1': + resolution: {integrity: sha512-PuDcgx7/qKEMzV1QFHJ7E4/MMeEjaA7+zS5UNcHCLPvvn59AeZQ0DSDGMpqC2xecfa/1cNGm4l8Ec/VxCuY7Ug==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.14': + resolution: {integrity: sha512-ljZN3iRvaJUgulfvobIuG97q1iUuCMrvXAlkZ4msY+ZuVHQHDIqn7FKZCEj+bx8omz6kF5yQXms/xhzjIO5XiA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.0': + resolution: {integrity: sha512-TXeCn22D56vvWr/5xPqALc9oO+LN+QpFjrSM7peG/ckqEPoI3zaKZFp+bFwfmiHhn5MGWPaLCqDOJPPIixk9Wg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.5': + resolution: {integrity: sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.0': + resolution: {integrity: sha512-u9OOfDa43MjagtJZ8AapJcmimP+K2Z7szXn8xbty4aza+7P1wjFmy2ewjSbhEiYQoW1unTlOAIV165weYAaowA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.5': + resolution: {integrity: sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.0': + resolution: {integrity: sha512-BWSiuGbwRnEE2SFfaAZEX0TqaxtvtSYPM/J73PFVm+A29Fg1HTPiYFb8TmX1DXp4hgcdyJcNQmprfd5foeORsg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.5': + resolution: {integrity: sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.0': + resolution: {integrity: sha512-0TD5M5HCGu5diEvZ/O/WquSjhJPasqv7trjoqHyWjNh/FBeBl7a0ztl9uFMOsauYtRfd8jvpzIAQhDHbx+nvZw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.6': + resolution: {integrity: sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.0': + resolution: {integrity: sha512-0Z+nxUU4/4T+SL8BCNN4ztKdQjToNvUYmkF1kXO5T7Yz3Gafzh0HeIG6mrkN8Fz3gn9hSyxuAT+6h4vM+iQSBQ==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.0': + 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==} + + '@tailwindcss/oxide-android-arm64@4.1.14': + resolution: {integrity: sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + resolution: {integrity: sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.14': + resolution: {integrity: sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + resolution: {integrity: sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + resolution: {integrity: sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + resolution: {integrity: sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + resolution: {integrity: sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.14': + resolution: {integrity: sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==} + engines: {node: '>= 10'} + + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@tailwindcss/vite@4.1.14': + resolution: {integrity: sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/query-core@5.90.2': + resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + + '@tanstack/react-query@5.90.2': + resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} + peerDependencies: + react: ^18 || ^19 + + '@tiptap/core@3.11.0': + resolution: {integrity: sha512-kmS7ZVpHm1EMnW1Wmft9H5ZLM7E0G0NGBx+aGEHGDcNxZBXD2ZUa76CuWjIhOGpwsPbELp684ZdpF2JWoNi4Dg==} + peerDependencies: + '@tiptap/pm': ^3.11.0 + + '@tiptap/extension-blockquote@3.11.0': + resolution: {integrity: sha512-0H8WVW6Vn4GJ7sQ6wfyDgUU+DqM8fp62g8N0fFPiEhoYtpIYUmCqGhpKnqYR0tet6ofFa648XmA6n2VX7sugzw==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-bold@3.11.0': + resolution: {integrity: sha512-V/c3XYO09Le9GlBGq1MK4c97Fffi0GADQTbZ+LFoi65nUrAwutn5wYnXBcEyWQI6RmFWVDJTieamqtc4j9teyw==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-bubble-menu@3.11.0': + resolution: {integrity: sha512-P3j9lQ+EZ5Zg/isJzLpCPX7bp7WUBmz8GPs/HPlyMyN2su8LqXntITBZr8IP1JNBlB/wR83k/W0XqdC57mG7cA==} + peerDependencies: + '@tiptap/core': ^3.11.0 + '@tiptap/pm': ^3.11.0 + + '@tiptap/extension-bullet-list@3.11.0': + resolution: {integrity: sha512-IKdb1C3bHA1sGPiUcntkL+wHebRg71K5+tgaaRnMw0qmtcpcOQb5zhQOSm5bXUsgCk/WgT04dkZPnpn6Gg1PvQ==} + peerDependencies: + '@tiptap/extension-list': ^3.11.0 + + '@tiptap/extension-code-block@3.11.0': + resolution: {integrity: sha512-y01RJVbygDJWYXxZ0SiCYwvUF2X91RANCLSdb8X0qiwVPgNOzsDrrzS/iqoXkiYmM93pJw+ZWelEZxRvxEwsrg==} + peerDependencies: + '@tiptap/core': ^3.11.0 + '@tiptap/pm': ^3.11.0 + + '@tiptap/extension-code@3.11.0': + resolution: {integrity: sha512-5OpR5O4bveHe1KG9CJsto86NgkuerYq3OLY78vzh9uFCLdv7xgXA2aZYJfRMhbZ7hKsR7hHg1etBJUCk+TKsMg==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-color@3.11.0': + resolution: {integrity: sha512-4H+3nyheow0tBt+q/Pkkm/pR/F6vUAdKZaq1HtWFi7LcFzCYxdB3Si625EparKO3kMvGvULjdd+/zt68NS7Acw==} + peerDependencies: + '@tiptap/extension-text-style': ^3.11.0 + + '@tiptap/extension-document@3.11.0': + resolution: {integrity: sha512-N2G3cwL2Dtur/CgD/byJmFx9T5no6fTO/U462VP3rthQYrRA1AB3TCYqtlwJkmyoxRTNd4qIg4imaPl8ej6Heg==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-dropcursor@3.11.0': + resolution: {integrity: sha512-gW/QMGAyiXGSpO+X/lTeiBQn1Or8T8UVB3y9Cv2Lh6zx0SWU+FA28EH+y6s3fm872reN4dH/9rEvMuJjhU/BEw==} + peerDependencies: + '@tiptap/extensions': ^3.11.0 + + '@tiptap/extension-floating-menu@3.11.0': + resolution: {integrity: sha512-nEHdWZHEJYX1II1oJQ4aeZ8O/Kss4BRbYFXQFGIvPelCfCYEATpUJh3aq3767ARSq40bOWyu+Dcd4SCW0We6Sw==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.11.0 + '@tiptap/pm': ^3.11.0 + + '@tiptap/extension-gapcursor@3.11.0': + resolution: {integrity: sha512-lXGEZiYX7k/pEFr8BgDE91vqjLTwuf+qhHLTgIpfhbt562nShLPIDj9Vzu3xrR4fwUAMiUNiLyaeInb8j3I4kg==} + peerDependencies: + '@tiptap/extensions': ^3.11.0 + + '@tiptap/extension-hard-break@3.11.0': + resolution: {integrity: sha512-NJEHTj++kFOayQXKSQSi9j9eAG33eSiJqai2pf4U+snW94fmb8cYLUurDmfYRe20O6EzBSX0X3GjVlkOz+5b7A==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-heading@3.11.0': + resolution: {integrity: sha512-4Eo67Yo7vsYLkizcMoGdZAR9aHbC7FFTrqfNEd4Em3ajRi0iNqyWMaI90UCYlitDdRdqFlq/njWrMqBOLUgaWQ==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-horizontal-rule@3.11.0': + resolution: {integrity: sha512-FugFHZG+oiMBV6k42hn9NOA4wRNc2b9UeEIMR+XwEMpWJInV4VwSwDvu8JClgkDo8z7FEnker9e51DZ00CLWqg==} + peerDependencies: + '@tiptap/core': ^3.11.0 + '@tiptap/pm': ^3.11.0 + + '@tiptap/extension-italic@3.11.0': + resolution: {integrity: sha512-WP6wL2b//8bLVdeUCWOpYA7nUStvrAMMD0nRn0F9CEW+l7vH6El2PZFhHmJ9uqXo5MnyugBpARiwgxfoAlef5w==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-link@3.11.0': + resolution: {integrity: sha512-RoUkGqowVMKLE76KktNOGhzNMyKtwrSDRqeYCe1ODPuOMZvDGexOE8cIuA4A1ODkgN6ji9qE/9Sf8uhpZdH39Q==} + peerDependencies: + '@tiptap/core': ^3.11.0 + '@tiptap/pm': ^3.11.0 + + '@tiptap/extension-list-item@3.11.0': + resolution: {integrity: sha512-KXTTSBH/T/WW8O1YhK/lVmwlSGh2w2VVucUkMLhgk1VPchahAkn2LfgbgKrCRG/F8M8Jlfvz67iJDo6+bbNqew==} + peerDependencies: + '@tiptap/extension-list': ^3.11.0 + + '@tiptap/extension-list-keymap@3.11.0': + resolution: {integrity: sha512-vm1zGdEqcbQnrGlVXchk1ibmTsyxyfGcGPVWsc4MG+UAFcNfcpAnvCar71BF4RGGPtpzOWdqGkvJENyh0L5/Hw==} + peerDependencies: + '@tiptap/extension-list': ^3.11.0 + + '@tiptap/extension-list@3.11.0': + resolution: {integrity: sha512-4Ane7VCVZ+GFOQNuy2nMP+SoWH7EemC3geTTqvgHm1H0tbSosxLJAVaZ9dF06F35RJmYCm+jLJUhRVd156eCRQ==} + peerDependencies: + '@tiptap/core': ^3.11.0 + '@tiptap/pm': ^3.11.0 + + '@tiptap/extension-ordered-list@3.11.0': + resolution: {integrity: sha512-kO8GH4w4Xil+qPiHJLAyILdGHF9hCjkhoVtPD8YEfqK6Qx3bZql5FPySCQNs+MU6rLSCCdam8SUPGY/+SCufqA==} + peerDependencies: + '@tiptap/extension-list': ^3.11.0 + + '@tiptap/extension-paragraph@3.11.0': + resolution: {integrity: sha512-hxgjZOXOqstRTWv+QjWJjK23rD5qzIV9ePlhX3imLeq/MgX0aU9VBDaG5SGKbSjaBNQnpLw6+sABJi3CDP6Z5A==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-strike@3.11.0': + resolution: {integrity: sha512-XVP/WMYLrqLBfUsGPu2H9MrOUZLhGUaxtZ3hSRffDi/lsw53x/coZ9eO0FxOB9R7z2ksHWmticIs+0YnKt9LNQ==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-text-style@3.11.0': + resolution: {integrity: sha512-q8RM4gzmdnUHosL65SIJzTTmL29bm+3hNPdloOuJyLd4sTYs2q+cues5mH5/n85HqX3+TvKrfrTVb1Yj62E1NA==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-text@3.11.0': + resolution: {integrity: sha512-ELAYm2BuChzZOqDG9B0k3W6zqM4pwNvXkam28KgHGiT2y7Ni68Rb+NXp16uVR+5zR6hkqnQ/BmJSKzAW59MXpA==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extension-underline@3.11.0': + resolution: {integrity: sha512-D3PsS/84RlQKFjd5eerMIUioC0mNh4yy1RRV/WbXx6ugu+6T+0hT42gNk9Ap8pDsVQZCk0SHfDyBEUFC2KOwKw==} + peerDependencies: + '@tiptap/core': ^3.11.0 + + '@tiptap/extensions@3.11.0': + resolution: {integrity: sha512-g43beA73ZMLezez1st9LEwYrRHZ0FLzlsSlOZKk7sdmtHLmuqWHf4oyb0XAHol1HZIdGv104rYaGNgmQXr1ecQ==} + peerDependencies: + '@tiptap/core': ^3.11.0 + '@tiptap/pm': ^3.11.0 + + '@tiptap/pm@3.11.0': + resolution: {integrity: sha512-plCQDLCZIOc92cizB8NNhBRN0szvYR3cx9i5IXo6v9Xsgcun8KHNcJkesc2AyeqdIs0BtOJZaqQ9adHThz8UDw==} + + '@tiptap/react@3.11.0': + resolution: {integrity: sha512-SDGei/2DjwmhzsxIQNr6dkB6NxLgXZjQ6hF36NfDm4937r5NLrWrNk5tCsoDQiKZ0DHEzuJ6yZM5C7I7LZLB6w==} + peerDependencies: + '@tiptap/core': ^3.11.0 + '@tiptap/pm': ^3.11.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.11.0': + resolution: {integrity: sha512-8kMMYqVSZ2Oqji+mY1o9meTjCRWp4DplFegu7APqDEQRhlb6mBI0wNuazYb7FKJIHJTtf0F6cYglJrxpu9c/fA==} + + '@trpc/client@11.6.0': + resolution: {integrity: sha512-DyWbYk2hd50BaVrXWVkaUnaSwgAF5g/lfBkXtkF1Aqlk6BtSzGUo3owPkgqQO2I5LwWy1+ra9TsSfBBvIZpTwg==} + peerDependencies: + '@trpc/server': 11.6.0 + typescript: '>=5.7.2' + + '@trpc/react-query@11.6.0': + resolution: {integrity: sha512-xljUCzROa23cC89SEd5fwbKiWrGus2NDwtg8zszPlsFvaByWW50Jx6y5sLPXhp/g1FBsEtCInNNhEEL0UCHwGw==} + peerDependencies: + '@tanstack/react-query': ^5.80.3 + '@trpc/client': 11.6.0 + '@trpc/server': 11.6.0 + react: '>=18.2.0' + react-dom: '>=18.2.0' + typescript: '>=5.7.2' + + '@trpc/server@11.6.0': + resolution: {integrity: sha512-skTso0AWbOZck40jwNeYv++AMZXNWLUWdyk+pB5iVaYmEKTuEeMoPrEudR12VafbEU6tZa8HK3QhBfTYYHDCdg==} + peerDependencies: + typescript: '>=5.7.2' + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/bcryptjs@3.0.0': + resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==} + deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed. + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express@4.17.21': + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/google.maps@3.58.1': + resolution: {integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/multer@2.0.0': + resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} + + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@24.7.0': + resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} + + '@types/nodemailer@7.0.4': + resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==} + + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + + '@types/pdfkit@0.17.4': + resolution: {integrity: sha512-odAmVuuguRxKh1X4pbMrJMp8ecwNqHRw6lweupvzK+wuyNmi6wzlUlGVZ9EqMvp3Bs2+L9Ty0sRlrvKL+gsQZg==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.2.1': + resolution: {integrity: sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.2': + resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/send@1.2.0': + resolution: {integrity: sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==} + + '@types/serve-static@1.15.9': + resolution: {integrity: sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-react@5.0.4': + resolution: {integrity: sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + add@2.0.6: + resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==} + + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-arraybuffer@1.0.2: + 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==} + + baseline-browser-mapping@2.8.12: + resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==} + hasBin: true + + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + 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} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001748: + resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==} + + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + 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'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + core-js@3.46.0: + resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + 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==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.1: + resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.11: + resolution: {integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==} + + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dompurify@3.3.0: + resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + drizzle-kit@0.31.5: + resolution: {integrity: sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg==} + hasBin: true + + drizzle-orm@0.44.6: + resolution: {integrity: sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.230: + resolution: {integrity: sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==} + + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + exceljs@4.4.0: + resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} + engines: {node: '>=8.3.0'} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-csv@4.3.6: + resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} + engines: {node: '>=10.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.3.2: + resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==} + engines: {node: '>=6.0.0'} + + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + 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==} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + framer-motion@12.23.22: + resolution: {integrity: sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-from-dom@5.0.1: + resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + input-otp@1.4.2: + resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + 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==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jspdf-autotable@5.0.2: + resolution: {integrity: sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==} + peerDependencies: + jspdf: ^2 || ^3 + + jspdf@3.0.3: + resolution: {integrity: sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + katex@0.16.25: + resolution: {integrity: sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + 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==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + + listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.2: + resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + lucide-react@0.453.0: + resolution: {integrity: sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + + lucide-react@0.542.0: + resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marked@16.4.1: + resolution: {integrity: sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==} + engines: {node: '>= 20'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + mermaid@11.12.0: + resolution: {integrity: sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + modern-screenshot@4.6.6: + resolution: {integrity: sha512-8tF0xEpe7yx37mK95UcIghSCWYeu628K2hLJl+ZNY2ANmRzYLlRLpquPHAQcL8keF6BoeEzTEw4GrgmUpGuZ8w==} + + motion-dom@12.23.21: + resolution: {integrity: sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multer@2.0.2: + resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} + engines: {node: '>= 10.16.0'} + + mysql2@3.15.1: + resolution: {integrity: sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.23: + resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} + + nodemailer@7.0.11: + resolution: {integrity: sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==} + engines: {node: '>=6.0.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.3: + resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + 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==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + 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==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + png-js@1.0.0: + resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + pnpm@10.18.0: + resolution: {integrity: sha512-6AT4ifHOzEDVctsITuw+SIFzn43sacD/ENLRvv+aTjCTg7ontbdQBZ1/TBSVNbbNDSyx7Trrc5I5pChKaPQM+g==} + engines: {node: '>=18.12'} + hasBin: true + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + prosemirror-changeset@2.3.1: + resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.0: + resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.2: + resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==} + + prosemirror-menu@1.2.5: + resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.1: + resolution: {integrity: sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.10.5: + resolution: {integrity: sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==} + + prosemirror-view@1.41.3: + resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + react-day-picker@9.11.1: + resolution: {integrity: sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + + react-hook-form@7.64.0: + resolution: {integrity: sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-resizable-panels@3.0.6: + resolution: {integrity: sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + + regexparam@3.0.0: + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} + engines: {node: '>=8'} + + rehype-harden@1.1.5: + resolution: {integrity: sha512-JrtBj5BVd/5vf3H3/blyJatXJbzQfRT9pJBmjafbTaPouQCAKxHwRyCc7dle9BXQKxv4z1OzZylz/tNamoiG3A==} + + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + 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'} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + rollup@4.52.4: + resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shiki@3.14.0: + resolution: {integrity: sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + streamdown@1.4.0: + resolution: {integrity: sha512-ylhDSQ4HpK5/nAH9v7OgIIdGJxlJB2HoYrYkJNGrO8lMpnWuKUcrz/A8xAMwA6eILA27469vIavcOTjmxctrKg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strnum@2.1.1: + resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} + + style-to-js@1.1.18: + resolution: {integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==} + + style-to-object@1.0.11: + resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==} + + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + + superjson@1.13.3: + resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==} + engines: {node: '>=10'} + + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@4.1.14: + resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} + engines: {node: '>=18'} + + 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==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + 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==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-plugin-manus-runtime@0.0.56: + resolution: {integrity: sha512-BkRMoFzUtT5enXBl+PO0h81KH+4uK4WpLGOp1tvljafb6fp9UX0b1klfxQz9QZFEdB8IPQTEkNzfqv09fHrkMw==} + + vite@5.4.20: + resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@7.1.9: + resolution: {integrity: sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + wouter@3.7.1: + resolution: {integrity: sha512-od5LGmndSUzntZkE2R5CHhoiJ7YMuTIbiXsa0Anytc2RATekgv4sfWRAxLEULBrp7ADzinWQw8g470lkT8+fOw==} + peerDependencies: + react: '>=16.8.0' + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.5.0 + tinyexec: 1.0.1 + + '@antfu/utils@9.3.0': {} + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.901.0 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.901.0 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-locate-window': 3.893.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-locate-window': 3.893.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.901.0 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.907.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.907.0 + '@aws-sdk/credential-provider-node': 3.907.0 + '@aws-sdk/middleware-bucket-endpoint': 3.901.0 + '@aws-sdk/middleware-expect-continue': 3.901.0 + '@aws-sdk/middleware-flexible-checksums': 3.907.0 + '@aws-sdk/middleware-host-header': 3.901.0 + '@aws-sdk/middleware-location-constraint': 3.901.0 + '@aws-sdk/middleware-logger': 3.901.0 + '@aws-sdk/middleware-recursion-detection': 3.901.0 + '@aws-sdk/middleware-sdk-s3': 3.907.0 + '@aws-sdk/middleware-ssec': 3.901.0 + '@aws-sdk/middleware-user-agent': 3.907.0 + '@aws-sdk/region-config-resolver': 3.901.0 + '@aws-sdk/signature-v4-multi-region': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-endpoints': 3.901.0 + '@aws-sdk/util-user-agent-browser': 3.907.0 + '@aws-sdk/util-user-agent-node': 3.907.0 + '@aws-sdk/xml-builder': 3.901.0 + '@smithy/config-resolver': 4.3.0 + '@smithy/core': 3.15.0 + '@smithy/eventstream-serde-browser': 4.2.0 + '@smithy/eventstream-serde-config-resolver': 4.3.0 + '@smithy/eventstream-serde-node': 4.2.0 + '@smithy/fetch-http-handler': 5.3.1 + '@smithy/hash-blob-browser': 4.2.1 + '@smithy/hash-node': 4.2.0 + '@smithy/hash-stream-node': 4.2.0 + '@smithy/invalid-dependency': 4.2.0 + '@smithy/md5-js': 4.2.0 + '@smithy/middleware-content-length': 4.2.0 + '@smithy/middleware-endpoint': 4.3.1 + '@smithy/middleware-retry': 4.4.1 + '@smithy/middleware-serde': 4.2.0 + '@smithy/middleware-stack': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/node-http-handler': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.0 + '@smithy/util-defaults-mode-node': 4.2.1 + '@smithy/util-endpoints': 3.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-retry': 4.2.0 + '@smithy/util-stream': 4.5.0 + '@smithy/util-utf8': 4.2.0 + '@smithy/util-waiter': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sesv2@3.940.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.940.0 + '@aws-sdk/credential-provider-node': 3.940.0 + '@aws-sdk/middleware-host-header': 3.936.0 + '@aws-sdk/middleware-logger': 3.936.0 + '@aws-sdk/middleware-recursion-detection': 3.936.0 + '@aws-sdk/middleware-user-agent': 3.940.0 + '@aws-sdk/region-config-resolver': 3.936.0 + '@aws-sdk/signature-v4-multi-region': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@aws-sdk/util-user-agent-browser': 3.936.0 + '@aws-sdk/util-user-agent-node': 3.940.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/core': 3.18.5 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/hash-node': 4.2.5 + '@smithy/invalid-dependency': 4.2.5 + '@smithy/middleware-content-length': 4.2.5 + '@smithy/middleware-endpoint': 4.3.12 + '@smithy/middleware-retry': 4.4.12 + '@smithy/middleware-serde': 4.2.6 + '@smithy/middleware-stack': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/node-http-handler': 4.4.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.8 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.11 + '@smithy/util-defaults-mode-node': 4.2.14 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.907.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.907.0 + '@aws-sdk/middleware-host-header': 3.901.0 + '@aws-sdk/middleware-logger': 3.901.0 + '@aws-sdk/middleware-recursion-detection': 3.901.0 + '@aws-sdk/middleware-user-agent': 3.907.0 + '@aws-sdk/region-config-resolver': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-endpoints': 3.901.0 + '@aws-sdk/util-user-agent-browser': 3.907.0 + '@aws-sdk/util-user-agent-node': 3.907.0 + '@smithy/config-resolver': 4.3.0 + '@smithy/core': 3.15.0 + '@smithy/fetch-http-handler': 5.3.1 + '@smithy/hash-node': 4.2.0 + '@smithy/invalid-dependency': 4.2.0 + '@smithy/middleware-content-length': 4.2.0 + '@smithy/middleware-endpoint': 4.3.1 + '@smithy/middleware-retry': 4.4.1 + '@smithy/middleware-serde': 4.2.0 + '@smithy/middleware-stack': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/node-http-handler': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.0 + '@smithy/util-defaults-mode-node': 4.2.1 + '@smithy/util-endpoints': 3.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-retry': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.940.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.940.0 + '@aws-sdk/middleware-host-header': 3.936.0 + '@aws-sdk/middleware-logger': 3.936.0 + '@aws-sdk/middleware-recursion-detection': 3.936.0 + '@aws-sdk/middleware-user-agent': 3.940.0 + '@aws-sdk/region-config-resolver': 3.936.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@aws-sdk/util-user-agent-browser': 3.936.0 + '@aws-sdk/util-user-agent-node': 3.940.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/core': 3.18.5 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/hash-node': 4.2.5 + '@smithy/invalid-dependency': 4.2.5 + '@smithy/middleware-content-length': 4.2.5 + '@smithy/middleware-endpoint': 4.3.12 + '@smithy/middleware-retry': 4.4.12 + '@smithy/middleware-serde': 4.2.6 + '@smithy/middleware-stack': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/node-http-handler': 4.4.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.8 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.11 + '@smithy/util-defaults-mode-node': 4.2.14 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.907.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@aws-sdk/xml-builder': 3.901.0 + '@smithy/core': 3.15.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/property-provider': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/signature-v4': 5.3.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.940.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@aws-sdk/xml-builder': 3.930.0 + '@smithy/core': 3.18.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/property-provider': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/signature-v4': 5.3.5 + '@smithy/smithy-client': 4.9.8 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.907.0': + dependencies: + '@aws-sdk/core': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.907.0': + dependencies: + '@aws-sdk/core': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/fetch-http-handler': 5.3.1 + '@smithy/node-http-handler': 4.3.0 + '@smithy/property-provider': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + '@smithy/util-stream': 4.5.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/node-http-handler': 4.4.5 + '@smithy/property-provider': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.8 + '@smithy/types': 4.9.0 + '@smithy/util-stream': 4.5.6 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.907.0': + dependencies: + '@aws-sdk/core': 3.907.0 + '@aws-sdk/credential-provider-env': 3.907.0 + '@aws-sdk/credential-provider-http': 3.907.0 + '@aws-sdk/credential-provider-process': 3.907.0 + '@aws-sdk/credential-provider-sso': 3.907.0 + '@aws-sdk/credential-provider-web-identity': 3.907.0 + '@aws-sdk/nested-clients': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/credential-provider-imds': 4.2.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-ini@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/credential-provider-env': 3.940.0 + '@aws-sdk/credential-provider-http': 3.940.0 + '@aws-sdk/credential-provider-login': 3.940.0 + '@aws-sdk/credential-provider-process': 3.940.0 + '@aws-sdk/credential-provider-sso': 3.940.0 + '@aws-sdk/credential-provider-web-identity': 3.940.0 + '@aws-sdk/nested-clients': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/credential-provider-imds': 4.2.5 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/nested-clients': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.907.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.907.0 + '@aws-sdk/credential-provider-http': 3.907.0 + '@aws-sdk/credential-provider-ini': 3.907.0 + '@aws-sdk/credential-provider-process': 3.907.0 + '@aws-sdk/credential-provider-sso': 3.907.0 + '@aws-sdk/credential-provider-web-identity': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/credential-provider-imds': 4.2.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.940.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.940.0 + '@aws-sdk/credential-provider-http': 3.940.0 + '@aws-sdk/credential-provider-ini': 3.940.0 + '@aws-sdk/credential-provider-process': 3.940.0 + '@aws-sdk/credential-provider-sso': 3.940.0 + '@aws-sdk/credential-provider-web-identity': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/credential-provider-imds': 4.2.5 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.907.0': + dependencies: + '@aws-sdk/core': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.907.0': + dependencies: + '@aws-sdk/client-sso': 3.907.0 + '@aws-sdk/core': 3.907.0 + '@aws-sdk/token-providers': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-sso@3.940.0': + dependencies: + '@aws-sdk/client-sso': 3.940.0 + '@aws-sdk/core': 3.940.0 + '@aws-sdk/token-providers': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.907.0': + dependencies: + '@aws-sdk/core': 3.907.0 + '@aws-sdk/nested-clients': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/nested-clients': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-arn-parser': 3.893.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-config-provider': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.907.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/is-array-buffer': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-stream': 4.5.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@aws/lambda-invoke-store': 0.0.1 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@aws/lambda-invoke-store': 0.2.1 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.907.0': + dependencies: + '@aws-sdk/core': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-arn-parser': 3.893.0 + '@smithy/core': 3.15.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/signature-v4': 5.3.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-stream': 4.5.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-arn-parser': 3.893.0 + '@smithy/core': 3.18.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/signature-v4': 5.3.5 + '@smithy/smithy-client': 4.9.8 + '@smithy/types': 4.9.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-stream': 4.5.6 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.907.0': + dependencies: + '@aws-sdk/core': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-endpoints': 3.901.0 + '@smithy/core': 3.15.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@smithy/core': 3.18.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.907.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.907.0 + '@aws-sdk/middleware-host-header': 3.901.0 + '@aws-sdk/middleware-logger': 3.901.0 + '@aws-sdk/middleware-recursion-detection': 3.901.0 + '@aws-sdk/middleware-user-agent': 3.907.0 + '@aws-sdk/region-config-resolver': 3.901.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-endpoints': 3.901.0 + '@aws-sdk/util-user-agent-browser': 3.907.0 + '@aws-sdk/util-user-agent-node': 3.907.0 + '@smithy/config-resolver': 4.3.0 + '@smithy/core': 3.15.0 + '@smithy/fetch-http-handler': 5.3.1 + '@smithy/hash-node': 4.2.0 + '@smithy/invalid-dependency': 4.2.0 + '@smithy/middleware-content-length': 4.2.0 + '@smithy/middleware-endpoint': 4.3.1 + '@smithy/middleware-retry': 4.4.1 + '@smithy/middleware-serde': 4.2.0 + '@smithy/middleware-stack': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/node-http-handler': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.0 + '@smithy/util-defaults-mode-node': 4.2.1 + '@smithy/util-endpoints': 3.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-retry': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/nested-clients@3.940.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.940.0 + '@aws-sdk/middleware-host-header': 3.936.0 + '@aws-sdk/middleware-logger': 3.936.0 + '@aws-sdk/middleware-recursion-detection': 3.936.0 + '@aws-sdk/middleware-user-agent': 3.940.0 + '@aws-sdk/region-config-resolver': 3.936.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@aws-sdk/util-user-agent-browser': 3.936.0 + '@aws-sdk/util-user-agent-node': 3.940.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/core': 3.18.5 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/hash-node': 4.2.5 + '@smithy/invalid-dependency': 4.2.5 + '@smithy/middleware-content-length': 4.2.5 + '@smithy/middleware-endpoint': 4.3.12 + '@smithy/middleware-retry': 4.4.12 + '@smithy/middleware-serde': 4.2.6 + '@smithy/middleware-stack': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/node-http-handler': 4.4.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.8 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.11 + '@smithy/util-defaults-mode-node': 4.2.14 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/region-config-resolver@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.907.0': + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@aws-sdk/util-format-url': 3.901.0 + '@smithy/middleware-endpoint': 4.3.1 + '@smithy/protocol-http': 5.3.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.907.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/signature-v4': 5.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.940.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/protocol-http': 5.3.5 + '@smithy/signature-v4': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.907.0': + dependencies: + '@aws-sdk/core': 3.907.0 + '@aws-sdk/nested-clients': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/token-providers@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/nested-clients': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.901.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.936.0': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.893.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-endpoints': 3.2.0 + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-endpoints': 3.2.5 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.901.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/querystring-builder': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.893.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.907.0': + dependencies: + '@aws-sdk/types': 3.901.0 + '@smithy/types': 4.6.0 + bowser: 2.12.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + bowser: 2.12.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.907.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.907.0 + '@aws-sdk/types': 3.901.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.940.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.901.0': + dependencies: + '@smithy/types': 4.6.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.930.0': + dependencies: + '@smithy/types': 4.9.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.0.1': {} + + '@aws/lambda-invoke-store@0.2.1': {} + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.28.4': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@braintree/sanitize-url@7.1.1': {} + + '@builder.io/jsx-loc-internals@0.0.1': + dependencies: + '@babel/parser': 7.28.4 + estree-walker: 2.0.2 + magic-string: 0.30.19 + + '@builder.io/vite-plugin-jsx-loc@0.1.1(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))': + dependencies: + '@builder.io/jsx-loc-internals': 0.0.1 + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + + '@date-fns/tz@1.4.1': {} + + '@drizzle-team/brocli@0.10.2': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.10.1 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@fast-csv/format@4.3.5': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + + '@fast-csv/parse@4.3.6': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@floating-ui/utils@0.2.10': {} + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.0.2': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@antfu/utils': 9.3.0 + '@iconify/types': 2.0.0 + debug: 4.4.3 + globals: 15.15.0 + kolorist: 1.8.0 + local-pkg: 1.1.2 + mlly: 1.8.0 + transitivePeerDependencies: + - supports-color + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@medv/finder@4.0.2': {} + + '@mermaid-js/parser@0.6.3': + dependencies: + langium: 3.3.1 + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accordion@1.2.12(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@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) + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-alert-dialog@1.1.15(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dialog': 1.1.15(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-arrow@1.1.7(@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)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-aspect-ratio@1.1.7(@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)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-avatar@1.1.10(@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)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-checkbox@1.3.3(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-collapsible@1.1.12(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-collection@1.1.7(@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)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-context-menu@2.2.16(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-menu': 2.1.16(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-dialog@1.1.15(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@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) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-focus-scope': 1.1.7(@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) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-portal': 1.1.9(@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) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + aria-hidden: 1.2.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-dismissable-layer@1.1.11(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-dropdown-menu@2.1.16(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-menu': 2.1.16(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-focus-scope@1.1.7(@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)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-hover-card@1.1.15(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@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) + '@radix-ui/react-popper': 1.2.8(@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) + '@radix-ui/react-portal': 1.1.9(@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) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-label@2.1.7(@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)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-menu@2.1.16(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@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) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-focus-scope': 1.1.7(@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) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-popper': 1.2.8(@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) + '@radix-ui/react-portal': 1.1.9(@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) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-roving-focus': 1.1.11(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + aria-hidden: 1.2.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-menubar@1.1.16(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-menu': 2.1.16(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-roving-focus': 1.1.11(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-navigation-menu@1.2.14(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@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) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-visually-hidden': 1.2.3(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-popover@1.1.15(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@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) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-focus-scope': 1.1.7(@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) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-popper': 1.2.8(@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) + '@radix-ui/react-portal': 1.1.9(@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) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + aria-hidden: 1.2.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-popper@1.2.8(@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)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-arrow': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/rect': 1.1.1 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-portal@1.1.9(@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)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-presence@1.1.5(@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)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-primitive@2.1.3(@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)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-progress@1.1.7(@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)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-radio-group@1.3.8(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-roving-focus': 1.1.11(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-roving-focus@1.1.11(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-scroll-area@1.2.10(@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)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-select@2.2.6(@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)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@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) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-focus-scope': 1.1.7(@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) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-popper': 1.2.8(@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) + '@radix-ui/react-portal': 1.1.9(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-visually-hidden': 1.2.3(@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) + aria-hidden: 1.2.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-separator@1.1.7(@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)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-slider@1.3.6(@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)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-switch@1.2.6(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-tabs@1.1.13(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-roving-focus': 1.1.11(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-toggle-group@1.1.11(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-roving-focus': 1.1.11(@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) + '@radix-ui/react-toggle': 1.1.10(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-toggle@1.1.10(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-tooltip@1.2.8(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@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) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-popper': 1.2.8(@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) + '@radix-ui/react-portal': 1.1.9(@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) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-visually-hidden': 1.2.3(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + + '@radix-ui/react-visually-hidden@1.2.3(@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)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + + '@radix-ui/rect@1.1.1': {} + + '@remirror/core-constants@3.0.0': {} + + '@rolldown/pluginutils@1.0.0-beta.38': {} + + '@rollup/rollup-android-arm-eabi@4.52.4': + optional: true + + '@rollup/rollup-android-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-x64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.4': + optional: true + + '@shikijs/core@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + + '@shikijs/engine-oniguruma@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + + '@shikijs/themes@3.14.0': + dependencies: + '@shikijs/types': 3.14.0 + + '@shikijs/types@3.14.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@smithy/abort-controller@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/abort-controller@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.2.1': + dependencies: + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.3.0': + dependencies: + '@smithy/node-config-provider': 4.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.0 + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.3': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + tslib: 2.8.1 + + '@smithy/core@3.15.0': + dependencies: + '@smithy/middleware-serde': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-stream': 4.5.0 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/core@3.18.5': + dependencies: + '@smithy/middleware-serde': 4.2.6 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-stream': 4.5.6 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.0': + dependencies: + '@smithy/node-config-provider': 4.3.0 + '@smithy/property-provider': 4.2.0 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.5': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/property-provider': 4.2.5 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.6.0 + '@smithy/util-hex-encoding': 4.2.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.0': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.0': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.0': + dependencies: + '@smithy/eventstream-codec': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.1': + dependencies: + '@smithy/protocol-http': 5.3.0 + '@smithy/querystring-builder': 4.2.0 + '@smithy/types': 4.6.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.6': + dependencies: + '@smithy/protocol-http': 5.3.5 + '@smithy/querystring-builder': 4.2.5 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.2.1': + dependencies: + '@smithy/chunked-blob-reader': 5.2.0 + '@smithy/chunked-blob-reader-native': 4.2.1 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.0': + dependencies: + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.5': + dependencies: + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.3.1': + dependencies: + '@smithy/core': 3.15.0 + '@smithy/middleware-serde': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + '@smithy/url-parser': 4.2.0 + '@smithy/util-middleware': 4.2.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.3.12': + dependencies: + '@smithy/core': 3.18.5 + '@smithy/middleware-serde': 4.2.6 + '@smithy/node-config-provider': 4.3.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-middleware': 4.2.5 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.1': + dependencies: + '@smithy/node-config-provider': 4.3.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/service-error-classification': 4.2.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-retry': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.12': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/service-error-classification': 4.2.5 + '@smithy/smithy-client': 4.9.8 + '@smithy/types': 4.9.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.0': + dependencies: + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.6': + dependencies: + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.0': + dependencies: + '@smithy/property-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.5': + dependencies: + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.3.0': + dependencies: + '@smithy/abort-controller': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/querystring-builder': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.4.5': + dependencies: + '@smithy/abort-controller': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/querystring-builder': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + + '@smithy/service-error-classification@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + + '@smithy/shared-ini-file-loader@4.3.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/shared-ini-file-loader@4.4.0': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.0 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.5': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.7.1': + dependencies: + '@smithy/core': 3.15.0 + '@smithy/middleware-endpoint': 4.3.1 + '@smithy/middleware-stack': 4.2.0 + '@smithy/protocol-http': 5.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-stream': 4.5.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.9.8': + dependencies: + '@smithy/core': 3.18.5 + '@smithy/middleware-endpoint': 4.3.12 + '@smithy/middleware-stack': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-stream': 4.5.6 + tslib: 2.8.1 + + '@smithy/types@4.6.0': + dependencies: + tslib: 2.8.1 + + '@smithy/types@4.9.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.0': + dependencies: + '@smithy/querystring-parser': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/url-parser@4.2.5': + dependencies: + '@smithy/querystring-parser': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.0': + dependencies: + '@smithy/property-provider': 4.2.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.11': + dependencies: + '@smithy/property-provider': 4.2.5 + '@smithy/smithy-client': 4.9.8 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.1': + dependencies: + '@smithy/config-resolver': 4.3.0 + '@smithy/credential-provider-imds': 4.2.0 + '@smithy/node-config-provider': 4.3.0 + '@smithy/property-provider': 4.2.0 + '@smithy/smithy-client': 4.7.1 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.14': + dependencies: + '@smithy/config-resolver': 4.4.3 + '@smithy/credential-provider-imds': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/property-provider': 4.2.5 + '@smithy/smithy-client': 4.9.8 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.0': + dependencies: + '@smithy/node-config-provider': 4.3.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.5': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.0': + dependencies: + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.0': + dependencies: + '@smithy/service-error-classification': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.5': + dependencies: + '@smithy/service-error-classification': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.0': + dependencies: + '@smithy/fetch-http-handler': 5.3.1 + '@smithy/node-http-handler': 4.3.0 + '@smithy/types': 4.6.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.6': + dependencies: + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/node-http-handler': 4.4.5 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.0': + dependencies: + '@smithy/abort-controller': 4.2.0 + '@smithy/types': 4.6.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.0': + 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 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + magic-string: 0.30.19 + source-map-js: 1.2.1 + tailwindcss: 4.1.14 + + '@tailwindcss/oxide-android-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide@4.1.14': + dependencies: + detect-libc: 2.1.2 + tar: 7.5.1 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-x64': 4.1.14 + '@tailwindcss/oxide-freebsd-x64': 4.1.14 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.14 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.14 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-x64-musl': 4.1.14 + '@tailwindcss/oxide-wasm32-wasi': 4.1.14 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.14 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.14 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.14)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.14 + + '@tailwindcss/vite@4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))': + dependencies: + '@tailwindcss/node': 4.1.14 + '@tailwindcss/oxide': 4.1.14 + tailwindcss: 4.1.14 + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) + + '@tanstack/query-core@5.90.2': {} + + '@tanstack/react-query@5.90.2(react@19.2.0)': + dependencies: + '@tanstack/query-core': 5.90.2 + react: 19.2.0 + + '@tiptap/core@3.11.0(@tiptap/pm@3.11.0)': + dependencies: + '@tiptap/pm': 3.11.0 + + '@tiptap/extension-blockquote@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-bold@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-bubble-menu@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/pm': 3.11.0 + optional: true + + '@tiptap/extension-bullet-list@3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + + '@tiptap/extension-code-block@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/pm': 3.11.0 + + '@tiptap/extension-code@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-color@3.11.0(@tiptap/extension-text-style@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)))': + dependencies: + '@tiptap/extension-text-style': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + + '@tiptap/extension-document@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-dropcursor@3.11.0(@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/extensions': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + + '@tiptap/extension-floating-menu@3.11.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/pm': 3.11.0 + optional: true + + '@tiptap/extension-gapcursor@3.11.0(@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/extensions': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + + '@tiptap/extension-hard-break@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-heading@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-horizontal-rule@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/pm': 3.11.0 + + '@tiptap/extension-italic@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-link@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/pm': 3.11.0 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + + '@tiptap/extension-list-keymap@3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + + '@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/pm': 3.11.0 + + '@tiptap/extension-ordered-list@3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + + '@tiptap/extension-paragraph@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-strike@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-text-style@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-text@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extension-underline@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + + '@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/pm': 3.11.0 + + '@tiptap/pm@3.11.0': + dependencies: + prosemirror-changeset: 2.3.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.0 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.2 + prosemirror-menu: 1.2.5 + prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.1 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.3 + + '@tiptap/react@3.11.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)(@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)': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/pm': 3.11.0 + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + '@types/use-sync-external-store': 0.0.6 + fast-deep-equal: 3.1.3 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-floating-menu': 3.11.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.11.0': + dependencies: + '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/extension-blockquote': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-bold': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-bullet-list': 3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) + '@tiptap/extension-code': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-code-block': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-document': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-dropcursor': 3.11.0(@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) + '@tiptap/extension-gapcursor': 3.11.0(@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) + '@tiptap/extension-hard-break': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-heading': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-horizontal-rule': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-italic': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-link': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-list-item': 3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) + '@tiptap/extension-list-keymap': 3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) + '@tiptap/extension-ordered-list': 3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) + '@tiptap/extension-paragraph': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-strike': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-text': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extension-underline': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + '@tiptap/extensions': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/pm': 3.11.0 + + '@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@trpc/server': 11.6.0(typescript@5.9.3) + typescript: 5.9.3 + + '@trpc/react-query@11.6.0(@tanstack/react-query@5.90.2(react@19.2.0))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)': + dependencies: + '@tanstack/react-query': 5.90.2(react@19.2.0) + '@trpc/client': 11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3) + '@trpc/server': 11.6.0(typescript@5.9.3) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + typescript: 5.9.3 + + '@trpc/server@11.6.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/bcryptjs@3.0.0': + dependencies: + bcryptjs: 3.0.3 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.7.0 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.7.0 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 24.7.0 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.0 + + '@types/express@4.17.21': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.9 + + '@types/geojson@7946.0.16': {} + + '@types/google.maps@3.58.1': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 24.7.0 + + '@types/katex@0.16.7': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/mime@1.3.5': {} + + '@types/ms@2.1.0': {} + + '@types/multer@2.0.0': + dependencies: + '@types/express': 4.17.21 + + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 24.7.0 + form-data: 4.0.4 + + '@types/node@14.18.63': {} + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@24.7.0': + dependencies: + undici-types: 7.14.0 + + '@types/nodemailer@7.0.4': + dependencies: + '@aws-sdk/client-sesv2': 3.940.0 + '@types/node': 24.7.0 + transitivePeerDependencies: + - aws-crt + + '@types/pako@2.0.4': {} + + '@types/pdfkit@0.17.4': + dependencies: + '@types/node': 24.7.0 + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 24.7.0 + + '@types/qs@6.14.0': {} + + '@types/raf@3.4.3': + optional: true + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.2.1(@types/react@19.2.2)': + dependencies: + '@types/react': 19.2.2 + + '@types/react@19.2.2': + dependencies: + csstype: 3.1.3 + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 24.7.0 + + '@types/send@1.2.0': + dependencies: + '@types/node': 24.7.0 + + '@types/serve-static@1.15.9': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.7.0 + '@types/send': 0.17.5 + + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-react@5.0.4(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.38 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.20(@types/node@24.7.0)(lightningcss@1.30.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 5.4.20(@types/node@24.7.0)(lightningcss@1.30.1) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.19 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn@8.15.0: {} + + add@2.0.6: {} + + adler-32@1.3.1: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + append-field@1.0.0: {} + + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + array-flatten@1.1.1: {} + + assertion-error@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.26.3 + caniuse-lite: 1.0.30001748 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + aws-ssl-profiles@1.1.2: {} + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + base64-arraybuffer@1.0.2: + optional: true + + base64-js@0.0.8: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.8.12: {} + + bcryptjs@3.0.3: {} + + big-integer@1.6.52: {} + + binary@0.3.0: + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird@3.4.7: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bowser@2.12.1: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + 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 + caniuse-lite: 1.0.30001748 + electron-to-chromium: 1.5.230 + node-releases: 2.0.23 + update-browserslist-db: 1.1.3(browserslist@4.26.3) + + buffer-crc32@0.2.13: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer-indexof-polyfill@1.0.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffers@0.1.1: {} + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001748: {} + + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.28.4 + '@types/raf': 3.4.3 + core-js: 3.46.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + + ccount@2.0.1: {} + + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chainsaw@0.1.0: + dependencies: + traverse: 0.3.9 + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + check-error@2.1.1: {} + + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.21 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + + chownr@3.0.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + 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): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dialog': 1.1.15(@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) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + codepage@1.15.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + cookie@1.0.2: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + core-js@3.46.0: + optional: true + + core-util-is@1.0.3: {} + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + crelt@1.0.6: {} + + crypto-js@4.2.0: {} + + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.1 + + cytoscape-fcose@2.2.0(cytoscape@3.33.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.1 + + cytoscape@3.33.1: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.11: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.21 + + date-fns-jalali@4.1.0-0: {} + + date-fns@4.1.0: {} + + dayjs@1.11.18: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decimal.js-light@2.5.1: {} + + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + + deep-eql@5.0.2: {} + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dfa@1.2.0: {} + + dijkstrajs@1.0.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.4 + csstype: 3.1.3 + + dompurify@3.3.0: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dotenv@17.2.3: {} + + drizzle-kit@0.31.5: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.10 + esbuild-register: 3.6.0(esbuild@0.25.10) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.44.6(mysql2@3.15.1): + optionalDependencies: + mysql2: 3.15.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.230: {} + + embla-carousel-react@8.6.0(react@19.2.0): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.2.0 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + + emoji-regex@8.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + entities@6.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild-register@3.6.0(esbuild@0.25.10): + dependencies: + debug: 4.4.3 + esbuild: 0.25.10 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + + exceljs@4.4.0: + dependencies: + archiver: 5.3.2 + dayjs: 1.11.18 + fast-csv: 4.3.6 + jszip: 3.10.1 + readable-stream: 3.6.2 + saxes: 5.0.1 + tmp: 0.2.5 + unzipper: 0.10.14 + uuid: 8.3.2 + + expect-type@1.2.2: {} + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.7: {} + + extend@3.0.2: {} + + fast-csv@4.3.6: + dependencies: + '@fast-csv/format': 4.3.5 + '@fast-csv/parse': 4.3.6 + + fast-deep-equal@3.1.3: {} + + fast-equals@5.3.2: {} + + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.1 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fflate@0.8.2: {} + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + 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: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + forwarded@0.2.0: {} + + frac@1.1.2: {} + + fraction.js@4.3.7: {} + + framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + motion-dom: 12.23.21 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + fresh@0.5.2: {} + + fs-constants@1.0.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + function-bind@1.1.2: {} + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@15.15.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + hachure-fill@0.5.2: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-from-dom@5.0.1: + dependencies: + '@types/hast': 3.0.4 + hastscript: 9.0.1 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.1 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.18 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + html-url-attributes@3.0.1: {} + + html-void-elements@3.0.0: {} + + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + immediate@3.0.6: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + inline-style-parser@0.2.4: {} + + input-otp@1.4.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + iobuffer@5.4.0: {} + + ipaddr.js@1.9.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-hexadecimal@2.0.1: {} + + is-plain-obj@4.1.0: {} + + is-property@1.0.2: {} + + is-what@4.1.16: {} + + isarray@1.0.0: {} + + jiti@2.6.1: {} + + jose@6.1.0: {} + + jpeg-exif@1.1.4: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + + jspdf-autotable@5.0.2(jspdf@3.0.3): + dependencies: + jspdf: 3.0.3 + + jspdf@3.0.3: + dependencies: + '@babel/runtime': 7.28.4 + fast-png: 6.4.0 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.46.0 + dompurify: 3.3.0 + html2canvas: 1.4.1 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + + katex@0.16.25: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + kolorist@1.8.0: {} + + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + 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 + + linkifyjs@4.3.2: {} + + listenercount@1.0.1: {} + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash-es@4.17.21: {} + + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + + lodash.groupby@4.6.0: {} + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isequal@4.5.0: {} + + lodash.isfunction@3.0.9: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnil@4.0.0: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.isundefined@3.0.1: {} + + lodash.once@4.1.1: {} + + lodash.union@4.6.0: {} + + lodash.uniq@4.5.0: {} + + lodash@4.17.21: {} + + long@5.3.2: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@7.18.3: {} + + lru.min@1.1.2: {} + + lucide-react@0.453.0(react@19.2.0): + dependencies: + react: 19.2.0 + + lucide-react@0.542.0(react@19.2.0): + dependencies: + react: 19.2.0 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdown-table@3.0.4: {} + + marked@16.4.1: {} + + math-intrinsics@1.1.0: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdurl@2.0.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + mermaid@11.12.0: + dependencies: + '@braintree/sanitize-url': 7.1.1 + '@iconify/utils': 3.0.2 + '@mermaid-js/parser': 0.6.3 + '@types/d3': 7.4.3 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.11 + dayjs: 1.11.18 + dompurify: 3.3.0 + katex: 0.16.25 + khroma: 2.1.0 + lodash-es: 4.17.21 + marked: 16.4.1 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + + methods@1.1.2: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.7 + devlop: 1.1.0 + katex: 0.16.25 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + mitt@3.0.1: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + modern-screenshot@4.6.6: {} + + motion-dom@12.23.21: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multer@2.0.2: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + + mysql2@3.15.1: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.0 + long: 5.3.2 + lru.min: 1.1.2 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + + nanoid@3.3.11: {} + + nanoid@5.1.6: {} + + negotiator@0.6.3: {} + + next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-releases@2.0.23: {} + + nodemailer@7.0.11: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.3: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.0.1 + regex-recursion: 6.0.2 + + openai@4.104.0(zod@4.1.12): + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + zod: 4.1.12 + transitivePeerDependencies: + - encoding + + orderedmap@2.1.1: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + package-manager-detector@1.5.0: {} + + pako@0.2.9: {} + + pako@1.0.11: {} + + pako@2.1.0: {} + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parseurl@1.3.3: {} + + path-data-parser@0.1.0: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-to-regexp@0.1.12: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + 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 + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + png-js@1.0.0: {} + + pngjs@5.0.0: {} + + pnpm@10.18.0: {} + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@3.6.2: {} + + process-nextick-args@2.0.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@6.5.0: {} + + property-information@7.1.0: {} + + prosemirror-changeset@2.3.1: + dependencies: + prosemirror-transform: 1.10.5 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.3 + + prosemirror-gapcursor@1.4.0: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.3 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.3 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.2: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.2.5: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.3 + + prosemirror-tables@1.8.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.3 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.3 + + prosemirror-transform@1.10.5: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.3: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + punycode.js@2.3.1: {} + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.11: {} + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + react-day-picker@9.11.1(react@19.2.0): + dependencies: + '@date-fns/tz': 1.4.1 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 + react: 19.2.0 + + react-dom@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + + react-hook-form@7.64.0(react@19.2.0): + dependencies: + react: 19.2.0 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-markdown@10.1.0(@types/react@19.2.2)(react@19.2.0): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.2 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.2.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.2)(react@19.2.0): + dependencies: + react: 19.2.0 + react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.2 + + react-remove-scroll@2.7.1(@types/react@19.2.2)(react@19.2.0): + dependencies: + react: 19.2.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.2)(react@19.2.0) + react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.2)(react@19.2.0) + use-sidecar: 1.1.3(@types/react@19.2.2)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + + react-resizable-panels@3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + react-smooth@4.0.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + fast-equals: 5.3.2 + prop-types: 15.8.1 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + + react-style-singleton@2.2.3(@types/react@19.2.2)(react@19.2.0): + dependencies: + get-nonce: 1.0.1 + react: 19.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.2 + + react-transition-group@4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@babel/runtime': 7.28.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + react@19.2.0: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + regenerator-runtime@0.13.11: + optional: true + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + + regexparam@3.0.0: {} + + rehype-harden@1.1.5: {} + + rehype-katex@7.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.7 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 + katex: 0.16.25 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + restructure@3.0.2: {} + + rgbcolor@1.0.1: + optional: true + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + robust-predicates@3.0.2: {} + + rollup@4.52.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.4 + '@rollup/rollup-android-arm64': 4.52.4 + '@rollup/rollup-darwin-arm64': 4.52.4 + '@rollup/rollup-darwin-x64': 4.52.4 + '@rollup/rollup-freebsd-arm64': 4.52.4 + '@rollup/rollup-freebsd-x64': 4.52.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 + '@rollup/rollup-linux-arm-musleabihf': 4.52.4 + '@rollup/rollup-linux-arm64-gnu': 4.52.4 + '@rollup/rollup-linux-arm64-musl': 4.52.4 + '@rollup/rollup-linux-loong64-gnu': 4.52.4 + '@rollup/rollup-linux-ppc64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-musl': 4.52.4 + '@rollup/rollup-linux-s390x-gnu': 4.52.4 + '@rollup/rollup-linux-x64-gnu': 4.52.4 + '@rollup/rollup-linux-x64-musl': 4.52.4 + '@rollup/rollup-openharmony-arm64': 4.52.4 + '@rollup/rollup-win32-arm64-msvc': 4.52.4 + '@rollup/rollup-win32-ia32-msvc': 4.52.4 + '@rollup/rollup-win32-x64-gnu': 4.52.4 + '@rollup/rollup-win32-x64-msvc': 4.52.4 + fsevents: 2.3.3 + + rope-sequence@1.3.4: {} + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.3: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + seq-queue@0.0.5: {} + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + shiki@3.14.0: + dependencies: + '@shikijs/core': 3.14.0 + '@shikijs/engine-javascript': 3.14.0 + '@shikijs/engine-oniguruma': 3.14.0 + '@shikijs/langs': 3.14.0 + '@shikijs/themes': 3.14.0 + '@shikijs/types': 3.14.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + space-separated-tokens@2.0.2: {} + + sqlstring@2.3.3: {} + + ssf@0.11.2: + dependencies: + frac: 1.1.2 + + stackback@0.0.2: {} + + stackblur-canvas@2.7.0: + optional: true + + statuses@2.0.1: {} + + std-env@3.9.0: {} + + streamdown@1.4.0(@types/react@19.2.2)(react@19.2.0): + dependencies: + clsx: 2.1.1 + katex: 0.16.25 + lucide-react: 0.542.0(react@19.2.0) + marked: 16.4.1 + mermaid: 11.12.0 + react: 19.2.0 + react-markdown: 10.1.0(@types/react@19.2.2)(react@19.2.0) + rehype-harden: 1.1.5 + rehype-katex: 7.0.1 + rehype-raw: 7.0.0 + remark-gfm: 4.0.1 + remark-math: 6.0.0 + shiki: 3.14.0 + tailwind-merge: 3.3.1 + transitivePeerDependencies: + - '@types/react' + - supports-color + + streamsearch@1.1.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strnum@2.1.1: {} + + style-to-js@1.1.18: + dependencies: + style-to-object: 1.0.11 + + style-to-object@1.0.11: + dependencies: + inline-style-parser: 0.2.4 + + stylis@4.3.6: {} + + superjson@1.13.3: + dependencies: + copy-anything: 3.0.5 + + svg-pathdata@6.0.3: + optional: true + + tailwind-merge@3.3.1: {} + + tailwindcss-animate@1.0.7(tailwindcss@4.1.14): + dependencies: + tailwindcss: 4.1.14 + + tailwindcss@4.1.14: {} + + tapable@2.3.0: {} + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@7.5.1: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + + tiny-inflate@1.0.3: {} + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.0.1: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tmp@0.2.5: {} + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + traverse@0.3.9: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-dedent@2.2.0: {} + + tslib@2.8.1: {} + + tsx@4.20.6: + dependencies: + esbuild: 0.25.10 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + tw-animate-css@1.4.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typedarray@0.0.6: {} + + typescript@5.9.3: {} + + uc.micro@2.1.0: {} + + ufo@1.6.1: {} + + undici-types@5.26.5: {} + + 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 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unpipe@1.0.0: {} + + unzipper@0.10.14: + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + update-browserslist-db@1.1.3(browserslist@4.26.3): + dependencies: + browserslist: 4.26.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@19.2.2)(react@19.2.0): + dependencies: + react: 19.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.2 + + use-sidecar@1.1.3(@types/react@19.2.2)(react@19.2.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.2 + + use-sync-external-store@1.6.0(react@19.2.0): + dependencies: + react: 19.2.0 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + + uuid@11.1.0: {} + + uuid@8.3.2: {} + + vary@1.1.2: {} + + vaul@1.1.2(@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): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@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) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite-node@2.1.9(@types/node@24.7.0)(lightningcss@1.30.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.20(@types/node@24.7.0)(lightningcss@1.30.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-manus-runtime@0.0.56: + dependencies: + '@medv/finder': 4.0.2 + clsx: 2.1.1 + modern-screenshot: 4.6.6 + nanoid: 5.1.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + tailwind-merge: 3.3.1 + + vite@5.4.20(@types/node@24.7.0)(lightningcss@1.30.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.4 + optionalDependencies: + '@types/node': 24.7.0 + fsevents: 2.3.3 + lightningcss: 1.30.1 + + vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.4 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.7.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + tsx: 4.20.6 + + vitest@2.1.9(@types/node@24.7.0)(lightningcss@1.30.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.20(@types/node@24.7.0)(lightningcss@1.30.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.20(@types/node@24.7.0)(lightningcss@1.30.1) + vite-node: 2.1.9(@types/node@24.7.0)(lightningcss@1.30.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.7.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + + w3c-keyname@2.2.8: {} + + web-namespaces@2.0.1: {} + + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-module@2.0.1: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wmf@1.0.2: {} + + word@0.3.0: {} + + wouter@3.7.1(patch_hash=4e16e6ff3fde7d6c1024d3e0c8605dc9eb6afb690d0d49958c2f449091813072)(react@19.2.0): + dependencies: + mitt: 3.0.1 + react: 19.2.0 + regexparam: 3.0.0 + use-sync-external-store: 1.6.0(react@19.2.0) + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + + xmlchars@2.2.0: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + + zod@4.1.12: {} + + zwitch@2.0.4: {} diff --git a/deployment-package/source_server/__tests__/localAuth.test.ts b/deployment-package/source_server/__tests__/localAuth.test.ts new file mode 100644 index 0000000..4385110 --- /dev/null +++ b/deployment-package/source_server/__tests__/localAuth.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { getDb } from "../db"; +import { users } from "../../drizzle/schema"; +import { eq } from "drizzle-orm"; + +describe("Local Authentication", () => { + beforeAll(async () => { + // Vérifier que la base de données est accessible + const db = await getDb(); + expect(db).toBeDefined(); + }); + + it("should have an admin user with username adminServFormation", async () => { + const db = await getDb(); + if (!db) { + throw new Error("Database not available"); + } + + const result = await db + .select() + .from(users) + .where(eq(users.username, "adminServFormation")) + .limit(1); + + expect(result.length).toBe(1); + expect(result[0].username).toBe("adminServFormation"); + expect(result[0].role).toBe("admin"); + expect(result[0].password).toBeDefined(); + expect(result[0].password).not.toBeNull(); + }); + + it("should authenticate with correct credentials", async () => { + const response = await fetch("http://localhost:3000/api/auth/local/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "adminServFormation", + password: "Itinova69!", + }), + }); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.success).toBe(true); + expect(data.user).toBeDefined(); + expect(data.user.username).toBe("adminServFormation"); + expect(data.user.role).toBe("admin"); + expect(data.user.password).toBeUndefined(); // Le mot de passe ne doit pas être retourné + }); + + it("should reject authentication with incorrect password", async () => { + const response = await fetch("http://localhost:3000/api/auth/local/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "adminServFormation", + password: "wrongpassword", + }), + }); + + expect(response.status).toBe(401); + const data = await response.json(); + expect(data.success).toBe(false); + expect(data.message).toContain("incorrect"); + }); + + it("should reject authentication with non-existent username", async () => { + const response = await fetch("http://localhost:3000/api/auth/local/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "nonexistentuser", + password: "anypassword", + }), + }); + + expect(response.status).toBe(401); + const data = await response.json(); + expect(data.success).toBe(false); + expect(data.message).toContain("incorrect"); + }); + + it("should reject authentication with missing credentials", async () => { + const response = await fetch("http://localhost:3000/api/auth/local/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "adminServFormation", + // password manquant + }), + }); + + expect(response.status).toBe(400); + const data = await response.json(); + expect(data.success).toBe(false); + expect(data.message).toContain("requis"); + }); +}); diff --git a/deployment-package/source_server/_core/appUrl.ts b/deployment-package/source_server/_core/appUrl.ts new file mode 100644 index 0000000..5bc793a --- /dev/null +++ b/deployment-package/source_server/_core/appUrl.ts @@ -0,0 +1,32 @@ +/** + * Récupère l'URL de base de l'application + * En production, utilise l'URL publique configurée + * En développement, utilise localhost + */ +export function getAppBaseUrl(): string { + // Si une URL publique est configurée, l'utiliser + if (process.env.APP_PUBLIC_URL) { + return process.env.APP_PUBLIC_URL; + } + + // En production, essayer de détecter l'URL depuis les variables d'environnement Manus + if (process.env.NODE_ENV === "production") { + // Pour les applications Manus déployées, l'URL est généralement disponible + // via les variables d'environnement du système + const port = process.env.PORT || 3000; + + // Si on est sur un serveur VPS avec un domaine configuré + if (process.env.DOMAIN_NAME) { + return `https://${process.env.DOMAIN_NAME}`; + } + + // Sinon, utiliser l'URL du serveur si disponible + if (process.env.SERVER_URL) { + return process.env.SERVER_URL; + } + } + + // Par défaut, utiliser localhost (développement) + const port = process.env.PORT || 3000; + return `http://localhost:${port}`; +} diff --git a/deployment-package/source_server/_core/context.ts b/deployment-package/source_server/_core/context.ts new file mode 100644 index 0000000..e4ae108 --- /dev/null +++ b/deployment-package/source_server/_core/context.ts @@ -0,0 +1,28 @@ +import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"; +import type { User } from "../../drizzle/schema"; +import { sdk } from "./sdk"; + +export type TrpcContext = { + req: CreateExpressContextOptions["req"]; + res: CreateExpressContextOptions["res"]; + user: User | null; +}; + +export async function createContext( + opts: CreateExpressContextOptions +): Promise { + let user: User | null = null; + + try { + user = await sdk.authenticateRequest(opts.req); + } catch (error) { + // Authentication is optional for public procedures. + user = null; + } + + return { + req: opts.req, + res: opts.res, + user, + }; +} diff --git a/deployment-package/source_server/_core/cookies.ts b/deployment-package/source_server/_core/cookies.ts new file mode 100644 index 0000000..664b6de --- /dev/null +++ b/deployment-package/source_server/_core/cookies.ts @@ -0,0 +1,48 @@ +import type { CookieOptions, Request } from "express"; + +const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]); + +function isIpAddress(host: string) { + // Basic IPv4 check and IPv6 presence detection. + if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true; + return host.includes(":"); +} + +function isSecureRequest(req: Request) { + if (req.protocol === "https") return true; + + const forwardedProto = req.headers["x-forwarded-proto"]; + if (!forwardedProto) return false; + + const protoList = Array.isArray(forwardedProto) + ? forwardedProto + : forwardedProto.split(","); + + return protoList.some(proto => proto.trim().toLowerCase() === "https"); +} + +export function getSessionCookieOptions( + req: Request +): Pick { + // const hostname = req.hostname; + // const shouldSetDomain = + // hostname && + // !LOCAL_HOSTS.has(hostname) && + // !isIpAddress(hostname) && + // hostname !== "127.0.0.1" && + // hostname !== "::1"; + + // const domain = + // shouldSetDomain && !hostname.startsWith(".") + // ? `.${hostname}` + // : shouldSetDomain + // ? hostname + // : undefined; + + return { + httpOnly: true, + path: "/", + sameSite: "lax", + secure: isSecureRequest(req), + }; +} diff --git a/deployment-package/source_server/_core/dataApi.ts b/deployment-package/source_server/_core/dataApi.ts new file mode 100644 index 0000000..7ef81b5 --- /dev/null +++ b/deployment-package/source_server/_core/dataApi.ts @@ -0,0 +1,64 @@ +/** + * Quick example (matches curl usage): + * await callDataApi("Youtube/search", { + * query: { gl: "US", hl: "en", q: "manus" }, + * }) + */ +import { ENV } from "./env"; + +export type DataApiCallOptions = { + query?: Record; + body?: Record; + pathParams?: Record; + formData?: Record; +}; + +export async function callDataApi( + apiId: string, + options: DataApiCallOptions = {} +): Promise { + if (!ENV.forgeApiUrl) { + throw new Error("BUILT_IN_FORGE_API_URL is not configured"); + } + if (!ENV.forgeApiKey) { + throw new Error("BUILT_IN_FORGE_API_KEY is not configured"); + } + + // Build the full URL by appending the service path to the base URL + const baseUrl = ENV.forgeApiUrl.endsWith("/") ? ENV.forgeApiUrl : `${ENV.forgeApiUrl}/`; + const fullUrl = new URL("webdevtoken.v1.WebDevService/CallApi", baseUrl).toString(); + + const response = await fetch(fullUrl, { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + "connect-protocol-version": "1", + authorization: `Bearer ${ENV.forgeApiKey}`, + }, + body: JSON.stringify({ + apiId, + query: options.query, + body: options.body, + path_params: options.pathParams, + multipart_form_data: options.formData, + }), + }); + + if (!response.ok) { + const detail = await response.text().catch(() => ""); + throw new Error( + `Data API request failed (${response.status} ${response.statusText})${detail ? `: ${detail}` : ""}` + ); + } + + const payload = await response.json().catch(() => ({})); + if (payload && typeof payload === "object" && "jsonData" in payload) { + try { + return JSON.parse((payload as Record).jsonData ?? "{}"); + } catch { + return (payload as Record).jsonData; + } + } + return payload; +} diff --git a/deployment-package/source_server/_core/emailSender.ts b/deployment-package/source_server/_core/emailSender.ts new file mode 100644 index 0000000..9d6fd7d --- /dev/null +++ b/deployment-package/source_server/_core/emailSender.ts @@ -0,0 +1,206 @@ +/** + * Service d'envoi d'emails réels via Resend API + * + * Configuration requise: + * - RESEND_API_KEY: Clé API Resend (obtenir sur https://resend.com) + * - RESEND_FROM_EMAIL: Adresse email d'envoi (doit être vérifiée dans Resend) + * + * Si ces variables ne sont pas configurées, les emails seront simulés + * et envoyés comme notifications au propriétaire du projet. + */ + +import { ENV } from "./env"; +import { notifyOwner } from "./notification"; +import { getActiveEmailConfig } from "../db"; +import { sendViaSMTP } from "./smtpSender"; + +interface EmailParams { + to: string; + subject: string; + html: string; + attachments?: Array<{ + filename: string; + content: string; + contentType: string; + }>; +} + +interface ResendEmailRequest { + from: string; + to: string[]; + subject: string; + html: string; + attachments?: Array<{ + filename: string; + content: string; + }>; +} + +/** + * Envoie un email via Resend API + */ +async function sendViaResend(params: EmailParams, config?: { apiKey: string; fromEmail: string; fromName: string }): Promise { + // Utiliser la config fournie ou les variables d'environnement + const resendApiKey = config?.apiKey || process.env.RESEND_API_KEY; + const fromEmail = config?.fromEmail || process.env.RESEND_FROM_EMAIL || 'noreply@manusvm.computer'; + const fromName = config?.fromName || 'Formation Manager Itinova'; + + if (!resendApiKey) { + console.warn('[Email] RESEND_API_KEY non configurée, email simulé'); + return false; + } + + try { + const payload: ResendEmailRequest = { + from: `${fromName} <${fromEmail}>`, + to: [params.to], + subject: params.subject, + html: params.html, + }; + + // Ajouter les pièces jointes si présentes + if (params.attachments && params.attachments.length > 0) { + payload.attachments = params.attachments.map(att => ({ + filename: att.filename, + content: att.content, // Resend accepte base64 ou string + })); + } + + const response = await fetch('https://api.resend.com/emails', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${resendApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response.text(); + console.error('[Email] Erreur Resend:', response.status, error); + return false; + } + + const result = await response.json(); + console.log('[Email] Email envoyé via Resend:', result.id); + return true; + } catch (error) { + console.error('[Email] Exception lors de l\'envoi via Resend:', error); + return false; + } +} + +/** + * Simule l'envoi d'un email en créant une notification pour le propriétaire + */ +async function simulateEmail(params: EmailParams): Promise { + console.log('=== EMAIL SIMULÉ ==='); + console.log('To:', params.to); + console.log('Subject:', params.subject); + + const emailContent = ` +Destinataire: ${params.to} +Sujet: ${params.subject} + +${params.html.replace(/<[^>]*>/g, '').substring(0, 500)}... + +${params.attachments ? `Pièces jointes: ${params.attachments.map(a => a.filename).join(', ')}` : ''} + +⚠️ Cet email est simulé. Pour envoyer de vrais emails: +1. Créez un compte sur https://resend.com +2. Ajoutez RESEND_API_KEY dans les secrets du projet +3. Ajoutez RESEND_FROM_EMAIL (ex: noreply@votredomaine.com)`; + + try { + await notifyOwner({ + title: `📧 Email simulé: ${params.subject}`, + content: emailContent, + }); + console.log('✓ Email simulé (notification envoyée au propriétaire)'); + } catch (error) { + console.error('Erreur lors de la simulation d\'email:', error); + } + + console.log('==================='); + + return true; +} + +/** + * Envoie un email (réel ou simulé selon la configuration) + */ +export async function sendEmail(params: EmailParams): Promise { + // Récupérer la configuration depuis la base de données + const config = await getActiveEmailConfig(); + + console.log('[Email] Configuration récupérée:', config ? { + provider: config.provider, + mode: config.mode, + fromEmail: config.fromEmail, + hasSmtpHost: !!config.smtpHost, + hasSmtpUser: !!config.smtpUser, + hasSmtpPassword: !!config.smtpPassword, + } : 'Aucune configuration'); + + // Si mode simulation ou pas de config, simuler + if (!config || config.mode === 'simulation') { + console.log('[Email] Mode simulation activé ou pas de config'); + return await simulateEmail(params); + } + + // Si mode production + if (config.mode === 'production') { + console.log('[Email] Mode production activé'); + + // Essayer SMTP en priorité si configuré + if (config.provider === 'smtp' && config.smtpHost && config.smtpPort && config.smtpUser && config.smtpPassword) { + console.log('[Email] Tentative d\'envoi via SMTP...'); + + const sent = await sendViaSMTP(params, { + host: config.smtpHost, + port: config.smtpPort, + secure: config.smtpSecure || 'tls', + user: config.smtpUser, + password: config.smtpPassword, + fromEmail: config.fromEmail, + fromName: config.fromName, + }); + + // Si l'envoi échoue, simuler en fallback + if (!sent) { + console.log('[Email] Échec de l\'envoi SMTP, basculement en simulation'); + return await simulateEmail(params); + } + + console.log('[Email] Email envoyé avec succès via SMTP'); + return true; + } + + // Sinon essayer Resend + if (config.provider === 'resend' && config.apiKey) { + console.log('[Email] Tentative d\'envoi via Resend...'); + const sent = await sendViaResend(params, { + apiKey: config.apiKey, + fromEmail: config.fromEmail, + fromName: config.fromName, + }); + + // Si l'envoi échoue, simuler en fallback + if (!sent) { + return await simulateEmail(params); + } + + return true; + } + } + + // Fallback: simuler + return await simulateEmail(params); +} + +/** + * Vérifie si le service d'envoi d'emails réel est configuré + */ +export function isEmailServiceConfigured(): boolean { + return !!process.env.RESEND_API_KEY; +} diff --git a/deployment-package/source_server/_core/env.ts b/deployment-package/source_server/_core/env.ts new file mode 100644 index 0000000..305082c --- /dev/null +++ b/deployment-package/source_server/_core/env.ts @@ -0,0 +1,11 @@ +export const ENV = { + appId: process.env.VITE_APP_ID ?? "", + cookieSecret: process.env.JWT_SECRET ?? "", + jwtSecret: process.env.JWT_SECRET ?? "", + databaseUrl: process.env.DATABASE_URL ?? "", + oAuthServerUrl: process.env.OAUTH_SERVER_URL ?? "", + ownerOpenId: process.env.OWNER_OPEN_ID ?? "", + isProduction: process.env.NODE_ENV === "production", + forgeApiUrl: process.env.BUILT_IN_FORGE_API_URL ?? "", + forgeApiKey: process.env.BUILT_IN_FORGE_API_KEY ?? "", +}; diff --git a/deployment-package/source_server/_core/fileUpload.ts b/deployment-package/source_server/_core/fileUpload.ts new file mode 100644 index 0000000..c050be1 --- /dev/null +++ b/deployment-package/source_server/_core/fileUpload.ts @@ -0,0 +1,59 @@ +import { Request, Response } from "express"; +import multer from "multer"; +import { storagePut } from "../storage"; +import crypto from "crypto"; +import path from "path"; + +// Configuration de multer pour l'upload en mémoire +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 10 * 1024 * 1024, // 10 Mo max + }, +}); + +// Middleware pour l'upload d'un seul fichier +export const uploadSingle = upload.single("file"); + +// Handler pour l'upload de fichier vers S3 +export async function handleFileUpload(req: Request, res: Response) { + try { + if (!req.file) { + return res.status(400).json({ error: "Aucun fichier fourni" }); + } + + const file = req.file; + + // Générer un nom de fichier unique + const randomSuffix = crypto.randomBytes(8).toString("hex"); + const ext = path.extname(file.originalname); + const baseName = path.basename(file.originalname, ext); + const safeBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, "_"); + const fileName = `${safeBaseName}-${randomSuffix}${ext}`; + + // Chemin S3 pour les pièces jointes de rappels + const s3Key = `rappels-attachments/${fileName}`; + + // Upload vers S3 + const { url, key } = await storagePut( + s3Key, + file.buffer, + file.mimetype + ); + + console.log(`[FileUpload] Fichier uploadé: ${fileName} (${file.size} octets)`); + + return res.json({ + url, + key, + filename: file.originalname, + mimetype: file.mimetype, + size: file.size, + }); + } catch (error) { + console.error("[FileUpload] Erreur:", error); + return res.status(500).json({ + error: "Erreur lors de l'upload du fichier" + }); + } +} diff --git a/deployment-package/source_server/_core/imageGeneration.ts b/deployment-package/source_server/_core/imageGeneration.ts new file mode 100644 index 0000000..0ba0c98 --- /dev/null +++ b/deployment-package/source_server/_core/imageGeneration.ts @@ -0,0 +1,92 @@ +/** + * Image generation helper using internal ImageService + * + * Example usage: + * const { url: imageUrl } = await generateImage({ + * prompt: "A serene landscape with mountains" + * }); + * + * For editing: + * const { url: imageUrl } = await generateImage({ + * prompt: "Add a rainbow to this landscape", + * originalImages: [{ + * url: "https://example.com/original.jpg", + * mimeType: "image/jpeg" + * }] + * }); + */ +import { storagePut } from "server/storage"; +import { ENV } from "./env"; + +export type GenerateImageOptions = { + prompt: string; + originalImages?: Array<{ + url?: string; + b64Json?: string; + mimeType?: string; + }>; +}; + +export type GenerateImageResponse = { + url?: string; +}; + +export async function generateImage( + options: GenerateImageOptions +): Promise { + if (!ENV.forgeApiUrl) { + throw new Error("BUILT_IN_FORGE_API_URL is not configured"); + } + if (!ENV.forgeApiKey) { + throw new Error("BUILT_IN_FORGE_API_KEY is not configured"); + } + + // Build the full URL by appending the service path to the base URL + const baseUrl = ENV.forgeApiUrl.endsWith("/") + ? ENV.forgeApiUrl + : `${ENV.forgeApiUrl}/`; + const fullUrl = new URL( + "images.v1.ImageService/GenerateImage", + baseUrl + ).toString(); + + const response = await fetch(fullUrl, { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + "connect-protocol-version": "1", + authorization: `Bearer ${ENV.forgeApiKey}`, + }, + body: JSON.stringify({ + prompt: options.prompt, + original_images: options.originalImages || [], + }), + }); + + if (!response.ok) { + const detail = await response.text().catch(() => ""); + throw new Error( + `Image generation request failed (${response.status} ${response.statusText})${detail ? `: ${detail}` : ""}` + ); + } + + const result = (await response.json()) as { + image: { + b64Json: string; + mimeType: string; + }; + }; + const base64Data = result.image.b64Json; + const buffer = Buffer.from(base64Data, "base64"); + + // Save to S3 + const { url } = await storagePut( + `generated/${Date.now()}.png`, + buffer, + result.image.mimeType + ); + return { + url, + }; +} diff --git a/deployment-package/source_server/_core/index.ts b/deployment-package/source_server/_core/index.ts new file mode 100644 index 0000000..47aaafc --- /dev/null +++ b/deployment-package/source_server/_core/index.ts @@ -0,0 +1,82 @@ +import "dotenv/config"; +import express from "express"; +import { createServer } from "http"; +import net from "net"; +import { createExpressMiddleware } from "@trpc/server/adapters/express"; +import { registerOAuthRoutes } from "./oauth"; +import { appRouter } from "../routers"; +import { createContext } from "./context"; +import localAuthRouter from "./localAuth"; +import uploadImageRouter from "./uploadImage"; +import uploadFileRouter from "./uploadFile"; +import { serveStatic, setupVite } from "./vite"; +import { initRappelScheduler } from "../rappelScheduler"; +import { initRappelRetryScheduler } from "../rappelRetry"; + +function isPortAvailable(port: number): Promise { + return new Promise(resolve => { + const server = net.createServer(); + server.listen(port, () => { + server.close(() => resolve(true)); + }); + server.on("error", () => resolve(false)); + }); +} + +async function findAvailablePort(startPort: number = 3000): Promise { + for (let port = startPort; port < startPort + 20; port++) { + if (await isPortAvailable(port)) { + return port; + } + } + throw new Error(`No available port found starting from ${startPort}`); +} + +async function startServer() { + const app = express(); + const server = createServer(app); + // Configure body parser with larger size limit for file uploads + app.use(express.json({ limit: "50mb" })); + app.use(express.urlencoded({ limit: "50mb", extended: true })); + // OAuth callback under /api/oauth/callback + registerOAuthRoutes(app); + // Local authentication under /api/auth/local + app.use("/api/auth/local", localAuthRouter); + // Image upload under /api/upload-image + app.use("/api/upload-image", uploadImageRouter); + // File upload under /api/upload-file + app.use("/api/upload-file", uploadFileRouter); + // tRPC API + app.use( + "/api/trpc", + createExpressMiddleware({ + router: appRouter, + createContext, + }) + ); + // development mode uses Vite, production mode uses static files + if (process.env.NODE_ENV === "development") { + await setupVite(app, server); + } else { + serveStatic(app); + } + + const preferredPort = parseInt(process.env.PORT || "3000"); + const port = await findAvailablePort(preferredPort); + + if (port !== preferredPort) { + console.log(`Port ${preferredPort} is busy, using port ${port} instead`); + } + + server.listen(port, () => { + console.log(`Server running on http://localhost:${port}/`); + + // Initialiser le scheduler de rappels automatiques + initRappelScheduler(); + + // Initialiser le scheduler de réessai automatique + initRappelRetryScheduler(); + }); +} + +startServer().catch(console.error); diff --git a/deployment-package/source_server/_core/llm.ts b/deployment-package/source_server/_core/llm.ts new file mode 100644 index 0000000..8ea4c4a --- /dev/null +++ b/deployment-package/source_server/_core/llm.ts @@ -0,0 +1,332 @@ +import { ENV } from "./env"; + +export type Role = "system" | "user" | "assistant" | "tool" | "function"; + +export type TextContent = { + type: "text"; + text: string; +}; + +export type ImageContent = { + type: "image_url"; + image_url: { + url: string; + detail?: "auto" | "low" | "high"; + }; +}; + +export type FileContent = { + type: "file_url"; + file_url: { + url: string; + mime_type?: "audio/mpeg" | "audio/wav" | "application/pdf" | "audio/mp4" | "video/mp4" ; + }; +}; + +export type MessageContent = string | TextContent | ImageContent | FileContent; + +export type Message = { + role: Role; + content: MessageContent | MessageContent[]; + name?: string; + tool_call_id?: string; +}; + +export type Tool = { + type: "function"; + function: { + name: string; + description?: string; + parameters?: Record; + }; +}; + +export type ToolChoicePrimitive = "none" | "auto" | "required"; +export type ToolChoiceByName = { name: string }; +export type ToolChoiceExplicit = { + type: "function"; + function: { + name: string; + }; +}; + +export type ToolChoice = + | ToolChoicePrimitive + | ToolChoiceByName + | ToolChoiceExplicit; + +export type InvokeParams = { + messages: Message[]; + tools?: Tool[]; + toolChoice?: ToolChoice; + tool_choice?: ToolChoice; + maxTokens?: number; + max_tokens?: number; + outputSchema?: OutputSchema; + output_schema?: OutputSchema; + responseFormat?: ResponseFormat; + response_format?: ResponseFormat; +}; + +export type ToolCall = { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; +}; + +export type InvokeResult = { + id: string; + created: number; + model: string; + choices: Array<{ + index: number; + message: { + role: Role; + content: string | Array; + tool_calls?: ToolCall[]; + }; + finish_reason: string | null; + }>; + usage?: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +}; + +export type JsonSchema = { + name: string; + schema: Record; + strict?: boolean; +}; + +export type OutputSchema = JsonSchema; + +export type ResponseFormat = + | { type: "text" } + | { type: "json_object" } + | { type: "json_schema"; json_schema: JsonSchema }; + +const ensureArray = ( + value: MessageContent | MessageContent[] +): MessageContent[] => (Array.isArray(value) ? value : [value]); + +const normalizeContentPart = ( + part: MessageContent +): TextContent | ImageContent | FileContent => { + if (typeof part === "string") { + return { type: "text", text: part }; + } + + if (part.type === "text") { + return part; + } + + if (part.type === "image_url") { + return part; + } + + if (part.type === "file_url") { + return part; + } + + throw new Error("Unsupported message content part"); +}; + +const normalizeMessage = (message: Message) => { + const { role, name, tool_call_id } = message; + + if (role === "tool" || role === "function") { + const content = ensureArray(message.content) + .map(part => (typeof part === "string" ? part : JSON.stringify(part))) + .join("\n"); + + return { + role, + name, + tool_call_id, + content, + }; + } + + const contentParts = ensureArray(message.content).map(normalizeContentPart); + + // If there's only text content, collapse to a single string for compatibility + if (contentParts.length === 1 && contentParts[0].type === "text") { + return { + role, + name, + content: contentParts[0].text, + }; + } + + return { + role, + name, + content: contentParts, + }; +}; + +const normalizeToolChoice = ( + toolChoice: ToolChoice | undefined, + tools: Tool[] | undefined +): "none" | "auto" | ToolChoiceExplicit | undefined => { + if (!toolChoice) return undefined; + + if (toolChoice === "none" || toolChoice === "auto") { + return toolChoice; + } + + if (toolChoice === "required") { + if (!tools || tools.length === 0) { + throw new Error( + "tool_choice 'required' was provided but no tools were configured" + ); + } + + if (tools.length > 1) { + throw new Error( + "tool_choice 'required' needs a single tool or specify the tool name explicitly" + ); + } + + return { + type: "function", + function: { name: tools[0].function.name }, + }; + } + + if ("name" in toolChoice) { + return { + type: "function", + function: { name: toolChoice.name }, + }; + } + + return toolChoice; +}; + +const resolveApiUrl = () => + ENV.forgeApiUrl && ENV.forgeApiUrl.trim().length > 0 + ? `${ENV.forgeApiUrl.replace(/\/$/, "")}/v1/chat/completions` + : "https://forge.manus.im/v1/chat/completions"; + +const assertApiKey = () => { + if (!ENV.forgeApiKey) { + throw new Error("OPENAI_API_KEY is not configured"); + } +}; + +const normalizeResponseFormat = ({ + responseFormat, + response_format, + outputSchema, + output_schema, +}: { + responseFormat?: ResponseFormat; + response_format?: ResponseFormat; + outputSchema?: OutputSchema; + output_schema?: OutputSchema; +}): + | { type: "json_schema"; json_schema: JsonSchema } + | { type: "text" } + | { type: "json_object" } + | undefined => { + const explicitFormat = responseFormat || response_format; + if (explicitFormat) { + if ( + explicitFormat.type === "json_schema" && + !explicitFormat.json_schema?.schema + ) { + throw new Error( + "responseFormat json_schema requires a defined schema object" + ); + } + return explicitFormat; + } + + const schema = outputSchema || output_schema; + if (!schema) return undefined; + + if (!schema.name || !schema.schema) { + throw new Error("outputSchema requires both name and schema"); + } + + return { + type: "json_schema", + json_schema: { + name: schema.name, + schema: schema.schema, + ...(typeof schema.strict === "boolean" ? { strict: schema.strict } : {}), + }, + }; +}; + +export async function invokeLLM(params: InvokeParams): Promise { + assertApiKey(); + + const { + messages, + tools, + toolChoice, + tool_choice, + outputSchema, + output_schema, + responseFormat, + response_format, + } = params; + + const payload: Record = { + model: "gemini-2.5-flash", + messages: messages.map(normalizeMessage), + }; + + if (tools && tools.length > 0) { + payload.tools = tools; + } + + const normalizedToolChoice = normalizeToolChoice( + toolChoice || tool_choice, + tools + ); + if (normalizedToolChoice) { + payload.tool_choice = normalizedToolChoice; + } + + payload.max_tokens = 32768 + payload.thinking = { + "budget_tokens": 128 + } + + const normalizedResponseFormat = normalizeResponseFormat({ + responseFormat, + response_format, + outputSchema, + output_schema, + }); + + if (normalizedResponseFormat) { + payload.response_format = normalizedResponseFormat; + } + + const response = await fetch(resolveApiUrl(), { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${ENV.forgeApiKey}`, + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `LLM invoke failed: ${response.status} ${response.statusText} – ${errorText}` + ); + } + + return (await response.json()) as InvokeResult; +} diff --git a/deployment-package/source_server/_core/localAuth.ts b/deployment-package/source_server/_core/localAuth.ts new file mode 100644 index 0000000..b020893 --- /dev/null +++ b/deployment-package/source_server/_core/localAuth.ts @@ -0,0 +1,102 @@ +import { Router } from "express"; +import bcrypt from "bcryptjs"; +import { getDb } from "../db"; +import { users } from "../../drizzle/schema"; +import { eq } from "drizzle-orm"; +import { COOKIE_NAME } from "@shared/const"; +import { getSessionCookieOptions } from "./cookies"; +import { sdk } from "./sdk"; + +const router = Router(); + +/** + * Route d'authentification locale avec username/password + * POST /api/auth/local/login + * Body: { username: string, password: string } + */ +router.post("/login", async (req, res) => { + try { + const { username, password } = req.body; + + // Validation des champs + if (!username || !password) { + return res.status(400).json({ + success: false, + message: "Identifiant et mot de passe requis", + }); + } + + // Récupérer l'utilisateur par username + const db = await getDb(); + if (!db) { + return res.status(500).json({ + success: false, + message: "Erreur de connexion à la base de données", + }); + } + + const result = await db + .select() + .from(users) + .where(eq(users.username, username)) + .limit(1); + + if (result.length === 0) { + return res.status(401).json({ + success: false, + message: "Identifiant ou mot de passe incorrect", + }); + } + + const user = result[0]; + + // Vérifier que l'utilisateur a un mot de passe défini + if (!user.password) { + return res.status(401).json({ + success: false, + message: "Cet utilisateur n'a pas de mot de passe défini", + }); + } + + // Comparer le mot de passe + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + return res.status(401).json({ + success: false, + message: "Identifiant ou mot de passe incorrect", + }); + } + + // Mettre à jour la date de dernière connexion + await db + .update(users) + .set({ lastSignedIn: new Date() }) + .where(eq(users.id, user.id)); + + // Créer le token de session (compatible avec le système OAuth) + const token = await sdk.createSessionToken(user.openId, { + name: user.name || "", + expiresInMs: 7 * 24 * 60 * 60 * 1000, // 7 jours + }); + + // Définir le cookie de session + const cookieOptions = getSessionCookieOptions(req); + res.cookie(COOKIE_NAME, token, cookieOptions); + + // Retourner l'utilisateur (sans le mot de passe) + const { password: _, ...userWithoutPassword } = user; + return res.json({ + success: true, + user: userWithoutPassword, + }); + } catch (error) { + console.error("[LocalAuth] Error during login:", error); + return res.status(500).json({ + success: false, + message: "Erreur lors de la connexion", + }); + } +}); + +export default router; diff --git a/deployment-package/source_server/_core/map.ts b/deployment-package/source_server/_core/map.ts new file mode 100644 index 0000000..ac2c28c --- /dev/null +++ b/deployment-package/source_server/_core/map.ts @@ -0,0 +1,319 @@ +/** + * Google Maps API Integration for Manus WebDev Templates + * + * Main function: makeRequest(endpoint, params) - Makes authenticated requests to Google Maps APIs + * All credentials are automatically injected. Array parameters use | as separator. + * + * See API examples below the type definitions for usage patterns. + */ + +import { ENV } from "./env"; + +// ============================================================================ +// Configuration +// ============================================================================ + +type MapsConfig = { + baseUrl: string; + apiKey: string; +}; + +function getMapsConfig(): MapsConfig { + const baseUrl = ENV.forgeApiUrl; + const apiKey = ENV.forgeApiKey; + + if (!baseUrl || !apiKey) { + throw new Error( + "Google Maps proxy credentials missing: set BUILT_IN_FORGE_API_URL and BUILT_IN_FORGE_API_KEY" + ); + } + + return { + baseUrl: baseUrl.replace(/\/+$/, ""), + apiKey, + }; +} + +// ============================================================================ +// Core Request Handler +// ============================================================================ + +interface RequestOptions { + method?: "GET" | "POST"; + body?: Record; +} + +/** + * Make authenticated requests to Google Maps APIs + * + * @param endpoint - The API endpoint (e.g., "/maps/api/geocode/json") + * @param params - Query parameters for the request + * @param options - Additional request options + * @returns The API response + */ +export async function makeRequest( + endpoint: string, + params: Record = {}, + options: RequestOptions = {} +): Promise { + const { baseUrl, apiKey } = getMapsConfig(); + + // Construct full URL: baseUrl + /v1/maps/proxy + endpoint + const url = new URL(`${baseUrl}/v1/maps/proxy${endpoint}`); + + // Add API key as query parameter (standard Google Maps API authentication) + url.searchParams.append("key", apiKey); + + // Add other query parameters + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.append(key, String(value)); + } + }); + + const response = await fetch(url.toString(), { + method: options.method || "GET", + headers: { + "Content-Type": "application/json", + }, + body: options.body ? JSON.stringify(options.body) : undefined, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Google Maps API request failed (${response.status} ${response.statusText}): ${errorText}` + ); + } + + return (await response.json()) as T; +} + +// ============================================================================ +// Type Definitions +// ============================================================================ + +export type TravelMode = "driving" | "walking" | "bicycling" | "transit"; +export type MapType = "roadmap" | "satellite" | "terrain" | "hybrid"; +export type SpeedUnit = "KPH" | "MPH"; + +export type LatLng = { + lat: number; + lng: number; +}; + +export type DirectionsResult = { + routes: Array<{ + legs: Array<{ + distance: { text: string; value: number }; + duration: { text: string; value: number }; + start_address: string; + end_address: string; + start_location: LatLng; + end_location: LatLng; + steps: Array<{ + distance: { text: string; value: number }; + duration: { text: string; value: number }; + html_instructions: string; + travel_mode: string; + start_location: LatLng; + end_location: LatLng; + }>; + }>; + overview_polyline: { points: string }; + summary: string; + warnings: string[]; + waypoint_order: number[]; + }>; + status: string; +}; + +export type DistanceMatrixResult = { + rows: Array<{ + elements: Array<{ + distance: { text: string; value: number }; + duration: { text: string; value: number }; + status: string; + }>; + }>; + origin_addresses: string[]; + destination_addresses: string[]; + status: string; +}; + +export type GeocodingResult = { + results: Array<{ + address_components: Array<{ + long_name: string; + short_name: string; + types: string[]; + }>; + formatted_address: string; + geometry: { + location: LatLng; + location_type: string; + viewport: { + northeast: LatLng; + southwest: LatLng; + }; + }; + place_id: string; + types: string[]; + }>; + status: string; +}; + +export type PlacesSearchResult = { + results: Array<{ + place_id: string; + name: string; + formatted_address: string; + geometry: { + location: LatLng; + }; + rating?: number; + user_ratings_total?: number; + business_status?: string; + types: string[]; + }>; + status: string; +}; + +export type PlaceDetailsResult = { + result: { + place_id: string; + name: string; + formatted_address: string; + formatted_phone_number?: string; + international_phone_number?: string; + website?: string; + rating?: number; + user_ratings_total?: number; + reviews?: Array<{ + author_name: string; + rating: number; + text: string; + time: number; + }>; + opening_hours?: { + open_now: boolean; + weekday_text: string[]; + }; + geometry: { + location: LatLng; + }; + }; + status: string; +}; + +export type ElevationResult = { + results: Array<{ + elevation: number; + location: LatLng; + resolution: number; + }>; + status: string; +}; + +export type TimeZoneResult = { + dstOffset: number; + rawOffset: number; + status: string; + timeZoneId: string; + timeZoneName: string; +}; + +export type RoadsResult = { + snappedPoints: Array<{ + location: LatLng; + originalIndex?: number; + placeId: string; + }>; +}; + +// ============================================================================ +// Google Maps API Reference +// ============================================================================ + +/** + * GEOCODING - Convert between addresses and coordinates + * Endpoint: /maps/api/geocode/json + * Input: { address: string } OR { latlng: string } // latlng: "37.42,-122.08" + * Output: GeocodingResult // results[0].geometry.location, results[0].formatted_address + */ + +/** + * DIRECTIONS - Get navigation routes between locations + * Endpoint: /maps/api/directions/json + * Input: { origin: string, destination: string, mode?: TravelMode, waypoints?: string, alternatives?: boolean } + * Output: DirectionsResult // routes[0].legs[0].distance, duration, steps + */ + +/** + * DISTANCE MATRIX - Calculate travel times/distances for multiple origin-destination pairs + * Endpoint: /maps/api/distancematrix/json + * Input: { origins: string, destinations: string, mode?: TravelMode, units?: "metric"|"imperial" } // origins: "NYC|Boston" + * Output: DistanceMatrixResult // rows[0].elements[1] = first origin to second destination + */ + +/** + * PLACE SEARCH - Find businesses/POIs by text query + * Endpoint: /maps/api/place/textsearch/json + * Input: { query: string, location?: string, radius?: number, type?: string } // location: "40.7,-74.0" + * Output: PlacesSearchResult // results[].name, rating, geometry.location, place_id + */ + +/** + * NEARBY SEARCH - Find places near a specific location + * Endpoint: /maps/api/place/nearbysearch/json + * Input: { location: string, radius: number, type?: string, keyword?: string } // location: "40.7,-74.0" + * Output: PlacesSearchResult + */ + +/** + * PLACE DETAILS - Get comprehensive information about a specific place + * Endpoint: /maps/api/place/details/json + * Input: { place_id: string, fields?: string } // fields: "name,rating,opening_hours,website" + * Output: PlaceDetailsResult // result.name, rating, opening_hours, etc. + */ + +/** + * ELEVATION - Get altitude data for geographic points + * Endpoint: /maps/api/elevation/json + * Input: { locations?: string, path?: string, samples?: number } // locations: "39.73,-104.98|36.45,-116.86" + * Output: ElevationResult // results[].elevation (meters) + */ + +/** + * TIME ZONE - Get timezone information for a location + * Endpoint: /maps/api/timezone/json + * Input: { location: string, timestamp: number } // timestamp: Math.floor(Date.now()/1000) + * Output: TimeZoneResult // timeZoneId, timeZoneName + */ + +/** + * ROADS - Snap GPS traces to roads, find nearest roads, get speed limits + * - /v1/snapToRoads: Input: { path: string, interpolate?: boolean } // path: "lat,lng|lat,lng" + * - /v1/nearestRoads: Input: { points: string } // points: "lat,lng|lat,lng" + * - /v1/speedLimits: Input: { path: string, units?: SpeedUnit } + * Output: RoadsResult + */ + +/** + * PLACE AUTOCOMPLETE - Real-time place suggestions as user types + * Endpoint: /maps/api/place/autocomplete/json + * Input: { input: string, location?: string, radius?: number } + * Output: { predictions: Array<{ description: string, place_id: string }> } + */ + +/** + * STATIC MAPS - Generate map images as URLs (for emails, reports, tags) + * Endpoint: /maps/api/staticmap + * Input: URL params - center: string, zoom: number, size: string, markers?: string, maptype?: MapType + * Output: Image URL (not JSON) - use directly in + * Note: Construct URL manually with getMapsConfig() for auth + */ + + + + diff --git a/deployment-package/source_server/_core/notification.ts b/deployment-package/source_server/_core/notification.ts new file mode 100644 index 0000000..889a573 --- /dev/null +++ b/deployment-package/source_server/_core/notification.ts @@ -0,0 +1,114 @@ +import { TRPCError } from "@trpc/server"; +import { ENV } from "./env"; + +export type NotificationPayload = { + title: string; + content: string; +}; + +const TITLE_MAX_LENGTH = 1200; +const CONTENT_MAX_LENGTH = 20000; + +const trimValue = (value: string): string => value.trim(); +const isNonEmptyString = (value: unknown): value is string => + typeof value === "string" && value.trim().length > 0; + +const buildEndpointUrl = (baseUrl: string): string => { + const normalizedBase = baseUrl.endsWith("/") + ? baseUrl + : `${baseUrl}/`; + return new URL( + "webdevtoken.v1.WebDevService/SendNotification", + normalizedBase + ).toString(); +}; + +const validatePayload = (input: NotificationPayload): NotificationPayload => { + if (!isNonEmptyString(input.title)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Notification title is required.", + }); + } + if (!isNonEmptyString(input.content)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Notification content is required.", + }); + } + + const title = trimValue(input.title); + const content = trimValue(input.content); + + if (title.length > TITLE_MAX_LENGTH) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Notification title must be at most ${TITLE_MAX_LENGTH} characters.`, + }); + } + + if (content.length > CONTENT_MAX_LENGTH) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Notification content must be at most ${CONTENT_MAX_LENGTH} characters.`, + }); + } + + return { title, content }; +}; + +/** + * Dispatches a project-owner notification through the Manus Notification Service. + * Returns `true` if the request was accepted, `false` when the upstream service + * cannot be reached (callers can fall back to email/slack). Validation errors + * bubble up as TRPC errors so callers can fix the payload. + */ +export async function notifyOwner( + payload: NotificationPayload +): Promise { + const { title, content } = validatePayload(payload); + + if (!ENV.forgeApiUrl) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Notification service URL is not configured.", + }); + } + + if (!ENV.forgeApiKey) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Notification service API key is not configured.", + }); + } + + const endpoint = buildEndpointUrl(ENV.forgeApiUrl); + + try { + const response = await fetch(endpoint, { + method: "POST", + headers: { + accept: "application/json", + authorization: `Bearer ${ENV.forgeApiKey}`, + "content-type": "application/json", + "connect-protocol-version": "1", + }, + body: JSON.stringify({ title, content }), + }); + + if (!response.ok) { + const detail = await response.text().catch(() => ""); + console.warn( + `[Notification] Failed to notify owner (${response.status} ${response.statusText})${ + detail ? `: ${detail}` : "" + }` + ); + return false; + } + + return true; + } catch (error) { + console.warn("[Notification] Error calling notification service:", error); + return false; + } +} diff --git a/deployment-package/source_server/_core/oauth.ts b/deployment-package/source_server/_core/oauth.ts new file mode 100644 index 0000000..fd45373 --- /dev/null +++ b/deployment-package/source_server/_core/oauth.ts @@ -0,0 +1,53 @@ +import { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const"; +import type { Express, Request, Response } from "express"; +import * as db from "../db"; +import { getSessionCookieOptions } from "./cookies"; +import { sdk } from "./sdk"; + +function getQueryParam(req: Request, key: string): string | undefined { + const value = req.query[key]; + return typeof value === "string" ? value : undefined; +} + +export function registerOAuthRoutes(app: Express) { + app.get("/api/oauth/callback", async (req: Request, res: Response) => { + const code = getQueryParam(req, "code"); + const state = getQueryParam(req, "state"); + + if (!code || !state) { + res.status(400).json({ error: "code and state are required" }); + return; + } + + try { + const tokenResponse = await sdk.exchangeCodeForToken(code, state); + const userInfo = await sdk.getUserInfo(tokenResponse.accessToken); + + if (!userInfo.openId) { + res.status(400).json({ error: "openId missing from user info" }); + return; + } + + await db.upsertUser({ + openId: userInfo.openId, + name: userInfo.name || null, + email: userInfo.email ?? null, + loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null, + lastSignedIn: new Date(), + }); + + const sessionToken = await sdk.createSessionToken(userInfo.openId, { + name: userInfo.name || "", + expiresInMs: ONE_YEAR_MS, + }); + + const cookieOptions = getSessionCookieOptions(req); + res.cookie(COOKIE_NAME, sessionToken, { ...cookieOptions, maxAge: ONE_YEAR_MS }); + + res.redirect(302, "/"); + } catch (error) { + console.error("[OAuth] Callback failed", error); + res.status(500).json({ error: "OAuth callback failed" }); + } + }); +} diff --git a/deployment-package/source_server/_core/sdk.ts b/deployment-package/source_server/_core/sdk.ts new file mode 100644 index 0000000..956239b --- /dev/null +++ b/deployment-package/source_server/_core/sdk.ts @@ -0,0 +1,330 @@ +import { AXIOS_TIMEOUT_MS, COOKIE_NAME, ONE_YEAR_MS } from "@shared/const"; +import { ForbiddenError } from "@shared/_core/errors"; +import axios, { type AxiosInstance } from "axios"; +import { parse as parseCookieHeader } from "cookie"; +import type { Request } from "express"; +import { SignJWT, jwtVerify } from "jose"; +import type { User } from "../../drizzle/schema"; +import * as db from "../db"; +import { ENV } from "./env"; +import type { + ExchangeTokenRequest, + ExchangeTokenResponse, + GetUserInfoResponse, + GetUserInfoWithJwtRequest, + GetUserInfoWithJwtResponse, +} from "./types/manusTypes"; +// Utility function +const isNonEmptyString = (value: unknown): value is string => + typeof value === "string" && value.length > 0; + +export type SessionPayload = { + openId: string; + appId: string; + name: string; +}; + +const EXCHANGE_TOKEN_PATH = `/webdev.v1.WebDevAuthPublicService/ExchangeToken`; +const GET_USER_INFO_PATH = `/webdev.v1.WebDevAuthPublicService/GetUserInfo`; +const GET_USER_INFO_WITH_JWT_PATH = `/webdev.v1.WebDevAuthPublicService/GetUserInfoWithJwt`; + +class OAuthService { + constructor(private client: ReturnType) { + console.log("[OAuth] Initialized with baseURL:", ENV.oAuthServerUrl); + if (!ENV.oAuthServerUrl) { + console.error( + "[OAuth] ERROR: OAUTH_SERVER_URL is not configured! Set OAUTH_SERVER_URL environment variable." + ); + } + } + + private decodeState(state: string): string { + const redirectUri = atob(state); + return redirectUri; + } + + async getTokenByCode( + code: string, + state: string + ): Promise { + const payload: ExchangeTokenRequest = { + clientId: ENV.appId, + grantType: "authorization_code", + code, + redirectUri: this.decodeState(state), + }; + + const { data } = await this.client.post( + EXCHANGE_TOKEN_PATH, + payload + ); + + return data; + } + + async getUserInfoByToken( + token: ExchangeTokenResponse + ): Promise { + const { data } = await this.client.post( + GET_USER_INFO_PATH, + { + accessToken: token.accessToken, + } + ); + + return data; + } +} + +const createOAuthHttpClient = (): AxiosInstance => + axios.create({ + baseURL: ENV.oAuthServerUrl, + timeout: AXIOS_TIMEOUT_MS, + }); + +class SDKServer { + private readonly client: AxiosInstance; + private readonly oauthService: OAuthService; + + constructor(client: AxiosInstance = createOAuthHttpClient()) { + this.client = client; + this.oauthService = new OAuthService(this.client); + } + + private deriveLoginMethod( + platforms: unknown, + fallback: string | null | undefined + ): string | null { + if (fallback && fallback.length > 0) return fallback; + if (!Array.isArray(platforms) || platforms.length === 0) return null; + const set = new Set( + platforms.filter((p): p is string => typeof p === "string") + ); + if (set.has("REGISTERED_PLATFORM_EMAIL")) return "email"; + if (set.has("REGISTERED_PLATFORM_GOOGLE")) return "google"; + if (set.has("REGISTERED_PLATFORM_APPLE")) return "apple"; + if ( + set.has("REGISTERED_PLATFORM_MICROSOFT") || + set.has("REGISTERED_PLATFORM_AZURE") + ) + return "microsoft"; + if (set.has("REGISTERED_PLATFORM_GITHUB")) return "github"; + const first = Array.from(set)[0]; + return first ? first.toLowerCase() : null; + } + + /** + * Exchange OAuth authorization code for access token + * @example + * const tokenResponse = await sdk.exchangeCodeForToken(code, state); + */ + async exchangeCodeForToken( + code: string, + state: string + ): Promise { + return this.oauthService.getTokenByCode(code, state); + } + + /** + * Get user information using access token + * @example + * const userInfo = await sdk.getUserInfo(tokenResponse.accessToken); + */ + async getUserInfo(accessToken: string): Promise { + const data = await this.oauthService.getUserInfoByToken({ + accessToken, + } as ExchangeTokenResponse); + const loginMethod = this.deriveLoginMethod( + (data as any)?.platforms, + (data as any)?.platform ?? data.platform ?? null + ); + return { + ...(data as any), + platform: loginMethod, + loginMethod, + } as GetUserInfoResponse; + } + + private parseCookies(cookieHeader: string | undefined) { + if (!cookieHeader) { + return new Map(); + } + + const parsed = parseCookieHeader(cookieHeader); + return new Map(Object.entries(parsed)); + } + + private getSessionSecret() { + const secret = ENV.cookieSecret; + return new TextEncoder().encode(secret); + } + + /** + * Create a session token for a Manus user openId + * @example + * const sessionToken = await sdk.createSessionToken(userInfo.openId); + */ + async createSessionToken( + openId: string, + options: { expiresInMs?: number; name?: string } = {} + ): Promise { + return this.signSession( + { + openId, + appId: ENV.appId, + name: options.name || "", + }, + options + ); + } + + async signSession( + payload: SessionPayload, + options: { expiresInMs?: number } = {} + ): Promise { + const issuedAt = Date.now(); + const expiresInMs = options.expiresInMs ?? ONE_YEAR_MS; + const expirationSeconds = Math.floor((issuedAt + expiresInMs) / 1000); + const secretKey = this.getSessionSecret(); + + return new SignJWT({ + openId: payload.openId, + appId: payload.appId, + name: payload.name, + }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setExpirationTime(expirationSeconds) + .sign(secretKey); + } + + async verifySession( + cookieValue: string | undefined | null + ): Promise<{ openId: string; appId: string; name: string } | null> { + if (!cookieValue) { + console.warn("[Auth] Missing session cookie"); + return null; + } + + try { + const secretKey = this.getSessionSecret(); + const { payload } = await jwtVerify(cookieValue, secretKey, { + algorithms: ["HS256"], + }); + const { openId, appId, name } = payload as Record; + + if ( + !isNonEmptyString(openId) || + !isNonEmptyString(appId) || + !isNonEmptyString(name) + ) { + console.warn("[Auth] Session payload missing required fields"); + return null; + } + + return { + openId, + appId, + name, + }; + } catch (error) { + console.warn("[Auth] Session verification failed", String(error)); + return null; + } + } + + async getUserInfoWithJwt( + jwtToken: string + ): Promise { + const payload: GetUserInfoWithJwtRequest = { + jwtToken, + projectId: ENV.appId, + }; + + const { data } = await this.client.post( + GET_USER_INFO_WITH_JWT_PATH, + payload + ); + + const loginMethod = this.deriveLoginMethod( + (data as any)?.platforms, + (data as any)?.platform ?? data.platform ?? null + ); + return { + ...(data as any), + platform: loginMethod, + loginMethod, + } as GetUserInfoWithJwtResponse; + } + + async authenticateRequest(req: Request): Promise { + // Regular authentication flow + const cookies = this.parseCookies(req.headers.cookie); + const sessionCookie = cookies.get(COOKIE_NAME); + + // Try to verify as local JWT first + let session: { openId: string; appId?: string; name: string } | null = null; + let isLocalAuth = false; + + if (sessionCookie) { + try { + const secretKey = this.getSessionSecret(); + const { payload } = await jwtVerify(sessionCookie, secretKey, { + algorithms: ["HS256"], + }); + const { openId, name, appId } = payload as Record; + + // Check if it's a local JWT (has openId but may not have appId) + if (isNonEmptyString(openId) && isNonEmptyString(name)) { + session = { openId, name, appId: appId as string | undefined }; + isLocalAuth = !appId; // Local auth doesn't have appId + } + } catch (error) { + console.warn("[Auth] JWT verification failed:", error); + } + } + + // If local JWT verification failed, try OAuth session + if (!session) { + session = await this.verifySession(sessionCookie); + } + + if (!session) { + throw ForbiddenError("Invalid session cookie"); + } + + const sessionUserId = session.openId; + const signedInAt = new Date(); + let user = await db.getUserByOpenId(sessionUserId); + + // If user not in DB, sync from OAuth server automatically (only for OAuth sessions) + if (!user && !isLocalAuth) { + try { + const userInfo = await this.getUserInfoWithJwt(sessionCookie ?? ""); + await db.upsertUser({ + openId: userInfo.openId, + name: userInfo.name || null, + email: userInfo.email ?? null, + loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null, + lastSignedIn: signedInAt, + }); + user = await db.getUserByOpenId(userInfo.openId); + } catch (error) { + console.error("[Auth] Failed to sync user from OAuth:", error); + throw ForbiddenError("Failed to sync user info"); + } + } + + if (!user) { + throw ForbiddenError("User not found"); + } + + await db.upsertUser({ + openId: user.openId, + lastSignedIn: signedInAt, + }); + + return user; + } +} + +export const sdk = new SDKServer(); diff --git a/deployment-package/source_server/_core/smtpSender.ts b/deployment-package/source_server/_core/smtpSender.ts new file mode 100644 index 0000000..64c0b30 --- /dev/null +++ b/deployment-package/source_server/_core/smtpSender.ts @@ -0,0 +1,103 @@ +/** + * Service d'envoi d'emails via SMTP avec nodemailer + */ + +import nodemailer from 'nodemailer'; +import type { Transporter } from 'nodemailer'; + +interface EmailParams { + to: string; + subject: string; + html: string; + attachments?: Array<{ + filename: string; + content: string; + contentType: string; + }>; +} + +interface SMTPConfig { + host: string; + port: number; + secure: 'none' | 'tls' | 'ssl'; + user: string; + password: string; + fromEmail: string; + fromName: string; +} + +/** + * Crée un transporteur nodemailer à partir de la configuration SMTP + */ +function createTransporter(config: SMTPConfig): Transporter { + const secure = config.secure === 'ssl'; // true pour SSL (port 465), false pour TLS/STARTTLS + + return nodemailer.createTransport({ + host: config.host, + port: config.port, + secure: secure, + auth: { + user: config.user, + pass: config.password, + }, + // Options supplémentaires pour améliorer la compatibilité + tls: { + // Ne pas échouer sur les certificats invalides (à utiliser avec précaution en production) + rejectUnauthorized: false, + }, + }); +} + +/** + * Envoie un email via SMTP + */ +export async function sendViaSMTP(params: EmailParams, config: SMTPConfig): Promise { + try { + const transporter = createTransporter(config); + + // Préparer les pièces jointes + const attachments = params.attachments?.map(att => ({ + filename: att.filename, + content: att.content, + contentType: att.contentType, + })); + + // Envoyer l'email + const info = await transporter.sendMail({ + from: `${config.fromName} <${config.fromEmail}>`, + to: params.to, + subject: params.subject, + html: params.html, + attachments: attachments, + }); + + console.log('[Email] Email envoyé via SMTP:', info.messageId); + return true; + } catch (error) { + console.error('[Email] Erreur lors de l\'envoi via SMTP:', error); + return false; + } +} + +/** + * Teste la connexion SMTP + */ +export async function testSMTPConnection(config: SMTPConfig): Promise<{ success: boolean; message: string }> { + try { + const transporter = createTransporter(config); + + // Vérifier la connexion + await transporter.verify(); + + return { + success: true, + message: 'Connexion SMTP réussie', + }; + } catch (error) { + console.error('[Email] Erreur de connexion SMTP:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Erreur de connexion SMTP', + }; + } +} diff --git a/deployment-package/source_server/_core/systemRouter.ts b/deployment-package/source_server/_core/systemRouter.ts new file mode 100644 index 0000000..babf6a4 --- /dev/null +++ b/deployment-package/source_server/_core/systemRouter.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; +import { notifyOwner } from "./notification"; +import { adminProcedure, publicProcedure, router } from "./trpc"; + +export const systemRouter = router({ + health: publicProcedure + .input( + z.object({ + timestamp: z.number().min(0, "timestamp cannot be negative"), + }) + ) + .query(() => ({ + ok: true, + })), + + notifyOwner: adminProcedure + .input( + z.object({ + title: z.string().min(1, "title is required"), + content: z.string().min(1, "content is required"), + }) + ) + .mutation(async ({ input }) => { + const delivered = await notifyOwner(input); + return { + success: delivered, + } as const; + }), +}); diff --git a/deployment-package/source_server/_core/trpc.ts b/deployment-package/source_server/_core/trpc.ts new file mode 100644 index 0000000..a0bcc05 --- /dev/null +++ b/deployment-package/source_server/_core/trpc.ts @@ -0,0 +1,45 @@ +import { NOT_ADMIN_ERR_MSG, UNAUTHED_ERR_MSG } from '@shared/const'; +import { initTRPC, TRPCError } from "@trpc/server"; +import superjson from "superjson"; +import type { TrpcContext } from "./context"; + +const t = initTRPC.context().create({ + transformer: superjson, +}); + +export const router = t.router; +export const publicProcedure = t.procedure; + +const requireUser = t.middleware(async opts => { + const { ctx, next } = opts; + + if (!ctx.user) { + throw new TRPCError({ code: "UNAUTHORIZED", message: UNAUTHED_ERR_MSG }); + } + + return next({ + ctx: { + ...ctx, + user: ctx.user, + }, + }); +}); + +export const protectedProcedure = t.procedure.use(requireUser); + +export const adminProcedure = t.procedure.use( + t.middleware(async opts => { + const { ctx, next } = opts; + + if (!ctx.user || ctx.user.role !== 'admin') { + throw new TRPCError({ code: "FORBIDDEN", message: NOT_ADMIN_ERR_MSG }); + } + + return next({ + ctx: { + ...ctx, + user: ctx.user, + }, + }); + }), +); diff --git a/deployment-package/source_server/_core/types/cookie.d.ts b/deployment-package/source_server/_core/types/cookie.d.ts new file mode 100644 index 0000000..d6d4e88 --- /dev/null +++ b/deployment-package/source_server/_core/types/cookie.d.ts @@ -0,0 +1,6 @@ +declare module "cookie" { + export function parse( + str: string, + options?: Record + ): Record; +} diff --git a/deployment-package/source_server/_core/types/manusTypes.ts b/deployment-package/source_server/_core/types/manusTypes.ts new file mode 100644 index 0000000..89a3819 --- /dev/null +++ b/deployment-package/source_server/_core/types/manusTypes.ts @@ -0,0 +1,69 @@ +// WebDev Auth TypeScript types +// Auto-generated from protobuf definitions +// Generated on: 2025-09-24T05:57:57.338Z + +export interface AuthorizeRequest { + redirectUri: string; + projectId: string; + state: string; + responseType: string; + scope: string; +} + +export interface AuthorizeResponse { + redirectUrl: string; +} + +export interface ExchangeTokenRequest { + grantType: string; + code: string; + refreshToken?: string; + clientId: string; + clientSecret?: string; + redirectUri: string; +} + +export interface ExchangeTokenResponse { + accessToken: string; + tokenType: string; + expiresIn: number; + refreshToken?: string; + scope: string; + idToken: string; +} + +export interface GetUserInfoRequest { + accessToken: string; +} + +export interface GetUserInfoResponse { + openId: string; + projectId: string; + name: string; + email?: string | null; + platform?: string | null; + loginMethod?: string | null; +} + +export interface CanAccessRequest { + openId: string; + projectId: string; +} + +export interface CanAccessResponse { + canAccess: boolean; +} + +export interface GetUserInfoWithJwtRequest { + jwtToken: string; + projectId: string; +} + +export interface GetUserInfoWithJwtResponse { + openId: string; + projectId: string; + name: string; + email?: string | null; + platform?: string | null; + loginMethod?: string | null; +} diff --git a/deployment-package/source_server/_core/uploadFile.ts b/deployment-package/source_server/_core/uploadFile.ts new file mode 100644 index 0000000..af6351a --- /dev/null +++ b/deployment-package/source_server/_core/uploadFile.ts @@ -0,0 +1,67 @@ +import { Router } from "express"; +import multer from "multer"; +import { storagePut } from "../storage"; +import { randomBytes } from "crypto"; +import path from "path"; + +const router = Router(); + +// Configuration de multer pour gérer les uploads en mémoire +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 10 * 1024 * 1024, // 10MB max + }, +}); + +/** + * Route d'upload de fichiers vers S3 + * POST /api/upload-file + * Body: multipart/form-data avec un champ "file" + */ +router.post("/", upload.single("file"), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + message: "Aucun fichier fourni", + }); + } + + const file = req.file; + + // Générer un nom de fichier unique + const randomSuffix = randomBytes(8).toString("hex"); + const ext = path.extname(file.originalname); + const baseName = path.basename(file.originalname, ext); + const safeBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, "_"); + const fileName = `rappels-attachments/${Date.now()}-${randomSuffix}-${safeBaseName}${ext}`; + + // Upload vers S3 + const result = await storagePut( + fileName, + file.buffer, + file.mimetype + ); + + console.log(`[UploadFile] Fichier uploadé: ${file.originalname} (${file.size} octets)`); + + return res.json({ + success: true, + url: result.url, + key: fileName, + filename: file.originalname, + mimetype: file.mimetype, + size: file.size, + message: "Fichier uploadé avec succès", + }); + } catch (error) { + console.error("[UploadFile] Error:", error); + return res.status(500).json({ + success: false, + message: "Erreur lors de l'upload du fichier", + }); + } +}); + +export default router; diff --git a/deployment-package/source_server/_core/uploadImage.ts b/deployment-package/source_server/_core/uploadImage.ts new file mode 100644 index 0000000..78f0c01 --- /dev/null +++ b/deployment-package/source_server/_core/uploadImage.ts @@ -0,0 +1,67 @@ +import { Router } from "express"; +import multer from "multer"; +import { storagePut } from "../storage"; +import { randomBytes } from "crypto"; + +const router = Router(); + +// Configuration de multer pour gérer les uploads en mémoire +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 5 * 1024 * 1024, // 5MB max + }, + fileFilter: (req, file, cb) => { + // Vérifier que c'est bien une image + if (file.mimetype.startsWith("image/")) { + cb(null, true); + } else { + cb(new Error("Le fichier doit être une image")); + } + }, +}); + +/** + * Route d'upload d'images vers S3 + * POST /api/upload-image + * Body: multipart/form-data avec un champ "file" + */ +router.post("/", upload.single("file"), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + message: "Aucun fichier fourni", + }); + } + + const file = req.file; + + // Générer un nom de fichier unique + const randomSuffix = randomBytes(8).toString("hex"); + const extension = file.originalname.split(".").pop() || "jpg"; + const fileName = `attestations/${Date.now()}-${randomSuffix}.${extension}`; + + // Upload vers S3 + const result = await storagePut( + fileName, + file.buffer, + file.mimetype + ); + + return res.json({ + success: true, + url: result.url, + s3Key: fileName, + message: "Image uploadée avec succès", + }); + } catch (error) { + console.error("[UploadImage] Error:", error); + return res.status(500).json({ + success: false, + message: "Erreur lors de l'upload de l'image", + }); + } +}); + +export default router; diff --git a/deployment-package/source_server/_core/vite.ts b/deployment-package/source_server/_core/vite.ts new file mode 100644 index 0000000..86d07f6 --- /dev/null +++ b/deployment-package/source_server/_core/vite.ts @@ -0,0 +1,67 @@ +import express, { type Express } from "express"; +import fs from "fs"; +import { type Server } from "http"; +import { nanoid } from "nanoid"; +import path from "path"; +import { createServer as createViteServer } from "vite"; +import viteConfig from "../../vite.config"; + +export async function setupVite(app: Express, server: Server) { + const serverOptions = { + middlewareMode: true, + hmr: { server }, + allowedHosts: true as const, + }; + + const vite = await createViteServer({ + ...viteConfig, + configFile: false, + server: serverOptions, + appType: "custom", + }); + + app.use(vite.middlewares); + app.use("*", async (req, res, next) => { + const url = req.originalUrl; + + try { + const clientTemplate = path.resolve( + import.meta.dirname, + "../..", + "client", + "index.html" + ); + + // always reload the index.html file from disk incase it changes + let template = await fs.promises.readFile(clientTemplate, "utf-8"); + template = template.replace( + `src="/src/main.tsx"`, + `src="/src/main.tsx?v=${nanoid()}"` + ); + const page = await vite.transformIndexHtml(url, template); + res.status(200).set({ "Content-Type": "text/html" }).end(page); + } catch (e) { + vite.ssrFixStacktrace(e as Error); + next(e); + } + }); +} + +export function serveStatic(app: Express) { + const distPath = + process.env.NODE_ENV === "development" + ? path.resolve(import.meta.dirname, "../..", "dist", "public") + : path.resolve(import.meta.dirname, "public"); + if (!fs.existsSync(distPath)) { + console.error( + `Could not find the build directory: ${distPath}, make sure to build the client first` + ); + } + + app.use(express.static(distPath)); + + // fall through to index.html if the file doesn't exist + app.use("*", (_req, res) => { + res.sendFile(path.resolve(distPath, "index.html")); + }); +} diff --git a/deployment-package/source_server/_core/voiceTranscription.ts b/deployment-package/source_server/_core/voiceTranscription.ts new file mode 100644 index 0000000..ed511f6 --- /dev/null +++ b/deployment-package/source_server/_core/voiceTranscription.ts @@ -0,0 +1,284 @@ +/** + * Voice transcription helper using internal Speech-to-Text service + * + * Frontend implementation guide: + * 1. Capture audio using MediaRecorder API + * 2. Upload audio to storage (e.g., S3) to get URL + * 3. Call transcription with the URL + * + * Example usage: + * ```tsx + * // Frontend component + * const transcribeMutation = trpc.voice.transcribe.useMutation({ + * onSuccess: (data) => { + * console.log(data.text); // Full transcription + * console.log(data.language); // Detected language + * console.log(data.segments); // Timestamped segments + * } + * }); + * + * // After uploading audio to storage + * transcribeMutation.mutate({ + * audioUrl: uploadedAudioUrl, + * language: 'en', // optional + * prompt: 'Transcribe the meeting' // optional + * }); + * ``` + */ +import { ENV } from "./env"; + +export type TranscribeOptions = { + audioUrl: string; // URL to the audio file (e.g., S3 URL) + language?: string; // Optional: specify language code (e.g., "en", "es", "zh") + prompt?: string; // Optional: custom prompt for the transcription +}; + +// Native Whisper API segment format +export type WhisperSegment = { + id: number; + seek: number; + start: number; + end: number; + text: string; + tokens: number[]; + temperature: number; + avg_logprob: number; + compression_ratio: number; + no_speech_prob: number; +}; + +// Native Whisper API response format +export type WhisperResponse = { + task: "transcribe"; + language: string; + duration: number; + text: string; + segments: WhisperSegment[]; +}; + +export type TranscriptionResponse = WhisperResponse; // Return native Whisper API response directly + +export type TranscriptionError = { + error: string; + code: "FILE_TOO_LARGE" | "INVALID_FORMAT" | "TRANSCRIPTION_FAILED" | "UPLOAD_FAILED" | "SERVICE_ERROR"; + details?: string; +}; + +/** + * Transcribe audio to text using the internal Speech-to-Text service + * + * @param options - Audio data and metadata + * @returns Transcription result or error + */ +export async function transcribeAudio( + options: TranscribeOptions +): Promise { + try { + // Step 1: Validate environment configuration + if (!ENV.forgeApiUrl) { + return { + error: "Voice transcription service is not configured", + code: "SERVICE_ERROR", + details: "BUILT_IN_FORGE_API_URL is not set" + }; + } + if (!ENV.forgeApiKey) { + return { + error: "Voice transcription service authentication is missing", + code: "SERVICE_ERROR", + details: "BUILT_IN_FORGE_API_KEY is not set" + }; + } + + // Step 2: Download audio from URL + let audioBuffer: Buffer; + let mimeType: string; + try { + const response = await fetch(options.audioUrl); + if (!response.ok) { + return { + error: "Failed to download audio file", + code: "INVALID_FORMAT", + details: `HTTP ${response.status}: ${response.statusText}` + }; + } + + audioBuffer = Buffer.from(await response.arrayBuffer()); + mimeType = response.headers.get('content-type') || 'audio/mpeg'; + + // Check file size (16MB limit) + const sizeMB = audioBuffer.length / (1024 * 1024); + if (sizeMB > 16) { + return { + error: "Audio file exceeds maximum size limit", + code: "FILE_TOO_LARGE", + details: `File size is ${sizeMB.toFixed(2)}MB, maximum allowed is 16MB` + }; + } + } catch (error) { + return { + error: "Failed to fetch audio file", + code: "SERVICE_ERROR", + details: error instanceof Error ? error.message : "Unknown error" + }; + } + + // Step 3: Create FormData for multipart upload to Whisper API + const formData = new FormData(); + + // Create a Blob from the buffer and append to form + const filename = `audio.${getFileExtension(mimeType)}`; + const audioBlob = new Blob([new Uint8Array(audioBuffer)], { type: mimeType }); + formData.append("file", audioBlob, filename); + + formData.append("model", "whisper-1"); + formData.append("response_format", "verbose_json"); + + // Add prompt - use custom prompt if provided, otherwise generate based on language + const prompt = options.prompt || ( + options.language + ? `Transcribe the user's voice to text, the user's working language is ${getLanguageName(options.language)}` + : "Transcribe the user's voice to text" + ); + formData.append("prompt", prompt); + + // Step 4: Call the transcription service + const baseUrl = ENV.forgeApiUrl.endsWith("/") + ? ENV.forgeApiUrl + : `${ENV.forgeApiUrl}/`; + + const fullUrl = new URL( + "v1/audio/transcriptions", + baseUrl + ).toString(); + + const response = await fetch(fullUrl, { + method: "POST", + headers: { + authorization: `Bearer ${ENV.forgeApiKey}`, + "Accept-Encoding": "identity", + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + return { + error: "Transcription service request failed", + code: "TRANSCRIPTION_FAILED", + details: `${response.status} ${response.statusText}${errorText ? `: ${errorText}` : ""}` + }; + } + + // Step 5: Parse and return the transcription result + const whisperResponse = await response.json() as WhisperResponse; + + // Validate response structure + if (!whisperResponse.text || typeof whisperResponse.text !== 'string') { + return { + error: "Invalid transcription response", + code: "SERVICE_ERROR", + details: "Transcription service returned an invalid response format" + }; + } + + return whisperResponse; // Return native Whisper API response directly + + } catch (error) { + // Handle unexpected errors + return { + error: "Voice transcription failed", + code: "SERVICE_ERROR", + details: error instanceof Error ? error.message : "An unexpected error occurred" + }; + } +} + +/** + * Helper function to get file extension from MIME type + */ +function getFileExtension(mimeType: string): string { + const mimeToExt: Record = { + 'audio/webm': 'webm', + 'audio/mp3': 'mp3', + 'audio/mpeg': 'mp3', + 'audio/wav': 'wav', + 'audio/wave': 'wav', + 'audio/ogg': 'ogg', + 'audio/m4a': 'm4a', + 'audio/mp4': 'm4a', + }; + + return mimeToExt[mimeType] || 'audio'; +} + +/** + * Helper function to get full language name from ISO code + */ +function getLanguageName(langCode: string): string { + const langMap: Record = { + 'en': 'English', + 'es': 'Spanish', + 'fr': 'French', + 'de': 'German', + 'it': 'Italian', + 'pt': 'Portuguese', + 'ru': 'Russian', + 'ja': 'Japanese', + 'ko': 'Korean', + 'zh': 'Chinese', + 'ar': 'Arabic', + 'hi': 'Hindi', + 'nl': 'Dutch', + 'pl': 'Polish', + 'tr': 'Turkish', + 'sv': 'Swedish', + 'da': 'Danish', + 'no': 'Norwegian', + 'fi': 'Finnish', + }; + + return langMap[langCode] || langCode; +} + +/** + * Example tRPC procedure implementation: + * + * ```ts + * // In server/routers.ts + * import { transcribeAudio } from "./_core/voiceTranscription"; + * + * export const voiceRouter = router({ + * transcribe: protectedProcedure + * .input(z.object({ + * audioUrl: z.string(), + * language: z.string().optional(), + * prompt: z.string().optional(), + * })) + * .mutation(async ({ input, ctx }) => { + * const result = await transcribeAudio(input); + * + * // Check if it's an error + * if ('error' in result) { + * throw new TRPCError({ + * code: 'BAD_REQUEST', + * message: result.error, + * cause: result, + * }); + * } + * + * // Optionally save transcription to database + * await db.insert(transcriptions).values({ + * userId: ctx.user.id, + * text: result.text, + * duration: result.duration, + * language: result.language, + * audioUrl: input.audioUrl, + * createdAt: new Date(), + * }); + * + * return result; + * }), + * }); + * ``` + */ diff --git a/deployment-package/source_server/analyticsDb.ts b/deployment-package/source_server/analyticsDb.ts new file mode 100644 index 0000000..5f1bcc4 --- /dev/null +++ b/deployment-package/source_server/analyticsDb.ts @@ -0,0 +1,147 @@ +/** + * Requêtes analytiques pour le tableau de bord + */ + +import { eq, sql, and, gte, lte, desc } from "drizzle-orm"; +import { getDb } from "./db"; +import { inscriptions, sequences, apprenants, datesFormation, formations } from "../drizzle/schema"; + +/** + * Récupère les statistiques d'inscriptions par mois + * @param startDate - Date de début de la période + * @param endDate - Date de fin de la période + */ +export async function getInscriptionsByMonth(startDate?: Date, endDate?: Date) { + const db = await getDb(); + if (!db) return []; + + const conditions = []; + if (startDate) { + conditions.push(gte(inscriptions.dateInscription, startDate)); + } + if (endDate) { + conditions.push(lte(inscriptions.dateInscription, endDate)); + } + + const result = await db + .select({ + mois: sql`DATE_FORMAT(${inscriptions.dateInscription}, '%Y-%m')`, + total: sql`COUNT(*)`, + confirmees: sql`SUM(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 ELSE 0 END)`, + listeAttente: sql`SUM(CASE WHEN ${inscriptions.statut} = 'liste_attente' THEN 1 ELSE 0 END)`, + annulees: sql`SUM(CASE WHEN ${inscriptions.statut} = 'annulee' THEN 1 ELSE 0 END)`, + }) + .from(inscriptions) + .where(conditions.length > 0 ? and(...conditions) : undefined) + .groupBy(sql`DATE_FORMAT(${inscriptions.dateInscription}, '%Y-%m')`) + .orderBy(sql`DATE_FORMAT(${inscriptions.dateInscription}, '%Y-%m')`); + + return result; +} + +/** + * Récupère le taux de remplissage des séquences par mois + */ +export async function getTauxRemplissageByMonth() { + const db = await getDb(); + if (!db) return []; + + const result = await db + .select({ + mois: sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`, + capaciteTotale: sql`SUM(${sequences.capaciteMax})`, + inscritsConfirmes: sql`SUM( + (SELECT COUNT(*) + FROM ${inscriptions} + WHERE ${inscriptions.sequenceId} = ${sequences.id} + AND ${inscriptions.statut} = 'confirmee') + )`, + tauxRemplissage: sql`ROUND( + (SUM( + (SELECT COUNT(*) + FROM ${inscriptions} + WHERE ${inscriptions.sequenceId} = ${sequences.id} + AND ${inscriptions.statut} = 'confirmee') + ) / SUM(${sequences.capaciteMax})) * 100, + 2 + )`, + }) + .from(sequences) + .groupBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`) + .orderBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`); + + return result; +} + +/** + * Récupère les statistiques de participation par établissement + */ +export async function getParticipationByEtablissement() { + const db = await getDb(); + if (!db) return []; + + const result = await db + .select({ + codeEtablissement: apprenants.codeEtablissement, + totalApprenants: sql`COUNT(DISTINCT ${apprenants.id})`, + totalInscriptions: sql`COUNT(${inscriptions.id})`, + inscriptionsConfirmees: sql`SUM(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 ELSE 0 END)`, + inscriptionsListeAttente: sql`SUM(CASE WHEN ${inscriptions.statut} = 'liste_attente' THEN 1 ELSE 0 END)`, + inscriptionsAnnulees: sql`SUM(CASE WHEN ${inscriptions.statut} = 'annulee' THEN 1 ELSE 0 END)`, + }) + .from(apprenants) + .leftJoin(inscriptions, eq(apprenants.id, inscriptions.apprenantId)) + .groupBy(apprenants.codeEtablissement) + .orderBy(desc(sql`COUNT(${inscriptions.id})`)); + + return result; +} + +/** + * Récupère les statistiques globales du tableau de bord + */ +export async function getGlobalStats() { + const db = await getDb(); + if (!db) return null; + + const [stats] = await db + .select({ + totalFormations: sql`(SELECT COUNT(*) FROM ${formations} WHERE ${formations.actif} = 1)`, + totalSequences: sql`(SELECT COUNT(*) FROM ${sequences})`, + sequencesOuvertes: sql`(SELECT COUNT(*) FROM ${sequences} WHERE ${sequences.statut} = 'ouverte')`, + totalApprenants: sql`(SELECT COUNT(*) FROM ${apprenants})`, + totalInscriptions: sql`(SELECT COUNT(*) FROM ${inscriptions})`, + inscriptionsConfirmees: sql`(SELECT COUNT(*) FROM ${inscriptions} WHERE ${inscriptions.statut} = 'confirmee')`, + inscriptionsListeAttente: sql`(SELECT COUNT(*) FROM ${inscriptions} WHERE ${inscriptions.statut} = 'liste_attente')`, + tauxRemplissageMoyen: sql`ROUND( + (SELECT COUNT(*) FROM ${inscriptions} WHERE ${inscriptions.statut} = 'confirmee') / + (SELECT SUM(${sequences.capaciteMax}) FROM ${sequences}) * 100, + 2 + )`, + }) + .from(sql`dual`); + + return stats; +} + +/** + * Récupère les statistiques de participation par fonction + */ +export async function getParticipationByFonction() { + const db = await getDb(); + if (!db) return []; + + const result = await db + .select({ + fonction: apprenants.fonction, + totalApprenants: sql`COUNT(DISTINCT ${apprenants.id})`, + totalInscriptions: sql`COUNT(${inscriptions.id})`, + inscriptionsConfirmees: sql`SUM(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 ELSE 0 END)`, + }) + .from(apprenants) + .leftJoin(inscriptions, eq(apprenants.id, inscriptions.apprenantId)) + .groupBy(apprenants.fonction) + .orderBy(desc(sql`COUNT(${inscriptions.id})`)); + + return result; +} diff --git a/deployment-package/source_server/analyticsExportService.ts b/deployment-package/source_server/analyticsExportService.ts new file mode 100644 index 0000000..392774c --- /dev/null +++ b/deployment-package/source_server/analyticsExportService.ts @@ -0,0 +1,145 @@ +/** + * Service d'export des rapports analytiques en PDF et Excel + */ + +import ExcelJS from 'exceljs'; +import { getGlobalStats, getInscriptionsByMonth, getTauxRemplissageByMonth, getParticipationByEtablissement, getParticipationByFonction } from './analyticsDb'; + +/** + * Génère un rapport Excel des statistiques analytiques + */ +export async function generateAnalyticsExcel(startDate?: Date, endDate?: Date): Promise { + const workbook = new ExcelJS.Workbook(); + + // Récupérer toutes les données + const globalStats = await getGlobalStats(); + const inscriptionsByMonth = await getInscriptionsByMonth(startDate, endDate); + const tauxRemplissage = await getTauxRemplissageByMonth(); + const participationEtablissement = await getParticipationByEtablissement(); + const participationFonction = await getParticipationByFonction(); + + // Feuille 1: Statistiques globales + const statsSheet = workbook.addWorksheet('Statistiques globales'); + statsSheet.columns = [ + { header: 'Indicateur', key: 'indicateur', width: 30 }, + { header: 'Valeur', key: 'valeur', width: 15 }, + ]; + + if (globalStats) { + statsSheet.addRows([ + { indicateur: 'Total Formations actives', valeur: globalStats.totalFormations }, + { indicateur: 'Total Séquences', valeur: globalStats.totalSequences }, + { indicateur: 'Séquences ouvertes', valeur: globalStats.sequencesOuvertes }, + { indicateur: 'Total Apprenants', valeur: globalStats.totalApprenants }, + { indicateur: 'Total Inscriptions', valeur: globalStats.totalInscriptions }, + { indicateur: 'Inscriptions confirmées', valeur: globalStats.inscriptionsConfirmees }, + { indicateur: 'Inscriptions en liste d\'attente', valeur: globalStats.inscriptionsListeAttente }, + { indicateur: 'Taux de remplissage moyen (%)', valeur: globalStats.tauxRemplissageMoyen }, + ]); + } + + // Feuille 2: Inscriptions par mois + const inscriptionsSheet = workbook.addWorksheet('Inscriptions par mois'); + inscriptionsSheet.columns = [ + { header: 'Mois', key: 'mois', width: 15 }, + { header: 'Total', key: 'total', width: 12 }, + { header: 'Confirmées', key: 'confirmees', width: 12 }, + { header: 'Liste d\'attente', key: 'listeAttente', width: 15 }, + { header: 'Annulées', key: 'annulees', width: 12 }, + ]; + + inscriptionsByMonth.forEach(item => { + inscriptionsSheet.addRow({ + mois: item.mois, + total: Number(item.total), + confirmees: Number(item.confirmees), + listeAttente: Number(item.listeAttente), + annulees: Number(item.annulees), + }); + }); + + // Feuille 3: Taux de remplissage + const tauxSheet = workbook.addWorksheet('Taux de remplissage'); + tauxSheet.columns = [ + { header: 'Mois', key: 'mois', width: 15 }, + { header: 'Capacité totale', key: 'capaciteTotale', width: 15 }, + { header: 'Inscrits confirmés', key: 'inscritsConfirmes', width: 18 }, + { header: 'Taux de remplissage (%)', key: 'tauxRemplissage', width: 20 }, + ]; + + tauxRemplissage.forEach(item => { + tauxSheet.addRow({ + mois: item.mois, + capaciteTotale: Number(item.capaciteTotale), + inscritsConfirmes: Number(item.inscritsConfirmes), + tauxRemplissage: Number(item.tauxRemplissage), + }); + }); + + // Feuille 4: Participation par établissement + const etablissementSheet = workbook.addWorksheet('Participation par établissement'); + etablissementSheet.columns = [ + { header: 'Code établissement', key: 'codeEtablissement', width: 20 }, + { header: 'Total apprenants', key: 'totalApprenants', width: 15 }, + { header: 'Total inscriptions', key: 'totalInscriptions', width: 18 }, + { header: 'Confirmées', key: 'confirmees', width: 12 }, + { header: 'Liste d\'attente', key: 'listeAttente', width: 15 }, + { header: 'Annulées', key: 'annulees', width: 12 }, + ]; + + participationEtablissement.forEach(item => { + etablissementSheet.addRow({ + codeEtablissement: item.codeEtablissement, + totalApprenants: Number(item.totalApprenants), + totalInscriptions: Number(item.totalInscriptions), + confirmees: Number(item.inscriptionsConfirmees), + listeAttente: Number(item.inscriptionsListeAttente), + annulees: Number(item.inscriptionsAnnulees), + }); + }); + + // Feuille 5: Participation par fonction + const fonctionSheet = workbook.addWorksheet('Participation par fonction'); + fonctionSheet.columns = [ + { header: 'Fonction', key: 'fonction', width: 20 }, + { header: 'Total apprenants', key: 'totalApprenants', width: 15 }, + { header: 'Total inscriptions', key: 'totalInscriptions', width: 18 }, + { header: 'Confirmées', key: 'confirmees', width: 12 }, + ]; + + participationFonction.forEach(item => { + fonctionSheet.addRow({ + fonction: item.fonction === 'directeur' ? 'Directeurs' : + item.fonction === 'chef_service' ? 'Chefs de service' : 'Autres', + totalApprenants: Number(item.totalApprenants), + totalInscriptions: Number(item.totalInscriptions), + confirmees: Number(item.inscriptionsConfirmees), + }); + }); + + // Styliser les en-têtes + [statsSheet, inscriptionsSheet, tauxSheet, etablissementSheet, fonctionSheet].forEach(sheet => { + sheet.getRow(1).font = { bold: true }; + sheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF3B82F6' }, + }; + sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } }; + }); + + // Générer le buffer + const buffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(buffer); +} + +/** + * Génère un rapport PDF des statistiques analytiques + * Note: Cette fonction retourne actuellement un placeholder + * Une implémentation complète nécessiterait une bibliothèque comme puppeteer ou pdfkit + */ +export async function generateAnalyticsPDF(startDate?: Date, endDate?: Date): Promise { + // Pour l'instant, retourner un message indiquant que la fonctionnalité est en développement + // Une implémentation complète nécessiterait de générer un HTML et de le convertir en PDF + throw new Error("L'export PDF sera implémenté dans une prochaine version"); +} diff --git a/deployment-package/source_server/attestationService.ts b/deployment-package/source_server/attestationService.ts new file mode 100644 index 0000000..b8a2dad --- /dev/null +++ b/deployment-package/source_server/attestationService.ts @@ -0,0 +1,430 @@ +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)); + + // 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 + (async () => { + try { + // Logo en haut à droite si disponible + if (config?.logoUrl) { + try { + const logoResponse = await fetch(config.logoUrl); + const logoBuffer = Buffer.from(await logoResponse.arrayBuffer()); + doc.image(logoBuffer, doc.page.width - 150, 50, { width: 100 }); + } catch (error) { + console.error("Erreur lors du chargement du logo:", error); + } + } + + // 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(3); + + // Signature si disponible + if (config?.signatureUrl) { + try { + const signatureResponse = await fetch(config.signatureUrl); + const signatureBuffer = Buffer.from(await signatureResponse.arrayBuffer()); + const signatureX = doc.page.width - 200; + const signatureY = doc.y; + doc.image(signatureBuffer, signatureX, signatureY, { width: 150, height: 50 }); + doc.moveDown(3); + } catch (error) { + console.error("Erreur lors du chargement de la signature:", error); + } + } + + 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)); +} + +/** + * 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); + } +} + +/** + * Génère une prévisualisation du modèle d'attestation avec des données fictives + */ +export async function genererPreviewAttestation(): Promise<{ pdfUrl: string }> { + const db = await getDb(); + if (!db) { + throw new Error("Base de données non disponible"); + } + + // Récupérer la configuration de l'attestation + const config = await getOrCreateConfigAttestation(); + if (!config) { + throw new Error("Configuration d'attestation introuvable"); + } + + // Données fictives pour la prévisualisation + const donneesFictives = { + apprenant: { + nom: "Dupont", + prenom: "Jean", + email: "jean.dupont@example.com", + }, + formation: { + nom: "Formation Manager Itinova", + nbJours: 5, + }, + sequence: { + nom: "Groupe A", + lieu: "Paris", + }, + dates: [ + { date: new Date("2026-02-10") }, + { date: new Date("2026-02-11") }, + { date: new Date("2026-02-12") }, + { date: new Date("2026-02-13") }, + { date: new Date("2026-02-14") }, + ], + }; + + // Créer le PDF + const doc = new PDFDocument({ size: "A4", margin: 50 }); + const chunks: Buffer[] = []; + + doc.on("data", (chunk) => chunks.push(chunk)); + + // En-tête avec logo si disponible + if (config.logoUrl) { + try { + const response = await fetch(config.logoUrl); + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + doc.image(buffer, 50, 45, { width: 100 }); + } catch (error) { + console.error("Erreur lors du chargement du logo:", error); + } + } + + // Titre + doc + .fontSize(24) + .font("Helvetica-Bold") + .text("ATTESTATION DE FORMATION", 0, 150, { align: "center" }); + + doc.moveDown(2); + + // Texte de l'attestation avec remplacement des variables + let texte = config.texteAttestation || "Nous attestons que {{nomComplet}} a suivi avec assiduité la formation \"{{nomFormation}}\" organisée du {{dateDebut}} au {{dateFin}}."; + + const dateDebut = format(donneesFictives.dates[0].date, "d MMMM yyyy", { locale: fr }); + const dateFin = format(donneesFictives.dates[donneesFictives.dates.length - 1].date, "d MMMM yyyy", { locale: fr }); + + texte = texte + .replace(/\{\{nomComplet\}\}/g, `${donneesFictives.apprenant.prenom} ${donneesFictives.apprenant.nom}`) + .replace(/\{\{nomFormation\}\}/g, donneesFictives.formation.nom) + .replace(/\{\{dateDebut\}\}/g, dateDebut) + .replace(/\{\{dateFin\}\}/g, dateFin) + .replace(/\{\{lieu\}\}/g, donneesFictives.sequence.lieu) + .replace(/\{\{nbJours\}\}/g, donneesFictives.formation.nbJours.toString()); + + doc + .fontSize(12) + .font("Helvetica") + .text(texte, { align: "justify", lineGap: 5 }); + + doc.moveDown(2); + + // Dates de formation + doc.fontSize(10).font("Helvetica-Bold").text("Dates de formation :"); + doc.font("Helvetica"); + donneesFictives.dates.forEach((d) => { + doc.text(`• ${format(d.date, "EEEE d MMMM yyyy", { locale: fr })}`, { indent: 20 }); + }); + + doc.moveDown(2); + + // Signature + doc.fontSize(10).font("Helvetica").text(`Fait à ${donneesFictives.sequence.lieu}, le ${format(new Date(), "d MMMM yyyy", { locale: fr })}`); + + doc.moveDown(1); + + if (config.signatureUrl) { + try { + const response = await fetch(config.signatureUrl); + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + doc.image(buffer, doc.x, doc.y, { width: 150 }); + doc.moveDown(3); + } catch (error) { + console.error("Erreur lors du chargement de la signature:", error); + } + } + + if (config.nomSignataire) { + doc.font("Helvetica-Bold").text(config.nomSignataire); + } + if (config.fonctionSignataire) { + doc.font("Helvetica").text(config.fonctionSignataire); + } + + // Finaliser le PDF + doc.end(); + + await new Promise((resolve) => doc.on("end", resolve)); + + const pdfBuffer = Buffer.concat(chunks); + + // Uploader sur S3 + const randomSuffix = crypto.randomBytes(8).toString("hex"); + const s3Key = `attestations/preview/preview-${randomSuffix}.pdf`; + const { url: pdfUrl } = await storagePut(s3Key, pdfBuffer, "application/pdf"); + + return { pdfUrl }; +} diff --git a/deployment-package/source_server/attestationsDb.ts b/deployment-package/source_server/attestationsDb.ts new file mode 100644 index 0000000..3bf46e8 --- /dev/null +++ b/deployment-package/source_server/attestationsDb.ts @@ -0,0 +1,93 @@ +import { desc, eq } from "drizzle-orm"; +import { getDb } from "./db"; +import { historiqueAttestations, sequences, apprenants, formations } from "../drizzle/schema"; + +/** + * Enregistrer un envoi d'attestation dans l'historique + */ +export async function enregistrerEnvoiAttestation(data: { + sequenceId: number; + apprenantId: number; + statut: "envoye" | "erreur"; + messageErreur?: string; + urlAttestation?: string; +}) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.insert(historiqueAttestations).values({ + sequenceId: data.sequenceId, + apprenantId: data.apprenantId, + statut: data.statut, + messageErreur: data.messageErreur || null, + urlAttestation: data.urlAttestation || null, + }); +} + +/** + * Récupérer l'historique des envois d'attestations + * Avec les informations de la séquence, formation et apprenant + */ +export async function getHistoriqueAttestations() { + const db = await getDb(); + if (!db) return []; + + const results = await db + .select({ + id: historiqueAttestations.id, + dateEnvoi: historiqueAttestations.dateEnvoi, + statut: historiqueAttestations.statut, + messageErreur: historiqueAttestations.messageErreur, + urlAttestation: historiqueAttestations.urlAttestation, + sequence: { + id: sequences.id, + nom: sequences.nom, + }, + formation: { + id: formations.id, + nom: formations.nom, + }, + apprenant: { + id: apprenants.id, + nom: apprenants.nom, + prenom: apprenants.prenom, + email: apprenants.email, + }, + }) + .from(historiqueAttestations) + .innerJoin(sequences, eq(historiqueAttestations.sequenceId, sequences.id)) + .innerJoin(formations, eq(sequences.formationId, formations.id)) + .innerJoin(apprenants, eq(historiqueAttestations.apprenantId, apprenants.id)) + .orderBy(desc(historiqueAttestations.dateEnvoi)); + + return results; +} + +/** + * Récupérer l'historique des attestations pour une séquence spécifique + */ +export async function getHistoriqueAttestationsBySequence(sequenceId: number) { + const db = await getDb(); + if (!db) return []; + + const results = await db + .select({ + id: historiqueAttestations.id, + dateEnvoi: historiqueAttestations.dateEnvoi, + statut: historiqueAttestations.statut, + messageErreur: historiqueAttestations.messageErreur, + urlAttestation: historiqueAttestations.urlAttestation, + apprenant: { + id: apprenants.id, + nom: apprenants.nom, + prenom: apprenants.prenom, + email: apprenants.email, + }, + }) + .from(historiqueAttestations) + .innerJoin(apprenants, eq(historiqueAttestations.apprenantId, apprenants.id)) + .where(eq(historiqueAttestations.sequenceId, sequenceId)) + .orderBy(desc(historiqueAttestations.dateEnvoi)); + + return results; +} diff --git a/deployment-package/source_server/dateUtils.ts b/deployment-package/source_server/dateUtils.ts new file mode 100644 index 0000000..6f1c294 --- /dev/null +++ b/deployment-package/source_server/dateUtils.ts @@ -0,0 +1,60 @@ +/** + * Utilitaires pour la gestion des dates + * Gère correctement les conversions entre heure locale et UTC + */ + +/** + * Convertit une chaîne datetime-local (format: "2025-01-15T14:30") en Date + * en construisant la date avec une chaîne ISO qui force l'heure locale + * Préserve l'heure locale sans conversion UTC + */ +export function parseLocalDateTime(dateTimeString: string): Date { + if (!dateTimeString) { + throw new Error('Date string is required'); + } + + // Le format datetime-local est "YYYY-MM-DDTHH:mm" + const match = dateTimeString.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); + + if (!match) { + throw new Error(`Invalid date format: ${dateTimeString}. Expected YYYY-MM-DDTHH:mm`); + } + + const [, year, month, day, hours, minutes] = match; + + // Créer un objet Date en utilisant le constructeur avec composants + // MySQL stocke en UTC, donc on doit compenser le décalage horaire + const date = new Date( + parseInt(year), + parseInt(month) - 1, + parseInt(day), + parseInt(hours), + parseInt(minutes), + 0 + ); + + // Compenser le décalage UTC en ajoutant le décalage du fuseau horaire + // Cela garantit que l'heure stockée en UTC correspond à l'heure locale saisie + const offset = date.getTimezoneOffset(); // en minutes (négatif pour Europe/Paris) + date.setMinutes(date.getMinutes() - offset); + + if (isNaN(date.getTime())) { + throw new Error(`Invalid date string: ${dateTimeString}`); + } + + return date; +} + +/** + * Formate une Date en chaîne datetime-local pour les inputs HTML + * Format: "YYYY-MM-DDTHH:mm" + */ +export function formatToLocalDateTime(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + return `${year}-${month}-${day}T${hours}:${minutes}`; +} diff --git a/deployment-package/source_server/db.ts b/deployment-package/source_server/db.ts new file mode 100644 index 0000000..dc0b2b3 --- /dev/null +++ b/deployment-package/source_server/db.ts @@ -0,0 +1,1272 @@ +import { eq, and, ne, sql } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/mysql2"; +import { + InsertUser, + users, + formations, + apprenants, + sequences, + datesFormation, + inscriptions, + passwordResetTokens, + InsertPasswordResetToken, + emailTemplates, + emailConfig, + InsertEmailConfig, + InsertSequence, + InsertDateFormation, + InsertInscription, + InsertFormation, + InsertApprenant, + Sequence, + DateFormation, + Apprenant, + Formation, + EmailTemplate, + InsertEmailTemplate, + formateurs, + InsertFormateur, + Formateur, + rappels, + InsertRappel, + Rappel, + rappelDates +} from "../drizzle/schema"; +import { ENV } from './_core/env'; + +let _db: ReturnType | null = null; + +// Lazily create the drizzle instance so local tooling can run without a DB. +export async function getDb() { + if (!_db && process.env.DATABASE_URL) { + try { + _db = drizzle(process.env.DATABASE_URL); + } catch (error) { + console.warn("[Database] Failed to connect:", error); + _db = null; + } + } + return _db; +} + +export async function upsertUser(user: InsertUser): Promise { + if (!user.openId) { + throw new Error("User openId is required for upsert"); + } + + const db = await getDb(); + if (!db) { + console.warn("[Database] Cannot upsert user: database not available"); + return; + } + + try { + const values: InsertUser = { + openId: user.openId, + }; + const updateSet: Record = {}; + + const textFields = ["name", "email", "loginMethod"] as const; + type TextField = (typeof textFields)[number]; + + const assignNullable = (field: TextField) => { + const value = user[field]; + if (value === undefined) return; + const normalized = value ?? null; + values[field] = normalized; + updateSet[field] = normalized; + }; + + textFields.forEach(assignNullable); + + if (user.lastSignedIn !== undefined) { + values.lastSignedIn = user.lastSignedIn; + updateSet.lastSignedIn = user.lastSignedIn; + } + if (user.role !== undefined) { + values.role = user.role; + updateSet.role = user.role; + } else if (user.openId === ENV.ownerOpenId) { + values.role = 'admin'; + updateSet.role = 'admin'; + } + + if (!values.lastSignedIn) { + values.lastSignedIn = new Date(); + } + + if (Object.keys(updateSet).length === 0) { + updateSet.lastSignedIn = new Date(); + } + + await db.insert(users).values(values).onDuplicateKeyUpdate({ + set: updateSet, + }); + } catch (error) { + console.error("[Database] Failed to upsert user:", error); + throw error; + } +} + +export async function getUserByOpenId(openId: string) { + const db = await getDb(); + if (!db) { + console.warn("[Database] Cannot get user: database not available"); + return undefined; + } + + const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1); + + return result.length > 0 ? result[0] : undefined; +} + +// ==================== FORMATIONS ==================== + +export async function createFormation(data: InsertFormation) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db.insert(formations).values(data); + return result; +} + +export async function getFormations() { + const db = await getDb(); + if (!db) return []; + + return await db.select().from(formations); +} + +export async function getFormationsByFormateur(formateurId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer les IDs de formations uniques pour lesquelles le formateur a des séquences + const seqs = await db.select({ formationId: sequences.formationId }) + .from(sequences) + .where(eq(sequences.formateurId, formateurId)); + + const formationIds = Array.from(new Set(seqs.map(s => s.formationId))); + + if (formationIds.length === 0) return []; + + // Récupérer les formations correspondantes + const result = await db.select().from(formations).where( + sql`${formations.id} IN (${sql.join(formationIds.map(id => sql`${id}`), sql`, `)})` + ); + + return result; +} + +export async function getFormationById(id: number) { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select().from(formations).where(eq(formations.id, id)).limit(1); + return result.length > 0 ? result[0] : undefined; +} + +export async function getFormationByLien(lien: string) { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select().from(formations).where(eq(formations.lienUnique, lien)).limit(1); + return result.length > 0 ? result[0] : undefined; +} + +export async function updateFormation(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.update(formations).set(data).where(eq(formations.id, id)); +} + +export async function deleteFormation(id: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(formations).where(eq(formations.id, id)); +} + +// ==================== SÉQUENCES ==================== + +export async function createSequence(data: InsertSequence) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db.insert(sequences).values(data); + return result; +} + +export async function getSequences() { + const db = await getDb(); + if (!db) return []; + + return await db.select().from(sequences); +} + +export async function getSequencesWithFormation() { + const db = await getDb(); + if (!db) return []; + + const seqs = await db.select().from(sequences); + + // Récupérer les informations de formation et de formateur pour chaque séquence + return await Promise.all( + seqs.map(async (seq) => { + const formation = await db.select().from(formations).where(eq(formations.id, seq.formationId)).limit(1); + + let formateur = null; + if (seq.formateurId) { + const formateurResult = await db.select().from(formateurs).where(eq(formateurs.id, seq.formateurId)).limit(1); + if (formateurResult.length > 0) { + formateur = { id: formateurResult[0].id, nom: formateurResult[0].nom }; + } + } + + return { + ...seq, + formation: formation.length > 0 ? { id: formation[0].id, nom: formation[0].nom } : null, + formateur, + }; + }) + ); +} + +export async function getSequencesByFormateur(formateurId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer uniquement les séquences de ce formateur + const seqs = await db.select().from(sequences).where(eq(sequences.formateurId, formateurId)); + + // Récupérer les informations de formation et de formateur pour chaque séquence + return await Promise.all( + seqs.map(async (seq) => { + const formation = await db.select().from(formations).where(eq(formations.id, seq.formationId)).limit(1); + + let formateur = null; + if (seq.formateurId) { + const formateurResult = await db.select().from(formateurs).where(eq(formateurs.id, seq.formateurId)).limit(1); + if (formateurResult.length > 0) { + formateur = { id: formateurResult[0].id, nom: formateurResult[0].nom }; + } + } + + return { + ...seq, + formation: formation.length > 0 ? { id: formation[0].id, nom: formation[0].nom } : null, + formateur, + }; + }) + ); +} + +export async function getSequenceById(id: number): Promise { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select().from(sequences).where(eq(sequences.id, id)).limit(1); + return result.length > 0 ? result[0] : undefined; +} + +export async function getSequencesByFormation(formationId: number) { + const db = await getDb(); + if (!db) return []; + + return await db.select().from(sequences).where(eq(sequences.formationId, formationId)); +} + +export async function updateSequence(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.update(sequences).set(data).where(eq(sequences.id, id)); +} + +export async function deleteSequence(id: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(sequences).where(eq(sequences.id, id)); +} + +// ==================== DATES DE FORMATION ==================== + +export async function createDateFormation(data: InsertDateFormation) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db.insert(datesFormation).values(data); + return result; +} + +export async function getDatesBySequence(sequenceId: number): Promise { + const db = await getDb(); + if (!db) return []; + + const results = await db.select() + .from(datesFormation) + .where(eq(datesFormation.sequenceId, sequenceId)) + .orderBy(datesFormation.ordre); + + return results; +} + +export async function deleteDatesBySequence(sequenceId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(datesFormation).where(eq(datesFormation.sequenceId, sequenceId)); +} + +// ==================== APPRENANTS ==================== + +export async function createApprenant(data: InsertApprenant) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db.insert(apprenants).values(data); + return result; +} + +export async function getApprenants() { + const db = await getDb(); + if (!db) return []; + + const apprenantsData = await db.select().from(apprenants); + + // Pour chaque apprenant, récupérer ses inscriptions + const apprenantsWithInscriptions = await Promise.all( + apprenantsData.map(async (apprenant) => { + const inscriptionsData = await db + .select() + .from(inscriptions) + .where(eq(inscriptions.apprenantId, apprenant.id)); + + return { + ...apprenant, + inscriptions: inscriptionsData, + }; + }) + ); + + return apprenantsWithInscriptions; +} + +export async function getApprenantsByFormateur(formateurId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer les IDs de séquences du formateur + const seqs = await db.select({ id: sequences.id }) + .from(sequences) + .where(eq(sequences.formateurId, formateurId)); + + const sequenceIds = seqs.map(s => s.id); + + if (sequenceIds.length === 0) return []; + + // Récupérer les IDs d'apprenants inscrits à ces séquences + const inscriptionsData = await db.select({ apprenantId: inscriptions.apprenantId }) + .from(inscriptions) + .where( + sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})` + ); + + const apprenantIds = Array.from(new Set(inscriptionsData.map(i => i.apprenantId))); + + if (apprenantIds.length === 0) return []; + + // Récupérer les apprenants correspondants + const apprenantsData = await db.select().from(apprenants).where( + sql`${apprenants.id} IN (${sql.join(apprenantIds.map(id => sql`${id}`), sql`, `)})` + ); + + // Pour chaque apprenant, récupérer ses inscriptions (filtrées par les séquences du formateur) + const apprenantsWithInscriptions = await Promise.all( + apprenantsData.map(async (apprenant) => { + const inscriptionsFiltered = await db + .select() + .from(inscriptions) + .where( + and( + eq(inscriptions.apprenantId, apprenant.id), + sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})` + ) + ); + + return { + ...apprenant, + inscriptions: inscriptionsFiltered, + }; + }) + ); + + return apprenantsWithInscriptions; +} + +export async function getApprenantById(id: number): Promise { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select().from(apprenants).where(eq(apprenants.id, id)).limit(1); + return result.length > 0 ? result[0] : undefined; +} + +export async function getApprenantByEmail(email: string) { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select().from(apprenants).where(eq(apprenants.email, email)).limit(1); + return result.length > 0 ? result[0] : undefined; +} + +export async function updateApprenant(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.update(apprenants).set(data).where(eq(apprenants.id, id)); +} + +export async function deleteApprenant(id: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(apprenants).where(eq(apprenants.id, id)); +} + +// ==================== INSCRIPTIONS ==================== + +export async function createInscription(data: InsertInscription) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db.insert(inscriptions).values(data); + return result; +} + +export async function getAllInscriptions() { + const db = await getDb(); + if (!db) return []; + + const results = await db.select({ + inscription: inscriptions, + apprenant: apprenants, + }) + .from(inscriptions) + .leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)); + + return results; +} + +export async function getInscriptionsBySequence(sequenceId: number) { + const db = await getDb(); + if (!db) return []; + + const results = await db.select({ + inscription: inscriptions, + apprenant: apprenants, + }) + .from(inscriptions) + .leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .where(and( + eq(inscriptions.sequenceId, sequenceId), + ne(inscriptions.statut, 'annulee') + )); + + return results; +} + +export async function getInscriptionsByApprenant(apprenantId: number) { + const db = await getDb(); + if (!db) return []; + + const results = await db.select({ + inscription: inscriptions, + sequence: sequences, + }) + .from(inscriptions) + .leftJoin(sequences, eq(inscriptions.sequenceId, sequences.id)) + .where(and( + eq(inscriptions.apprenantId, apprenantId), + ne(inscriptions.statut, 'annulee') + )); + + return results; +} + +export async function checkExistingInscription(apprenantId: number, sequenceId: number) { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select() + .from(inscriptions) + .where(and( + eq(inscriptions.apprenantId, apprenantId), + eq(inscriptions.sequenceId, sequenceId), + ne(inscriptions.statut, 'annulee') + )) + .limit(1); + + return result.length > 0 ? result[0] : undefined; +} + +export async function countInscriptionsBySequence(sequenceId: number, statut?: string) { + const db = await getDb(); + if (!db) return 0; + + const conditions = [eq(inscriptions.sequenceId, sequenceId)]; + if (statut) { + conditions.push(eq(inscriptions.statut, statut as any)); + } + + const result = await db.select({ count: sql`count(*)` }) + .from(inscriptions) + .where(and(...conditions)); + + return result[0]?.count || 0; +} + +export async function getInscriptionById(id: number) { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select().from(inscriptions).where(eq(inscriptions.id, id)).limit(1); + return result.length > 0 ? result[0] : undefined; +} + +export async function updateInscription(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.update(inscriptions).set(data).where(eq(inscriptions.id, id)); +} + +export async function deleteInscription(id: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(inscriptions).where(eq(inscriptions.id, id)); +} + +// ==================== GESTION DES UTILISATEURS ==================== + +export async function createUser(data: InsertUser) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Hasher le mot de passe si fourni + if (data.password) { + const bcrypt = await import('bcryptjs'); + data.password = await bcrypt.hash(data.password, 10); + } + + const result = await db.insert(users).values(data); + return result; +} + +export async function getAllUsers() { + const db = await getDb(); + if (!db) return []; + + return await db.select().from(users); +} + +export async function getUserById(id: number) { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select().from(users).where(eq(users.id, id)).limit(1); + return result.length > 0 ? result[0] : undefined; +} + +export async function updateUser(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Hasher le mot de passe si fourni + if (data.password) { + const bcrypt = await import('bcryptjs'); + data.password = await bcrypt.hash(data.password, 10); + } + + await db.update(users).set(data).where(eq(users.id, id)); +} + +export async function toggleUserStatus(id: number, isActive: boolean) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.update(users).set({ isActive }).where(eq(users.id, id)); +} + +export async function deleteUser(id: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(users).where(eq(users.id, id)); +} + + +// ==================== GESTION DES TOKENS DE RÉINITIALISATION ==================== + +export async function createPasswordResetToken(userId: number, token: string, expiresAt: Date) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db.insert(passwordResetTokens).values({ + userId, + token, + expiresAt, + used: false, + }); + return result; +} + +export async function getPasswordResetToken(token: string) { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select() + .from(passwordResetTokens) + .where(eq(passwordResetTokens.token, token)) + .limit(1); + + return result.length > 0 ? result[0] : undefined; +} + +export async function markTokenAsUsed(tokenId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.update(passwordResetTokens) + .set({ used: true }) + .where(eq(passwordResetTokens.id, tokenId)); +} + +export async function deleteExpiredTokens() { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const now = new Date(); + await db.delete(passwordResetTokens) + .where(sql`${passwordResetTokens.expiresAt} < ${now}`); +} + +// ============================================ +// Email Templates Management +// ============================================ + +/** + * Récupère tous les templates d'emails + */ +export async function getAllEmailTemplates() { + const db = await getDb(); + if (!db) return []; + + return await db.select().from(emailTemplates); +} + +/** + * Récupère un template par son type + */ +export async function getEmailTemplateByType(type: string) { + const db = await getDb(); + if (!db) return null; + + const results = await db.select().from(emailTemplates).where(eq(emailTemplates.type, type)).limit(1); + return results.length > 0 ? results[0] : null; +} + +/** + * Crée ou met à jour un template d'email + */ +export async function upsertEmailTemplate(template: InsertEmailTemplate) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const existing = await getEmailTemplateByType(template.type); + + if (existing) { + // Mise à jour + await db.update(emailTemplates) + .set({ + name: template.name, + logoUrl: template.logoUrl, + primaryColor: template.primaryColor, + headerBgColor: template.headerBgColor, + headerTextColor: template.headerTextColor, + headerTitle: template.headerTitle, + bodyContent: template.bodyContent, + footerText: template.footerText, + active: template.active, + updatedAt: new Date(), + }) + .where(eq(emailTemplates.type, template.type)); + + return await getEmailTemplateByType(template.type); + } else { + // Création + await db.insert(emailTemplates).values(template); + return await getEmailTemplateByType(template.type); + } +} + +/** + * Supprime un template d'email + */ +export async function deleteEmailTemplate(type: string) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(emailTemplates).where(eq(emailTemplates.type, type)); +} + +/** + * Initialise les templates par défaut si la table est vide + */ +export async function initializeDefaultEmailTemplates() { + const db = await getDb(); + if (!db) return; + + const existing = await getAllEmailTemplates(); + if (existing.length > 0) return; // Déjà initialisé + + const defaultTemplates: InsertEmailTemplate[] = [ + { + type: "inscription", + name: "Confirmation d'inscription", + logoUrl: null, + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", + active: true, + }, + { + type: "teaser", + name: "Email teaser", + logoUrl: null, + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", + active: true, + }, + { + type: "rappel", + name: "Rappel J-7", + logoUrl: null, + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + bodyContent: `

Rappel : Votre formation commence bientôt !

+

Bonjour {{prenomApprenant}} {{nomApprenant}},

+ +

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

+ +
+

Séquence : {{nomSequence}}

+

Dates :

+ {{datesHTML}} +

Lieu : {{lieu}}

+

Formateur : {{formateur}}

+
+ +

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

+ +

À très bientôt !

`, + footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", + active: true, + }, + { + type: "rappelJ1", + name: "Rappel J-1", + logoUrl: null, + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + bodyContent: `

Rappel : Votre formation commence demain !

+

Bonjour {{prenomApprenant}} {{nomApprenant}},

+ +

Nous vous rappelons que votre formation {{nomFormation}} commence demain.

+ +
+

Séquence : {{nomSequence}}

+

Dates :

+ {{datesHTML}} +

Lieu : {{lieu}}

+

Formateur : {{formateur}}

+
+ +

Merci de vous présenter à l'heure indiquée.

+

N'oubliez pas d'apporter le matériel nécessaire.

+ +

À demain !

`, + footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", + active: true, + }, + { + type: "rappel3", + name: "Rappel 3", + logoUrl: null, + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + bodyContent: `

Rappel : Votre formation commence bientôt !

+

Bonjour {{prenomApprenant}} {{nomApprenant}},

+ +

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

+ +
+

Séquence : {{nomSequence}}

+

Dates :

+ {{datesHTML}} +

Lieu : {{lieu}}

+

Formateur : {{formateur}}

+
+ +

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

+ +

À très bientôt !

`, + footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", + active: true, + }, + { + type: "rappel4", + name: "Rappel 4", + logoUrl: null, + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + bodyContent: `

Rappel : Votre formation commence bientôt !

+

Bonjour {{prenomApprenant}} {{nomApprenant}},

+ +

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

+ +
+

Séquence : {{nomSequence}}

+

Dates :

+ {{datesHTML}} +

Lieu : {{lieu}}

+

Formateur : {{formateur}}

+
+ +

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

+ +

À très bientôt !

`, + footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", + active: true, + }, + { + type: "rappel5", + name: "Rappel 5", + logoUrl: null, + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + bodyContent: `

Rappel : Votre formation commence bientôt !

+

Bonjour {{prenomApprenant}} {{nomApprenant}},

+ +

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

+ +
+

Séquence : {{nomSequence}}

+

Dates :

+ {{datesHTML}} +

Lieu : {{lieu}}

+

Formateur : {{formateur}}

+
+ +

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

+ +

À très bientôt !

`, + footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", + active: true, + }, + { + type: "rappel6", + name: "Rappel 6", + logoUrl: null, + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + bodyContent: `

Rappel : Votre formation commence bientôt !

+

Bonjour {{prenomApprenant}} {{nomApprenant}},

+ +

Nous vous rappelons que votre formation {{nomFormation}} commence dans une semaine.

+ +
+

Séquence : {{nomSequence}}

+

Dates :

+ {{datesHTML}} +

Lieu : {{lieu}}

+

Formateur : {{formateur}}

+
+ +

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

+ +

À très bientôt !

`, + footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", + active: true, + }, + { + type: "reset_password", + name: "Réinitialisation de mot de passe", + logoUrl: null, + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.", + active: true, + }, + ]; + + for (const template of defaultTemplates) { + await db.insert(emailTemplates).values(template); + } + + console.log("[DB] Templates d'emails par défaut initialisés"); +} + + +// ==================== Email Config ==================== + +/** + * Récupère la configuration email active + */ +export async function getActiveEmailConfig() { + const db = await getDb(); + if (!db) return undefined; + + const result = await db + .select() + .from(emailConfig) + .where(eq(emailConfig.active, true)) + .limit(1); + + return result.length > 0 ? result[0] : undefined; +} + +/** + * Crée ou met à jour la configuration email + */ +export async function upsertEmailConfig(data: InsertEmailConfig) { + const db = await getDb(); + if (!db) return; + + // Vérifier s'il existe déjà une configuration active + const existing = await db + .select() + .from(emailConfig) + .where(eq(emailConfig.active, true)) + .limit(1); + + if (existing.length > 0) { + // Mettre à jour la configuration existante + await db + .update(emailConfig) + .set({ + ...data, + updatedAt: new Date(), + }) + .where(eq(emailConfig.id, existing[0].id)); + } else { + // Créer une nouvelle configuration + await db.insert(emailConfig).values({ + ...data, + active: true, + }); + } +} + +/** + * Met à jour la configuration email + */ +export async function updateEmailConfig(id: number, data: Partial) { + const db = await getDb(); + if (!db) return; + + await db + .update(emailConfig) + .set({ + ...data, + updatedAt: new Date(), + }) + .where(eq(emailConfig.id, id)); +} + + +// ==================== FORMATEURS ==================== + +export async function createFormateur(data: InsertFormateur) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db.insert(formateurs).values(data); + return result; +} + +export async function getFormateurs() { + const db = await getDb(); + if (!db) return []; + + return await db.select().from(formateurs); +} + +export async function getFormateurById(id: number): Promise { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select().from(formateurs).where(eq(formateurs.id, id)).limit(1); + return result.length > 0 ? result[0] : undefined; +} + +export async function updateFormateur(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.update(formateurs).set(data).where(eq(formateurs.id, id)); +} + +export async function deleteFormateur(id: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(formateurs).where(eq(formateurs.id, id)); +} + + +// ==================== RAPPELS ==================== + +export async function createRappel(data: InsertRappel & { fichier?: { nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null }) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Construire un objet d'insertion explicite + const insertData: Partial = { + nom: data.nom, + templateType: data.templateType, + timing: data.timing, + joursAvant: data.joursAvant, + heureEnvoi: data.heureEnvoi, + actif: data.actif, + }; + + // Ajouter les champs de fichier si présents + if (data.fichier) { + insertData.nomFichier = data.fichier.nomFichier; + insertData.urlFichier = data.fichier.urlFichier; + insertData.s3Key = data.fichier.s3Key; + insertData.typeFichier = data.fichier.typeFichier; + insertData.tailleFichier = data.fichier.tailleFichier; + } + + const result = await db.insert(rappels).values(insertData as InsertRappel); + return result; +} + +export async function getRappels() { + const db = await getDb(); + if (!db) return []; + + const allRappels = await db.select().from(rappels).orderBy(rappels.joursAvant); + + // Pour chaque rappel, récupérer les dates de formation associées avec leurs infos + const rappelsWithDates = await Promise.all( + allRappels.map(async (rappel) => { + const dateFormationIds = await getRappelDates(rappel.id); + + // Récupérer les détails des dates de formation + const datesDetails = await Promise.all( + dateFormationIds.map(async (dateId) => { + const dateFormationResult = await db.select().from(datesFormation).where(eq(datesFormation.id, dateId)).limit(1); + if (dateFormationResult.length === 0) return null; + + const dateFormation = dateFormationResult[0]; + const sequenceResult = await db.select().from(sequences).where(eq(sequences.id, dateFormation.sequenceId)).limit(1); + if (sequenceResult.length === 0) return null; + + const sequence = sequenceResult[0]; + const formationResult = await db.select().from(formations).where(eq(formations.id, sequence.formationId)).limit(1); + if (formationResult.length === 0) return null; + + const formation = formationResult[0]; + + return { + dateFormationId: dateFormation.id, + dateDebut: dateFormation.dateDebut, + dateFin: dateFormation.dateFin, + ordre: dateFormation.ordre, + sequenceId: sequence.id, + sequenceNom: sequence.nom, + formationId: formation.id, + formationNom: formation.nom, + }; + }) + ); + + return { + ...rappel, + dateFormationIds, + datesDetails: datesDetails.filter(d => d !== null), + }; + }) + ); + + return rappelsWithDates; +} + +export async function getRappelById(id: number): Promise { + const db = await getDb(); + if (!db) return undefined; + + const result = await db.select().from(rappels).where(eq(rappels.id, id)).limit(1); + return result.length > 0 ? result[0] : undefined; +} + +export async function getActiveRappels(): Promise { + const db = await getDb(); + if (!db) return []; + + return await db.select().from(rappels).where(eq(rappels.actif, true)); +} + +export async function updateRappel(id: number, data: Partial & { fichier?: { nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null }) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const updateData: any = { ...data }; + + // Gérer les champs de fichier + if ('fichier' in data) { + delete updateData.fichier; + if (data.fichier) { + updateData.nomFichier = data.fichier.nomFichier; + updateData.urlFichier = data.fichier.urlFichier; + updateData.s3Key = data.fichier.s3Key; + updateData.typeFichier = data.fichier.typeFichier; + updateData.tailleFichier = data.fichier.tailleFichier; + } else { + // Supprimer le fichier + updateData.nomFichier = null; + updateData.urlFichier = null; + updateData.s3Key = null; + updateData.typeFichier = null; + updateData.tailleFichier = null; + } + } + + await db.update(rappels).set({ + ...updateData, + updatedAt: new Date(), + }).where(eq(rappels.id, id)); +} + +export async function deleteRappel(id: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Supprimer d'abord les associations + await db.delete(rappelDates).where(eq(rappelDates.rappelId, id)); + // Puis le rappel + await db.delete(rappels).where(eq(rappels.id, id)); +} + +// ==================== RAPPEL DATES ==================== + +export async function createRappelDates(rappelId: number, dateFormationIds: number[]) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + if (dateFormationIds.length === 0) return; + + const values = dateFormationIds.map(dateFormationId => ({ + rappelId, + dateFormationId, + })); + + await db.insert(rappelDates).values(values); +} + +export async function deleteRappelDates(rappelId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(rappelDates).where(eq(rappelDates.rappelId, rappelId)); +} + +export async function getRappelDates(rappelId: number): Promise { + const db = await getDb(); + if (!db) return []; + + const results = await db.select().from(rappelDates).where(eq(rappelDates.rappelId, rappelId)); + return results.map(r => r.dateFormationId); +} + +export async function updateRappelExecution(id: number) { + const db = await getDb(); + if (!db) return; + + await db.update(rappels).set({ + derniereExecution: new Date(), + }).where(eq(rappels.id, id)); +} + +export async function getRappelsByDateFormation(dateFormationId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer les IDs des rappels associés à cette date + const rappelDatesResults = await db.select().from(rappelDates).where(eq(rappelDates.dateFormationId, dateFormationId)); + const rappelIds = rappelDatesResults.map(rd => rd.rappelId); + + if (rappelIds.length === 0) { + // Retourner tous les rappels actifs qui n'ont pas de dates spécifiques (rappels globaux) + const allRappels = await db.select().from(rappels).where(eq(rappels.actif, true)); + const globalRappels = []; + + for (const rappel of allRappels) { + const dates = await getRappelDates(rappel.id); + if (dates.length === 0) { + globalRappels.push(rappel); + } + } + + return globalRappels; + } + + // Récupérer les détails des rappels + const rappelsResults = await Promise.all( + rappelIds.map(async (id) => { + const rappelResult = await db.select().from(rappels).where(eq(rappels.id, id)).limit(1); + return rappelResult.length > 0 ? rappelResult[0] : null; + }) + ); + + // Filtrer les null et retourner + return rappelsResults.filter((r): r is Rappel => r !== null); +} + + +// ==================== UTILISATEURS ADMIN ==================== + +/** + * Récupère tous les utilisateurs avec le rôle admin + */ +export async function getAdminUsers() { + const db = await getDb(); + if (!db) return []; + + return await db.select().from(users).where(eq(users.role, 'admin')); +} diff --git a/deployment-package/source_server/emailPreview.ts b/deployment-package/source_server/emailPreview.ts new file mode 100644 index 0000000..9628c4d --- /dev/null +++ b/deployment-package/source_server/emailPreview.ts @@ -0,0 +1,177 @@ +/** + * Génération d'aperçus d'emails avec données réelles + */ + +import * as db from "./db"; +import { generateEmailFromTemplate } from "./emailTemplateGenerator"; + +export interface EmailPreviewParams { + sequenceId: number; + type: 'teaser' | 'rappel' | 'rappel_j1'; + apprenantId?: number; // Si non fourni, prendre le premier apprenant inscrit +} + +export async function generateEmailPreview(params: EmailPreviewParams): Promise<{ + html: string; + subject: string; + recipient: { + email: string; + nom: string; + prenom: string; + }; +}> { + // Récupérer la séquence + const sequence = await db.getSequenceById(params.sequenceId); + if (!sequence) { + throw new Error('Séquence non trouvée'); + } + + // Récupérer la formation + const formation = await db.getFormationById(sequence.formationId); + if (!formation) { + throw new Error('Formation non trouvée'); + } + + // Récupérer les dates + const dates = await db.getDatesBySequence(sequence.id); + if (dates.length === 0) { + throw new Error('Aucune date trouvée pour cette séquence'); + } + + // Récupérer le formateur si disponible + let formateurNom: string | undefined; + if (sequence.formateurId) { + const formateur = await db.getFormateurById(sequence.formateurId); + formateurNom = formateur?.nom; + } + + // Récupérer un apprenant inscrit + const inscriptions = await db.getInscriptionsBySequence(params.sequenceId); + const confirmedInscriptions = inscriptions.filter(i => i.inscription.statut === 'confirmee' && i.apprenant); + + if (confirmedInscriptions.length === 0) { + throw new Error('Aucun apprenant confirmé trouvé pour cette séquence'); + } + + // Utiliser l'apprenant spécifié ou le premier de la liste + let selectedInscription = confirmedInscriptions[0]; + if (params.apprenantId) { + const found = confirmedInscriptions.find(i => i.apprenant!.id === params.apprenantId); + if (found) { + selectedInscription = found; + } + } + + const apprenant = selectedInscription.apprenant!; + + // Préparer les données pour l'email + const fonctionLabel = apprenant.fonction === 'directeur' ? 'Directeur' : + apprenant.fonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${apprenant.prenom} ${apprenant.nom}` : apprenant.prenom; + + const datesHTML = dates.map(date => ` +
+ Date ${date.ordre} : ${new Date(date.dateDebut).toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + })} +
+ `).join(''); + + // Générer le contenu selon le type + let content: string; + let subject: string; + + if (params.type === 'teaser') { + content = ` +

Votre formation approche !

+

Bonjour ${salutation},

+ +

Nous sommes ravis de vous accueillir prochainement pour la formation ${formation.nom}.

+ +
+

Séquence : ${sequence.nom}

+

Dates :

+ ${datesHTML} + ${sequence.lieu ? `

Lieu : ${sequence.lieu}

` : ''} + ${formateurNom ? `

Formateur : ${formateurNom}

` : ''} +
+ +

Préparez-vous à vivre une expérience enrichissante qui vous permettra de développer vos compétences managériales.

+ +

À très bientôt !

+ `; + subject = `Votre formation ${formation.nom} approche !`; + } else if (params.type === 'rappel') { + // Rappel J-7 + content = ` +

Rappel : Votre formation commence bientôt !

+

Bonjour ${salutation},

+ +

Nous vous rappelons que votre formation ${formation.nom} commence dans une semaine.

+ +
+

Séquence : ${sequence.nom}

+

Dates :

+ ${datesHTML} + ${sequence.lieu ? `

Lieu : ${sequence.lieu}

` : ''} + ${formateurNom ? `

Formateur : ${formateurNom}

` : ''} +
+ +

N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.

+ +

À très bientôt !

+ `; + subject = `Rappel J-7 : Formation ${formation.nom}`; + } else { + // Rappel J-1 + content = ` +

Rappel : Votre formation commence demain !

+

Bonjour ${salutation},

+ +

Nous vous rappelons que votre formation ${formation.nom} commence demain.

+ +
+

Séquence : ${sequence.nom}

+

Dates :

+ ${datesHTML} + ${sequence.lieu ? `

Lieu : ${sequence.lieu}

` : ''} + ${formateurNom ? `

Formateur : ${formateurNom}

` : ''} +
+ +

Merci de vous présenter à l'heure indiquée.

+

N'oubliez pas d'apporter le matériel nécessaire.

+ +

À demain !

+ `; + subject = `Rappel J-1 : Formation ${formation.nom} - C'est demain !`; + } + + // Préparer les variables + const variables = { + nomApprenant: apprenant.nom, + prenomApprenant: apprenant.prenom, + nomFormation: formation.nom, + nomSequence: sequence.nom, + dateDebut: new Date(dates[0].dateDebut).toLocaleDateString('fr-FR'), + dateFin: new Date(dates[dates.length - 1].dateFin).toLocaleDateString('fr-FR'), + datesHTML: datesHTML, // Ajouter le HTML des dates + lieu: sequence.lieu || '', + formateur: formateurNom || '', + }; + + // Générer le HTML final avec le template + const html = await generateEmailFromTemplate(params.type === 'teaser' ? 'teaser' : 'rappel', content, variables); + + return { + html, + subject, + recipient: { + email: apprenant.email, + nom: apprenant.nom, + prenom: apprenant.prenom, + }, + }; +} diff --git a/deployment-package/source_server/emailService.ts b/deployment-package/source_server/emailService.ts new file mode 100644 index 0000000..68f2790 --- /dev/null +++ b/deployment-package/source_server/emailService.ts @@ -0,0 +1,1023 @@ +/** + * Service d'envoi d'emails pour les formations + * Utilise le système de notification Manus pour envoyer des emails + */ + +import { generateFormationICS } from "./icsGenerator"; +import { sendEmail as sendEmailViaService, isEmailServiceConfigured } from "./_core/emailSender"; +import { generateEmailFromTemplate } from "./emailTemplateGenerator"; +import { EmailVariables } from "./emailTemplateUtils"; + +interface EmailParams { + to: string; + subject: string; + html: string; + attachments?: Array<{ + filename: string; + content: string; + contentType: string; + }>; +} + +/** + * Envoie un email via le service d'envoi configuré + * Utilise Resend si RESEND_API_KEY est configuré, sinon simule l'envoi + */ +async function sendEmail(params: EmailParams): Promise { + return await sendEmailViaService(params); +} + +/** + * Template HTML de base pour les emails (avec template personnalisé) + */ +async function getEmailTemplate( + content: string, + templateType: string = 'inscription', + variables?: EmailVariables +): Promise { + return await generateEmailFromTemplate(templateType, content, variables); +} + +/** + * Envoie un email de confirmation d'inscription avec invitations Outlook pour toutes les dates + */ +export async function sendInscriptionConfirmation(params: { + apprenantEmail: string; + apprenantNom: string; + apprenantPrenom: string; + apprenantFonction: string; + formationNom: string; + sequenceNom: string; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; + lieu: string; + statut: 'confirmee' | 'liste_attente'; +}): Promise { + const isConfirmed = params.statut === 'confirmee'; + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre'; + + // Générer la liste des dates + const datesHTML = params.dates.map(date => ` +
+ Date ${date.ordre} :
+ Du ${date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })}
+ au ${date.dateFin.toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +
+ `).join(''); + + const content = ` +

Confirmation d'inscription

+

Bonjour ${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom},

+ + ${isConfirmed + ? '

Votre inscription a été confirmée !

' + : '

Vous avez été ajouté à la liste d\'attente.

' + } + +
+

Détails de la formation

+

Formation : ${params.formationNom}

+

Séquence : ${params.sequenceNom}

+

Lieu : ${params.lieu}

+

Dates de formation (${params.dates.length} séance${params.dates.length > 1 ? 's' : ''}) :

+ ${datesHTML} +
+ + ${isConfirmed + ? `

Des invitations Outlook sont jointes à cet email pour chaque date de formation. Merci de les ajouter à votre calendrier pour bloquer votre agenda.

+

Important : Les inscriptions seront bloquées 15 jours avant le début de la séquence. Après cette date, aucune modification ne sera possible.

` + : '

Nous vous contacterons dès qu\'une place se libère.

' + } + +

À bientôt pour cette formation !

+ `; + + // Générer une invitation ICS pour chaque date + const attachments = isConfirmed ? params.dates.map((date, index) => ({ + filename: `invitation_date_${date.ordre}.ics`, + content: generateFormationICS({ + formationNom: params.formationNom, + sessionNom: `${params.sequenceNom} - Date ${date.ordre}`, + dateDebut: date.dateDebut, + dateFin: date.dateFin, + lieu: params.lieu, + apprenantNom: params.apprenantNom, + apprenantPrenom: params.apprenantPrenom, + apprenantEmail: params.apprenantEmail, + }), + contentType: 'text/calendar', + })) : undefined; + + // Préparer les variables pour le remplacement (avec alias pour compatibilité) + const variables: EmailVariables = { + nomApprenant: params.apprenantNom, + prenomApprenant: params.apprenantPrenom, + nomFormation: params.formationNom, + nomSequence: params.sequenceNom, + dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '', + dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '', + lieu: params.lieu, + datesHTML: datesHTML, // HTML formaté de toutes les dates + // Alias pour compatibilité avec les templates utilisant les noms courts + nom: params.apprenantNom, + prenom: params.apprenantPrenom, + formation: params.formationNom, + sequence: params.sequenceNom, + }; + + return sendEmail({ + to: params.apprenantEmail, + subject: isConfirmed + ? `Confirmation d'inscription - ${params.formationNom}` + : `Liste d'attente - ${params.formationNom}`, + html: await getEmailTemplate(content, 'inscription', variables), + attachments, + }); +} + +/** + * Envoie un email teaser pour une séquence + */ +export async function sendTeaserEmail(params: { + apprenantEmail: string; + apprenantPrenom: string; + apprenantNom: string; + apprenantFonction: string; + formationNom: string; + sequenceNom: string; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; + lieu?: string; + formateur?: string; +}): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom; + + const datesHTML = params.dates.map(date => ` +
+ Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + })} +
+ `).join(''); + + const content = ` +

Votre formation approche !

+

Bonjour ${salutation},

+ +

Nous sommes ravis de vous accueillir prochainement pour la formation ${params.formationNom}.

+ +
+

Séquence : ${params.sequenceNom}

+

Dates :

+ ${datesHTML} + ${params.lieu ? `

Lieu : ${params.lieu}

` : ''} + ${params.formateur ? `

Formateur : ${params.formateur}

` : ''} +
+ +

Préparez-vous à vivre une expérience enrichissante qui vous permettra de développer vos compétences managériales.

+ +

À très bientôt !

+ `; + + const variables = { + nomApprenant: params.apprenantNom, + prenomApprenant: params.apprenantPrenom, + nomFormation: params.formationNom, + nomSequence: params.sequenceNom, + dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '', + dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '', + datesHTML: datesHTML, // Ajouter le HTML des dates + lieu: params.lieu || '', + formateur: params.formateur || '', + }; + + return sendEmail({ + to: params.apprenantEmail, + subject: `Votre formation ${params.formationNom} approche !`, + html: await getEmailTemplate(content, 'teaser', variables), + }); +} + +/** + * Envoie un email de rappel J-7 + */ +export async function sendRappelJ7Email(params: { + apprenantEmail: string; + apprenantPrenom: string; + apprenantNom: string; + apprenantFonction: string; + formationNom: string; + sequenceNom: string; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; + lieu: string; + formateur?: string; + attachmentUrl?: string; + attachmentFilename?: string; + attachmentMimeType?: string; +}): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom; + + const datesHTML = params.dates.map(date => ` +
+ Date ${date.ordre} :
+ ${date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +
+ `).join(''); + + const content = ` +

Rappel : Votre formation dans 7 jours

+

Bonjour ${salutation},

+ +

Nous vous rappelons que votre formation ${params.formationNom} commence dans 7 jours.

+ +
+

Informations pratiques

+

Séquence : ${params.sequenceNom}

+

Lieu : ${params.lieu}

+ ${params.formateur ? `

Formateur : ${params.formateur}

` : ''} +

Dates :

+ ${datesHTML} +
+ +

Merci de vous présenter à l'heure indiquée.

+ +

Si vous avez des questions, n'hésitez pas à contacter le service RH.

+ +

À très bientôt !

+ `; + + const variables = { + nomApprenant: params.apprenantNom, + prenomApprenant: params.apprenantPrenom, + nomFormation: params.formationNom, + nomSequence: params.sequenceNom, + dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '', + dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '', + datesHTML: datesHTML, // Ajouter le HTML des dates + lieu: params.lieu, + formateur: params.formateur || '', + }; + + // Préparer les pièces jointes si nécessaire + const attachments: Array<{ filename: string; content: string; contentType: string }> = []; + + if (params.attachmentUrl && params.attachmentFilename && params.attachmentMimeType) { + try { + // Télécharger le fichier depuis S3 et le convertir en base64 + const response = await fetch(params.attachmentUrl); + const buffer = await response.arrayBuffer(); + const base64 = Buffer.from(buffer).toString('base64'); + + attachments.push({ + filename: params.attachmentFilename, + content: base64, + contentType: params.attachmentMimeType, + }); + } catch (error) { + console.error('[Email] Erreur lors du téléchargement de la pièce jointe:', error); + // Continuer l'envoi sans la pièce jointe + } + } + + return sendEmail({ + to: params.apprenantEmail, + subject: `Rappel J-7 : ${params.formationNom}`, + html: await getEmailTemplate(content, 'rappel', variables), + attachments: attachments.length > 0 ? attachments : undefined, + }); +} + +/** + * Envoie un email de rappel J-1 à un apprenant + */ +export async function sendRappelJ1Email(params: { + apprenantEmail: string; + apprenantNom: string; + apprenantPrenom: string; + apprenantFonction: string; + formationNom: string; + sequenceNom: string; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; + lieu?: string; + formateur?: string; + attachmentUrl?: string; + attachmentFilename?: string; + attachmentMimeType?: string; +}): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom; + + const datesHTML = params.dates.map(date => ` +
+ Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} - ${date.dateFin.toLocaleDateString('fr-FR', { + hour: '2-digit', + minute: '2-digit' + })} +
+ `).join(''); + + const content = ` +

Rappel : Votre formation commence demain !

+

Bonjour ${salutation},

+ +

Nous vous rappelons que votre formation ${params.formationNom} commence demain.

+ +
+

Informations pratiques

+

Séquence : ${params.sequenceNom}

+ ${params.lieu ? `

Lieu : ${params.lieu}

` : ''} + ${params.formateur ? `

Formateur : ${params.formateur}

` : ''} +

Dates :

+ ${datesHTML} +
+ +

Merci de vous présenter à l'heure indiquée.

+

N'oubliez pas d'apporter le matériel nécessaire.

+ +

Si vous avez des questions de dernière minute, n'hésitez pas à contacter le service RH.

+ +

À demain !

+ `; + + const variables = { + nomApprenant: params.apprenantNom, + prenomApprenant: params.apprenantPrenom, + nomFormation: params.formationNom, + nomSequence: params.sequenceNom, + dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '', + dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '', + datesHTML: datesHTML, // Ajouter le HTML des dates + lieu: params.lieu || '', + formateur: params.formateur || '', + }; + + // Préparer les pièces jointes si nécessaire + const attachments: Array<{ filename: string; content: string; contentType: string }> = []; + + if (params.attachmentUrl && params.attachmentFilename && params.attachmentMimeType) { + try { + // Télécharger le fichier depuis S3 et le convertir en base64 + const response = await fetch(params.attachmentUrl); + const buffer = await response.arrayBuffer(); + const base64 = Buffer.from(buffer).toString('base64'); + + attachments.push({ + filename: params.attachmentFilename, + content: base64, + contentType: params.attachmentMimeType, + }); + } catch (error) { + console.error('[Email] Erreur lors du téléchargement de la pièce jointe:', error); + // Continuer l'envoi sans la pièce jointe + } + } + + return sendEmail({ + to: params.apprenantEmail, + subject: `Rappel J-1 : ${params.formationNom} - C'est demain !`, + html: await getEmailTemplate(content, 'rappel', variables), + attachments: attachments.length > 0 ? attachments : undefined, + }); +} + +/** + * Envoie des emails groupés à tous les inscrits d'une séquence + */ +export async function sendGroupEmail(params: { + recipients: Array<{ email: string; prenom: string; nom: string; fonction: string }>; + formationNom: string; + sequenceNom: string; + type: 'teaser' | 'rappel' | 'rappel_j1'; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; + lieu?: string; + formateur?: string; +}): Promise<{ sent: number; failed: number }> { + let sent = 0; + let failed = 0; + + for (const recipient of params.recipients) { + try { + if (params.type === 'teaser') { + await sendTeaserEmail({ + apprenantEmail: recipient.email, + apprenantPrenom: recipient.prenom, + apprenantNom: recipient.nom, + apprenantFonction: recipient.fonction, + formationNom: params.formationNom, + sequenceNom: params.sequenceNom, + dates: params.dates, + lieu: params.lieu, + formateur: params.formateur, + }); + } else if (params.type === 'rappel') { + await sendRappelJ7Email({ + apprenantEmail: recipient.email, + apprenantPrenom: recipient.prenom, + apprenantNom: recipient.nom, + apprenantFonction: recipient.fonction, + formationNom: params.formationNom, + sequenceNom: params.sequenceNom, + dates: params.dates, + lieu: params.lieu || '', + formateur: params.formateur, + }); + } else { + // rappel_j1 + await sendRappelJ1Email({ + apprenantEmail: recipient.email, + apprenantPrenom: recipient.prenom, + apprenantNom: recipient.nom, + apprenantFonction: recipient.fonction, + formationNom: params.formationNom, + sequenceNom: params.sequenceNom, + dates: params.dates, + lieu: params.lieu, + formateur: params.formateur, + }); + } + sent++; + } catch (error) { + console.error(`Erreur envoi email à ${recipient.email}:`, error); + failed++; + } + } + + return { sent, failed }; +} + +/** + * Envoie un email de réinitialisation de mot de passe + */ +export async function sendPasswordResetEmail(params: { + apprenantEmail: string; + apprenantNom: string; + apprenantPrenom: string; + resetLink: string; +}): Promise { + const content = ` +

Réinitialisation de mot de passe

+

Bonjour ${params.apprenantPrenom} ${params.apprenantNom},

+ +

Vous avez demandé la réinitialisation de votre mot de passe pour votre compte Manager Itinova.

+ +

Cliquez sur le bouton ci-dessous pour créer un nouveau mot de passe :

+ + + +

Ou copiez ce lien dans votre navigateur :

+
+ ${params.resetLink} +
+ +
+

⚠️ Important :

+
    +
  • Ce lien est valide pendant 24 heures seulement
  • +
  • Il ne peut être utilisé qu'une seule fois
  • +
  • Si vous n'avez pas demandé cette réinitialisation, ignorez cet email
  • +
+
+ +

Pour toute question, contactez le service RH.

+ `; + + return sendEmail({ + to: params.apprenantEmail, + subject: 'Réinitialisation de votre mot de passe - Manager Itinova', + html: await getEmailTemplate(content, 'reset_password'), + }); +} + + +/** + * ======================================== + * SYSTÈME DE NOTIFICATIONS ENRICHI + * ======================================== + */ + +/** + * Envoie un email de remerciement post-formation à un apprenant + */ +export async function sendRemerciementPostFormation(params: { + apprenantEmail: string; + apprenantNom: string; + apprenantPrenom: string; + apprenantFonction: string; + formationNom: string; + sequenceNom: string; + formateurNom?: string; + lienQuestionnaire?: string; +}): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom; + + const content = ` +

Merci pour votre participation !

+

Bonjour ${salutation},

+ +

Nous tenons à vous remercier pour votre participation à la formation ${params.formationNom} (${params.sequenceNom}).

+ + ${params.formateurNom ? `

Nous espérons que les sessions animées par ${params.formateurNom} ont répondu à vos attentes.

` : ''} + +
+

Votre avis compte !

+

Afin d'améliorer continuellement nos formations, nous vous invitons à partager votre retour d'expérience.

+ ${params.lienQuestionnaire ? ` + + ` : '

Un questionnaire de satisfaction vous sera envoyé prochainement.

'} +
+ +

Nous vous souhaitons une excellente continuation dans vos fonctions et espérons vous revoir lors de prochaines formations.

+ +

Cordialement,
L'équipe Formation

+ `; + + const variables = { + nomApprenant: params.apprenantNom, + prenomApprenant: params.apprenantPrenom, + nomFormation: params.formationNom, + nomSequence: params.sequenceNom, + formateur: params.formateurNom || '', + }; + + return sendEmail({ + to: params.apprenantEmail, + subject: `Merci pour votre participation - ${params.formationNom}`, + html: await getEmailTemplate(content, 'remerciement', variables), + }); +} + +/** + * Envoie une notification au formateur pour une nouvelle inscription + */ +export async function sendNotificationFormateurNouvelleInscription(params: { + formateurEmail: string; + formateurNom: string; + apprenantNom: string; + apprenantPrenom: string; + apprenantFonction: string; + apprenantEtablissement?: string; + formationNom: string; + sequenceNom: string; + nbInscrits: number; + capaciteMax: number; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; +}): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre'; + + const datesHTML = params.dates.map(date => ` +
  • Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric' + })}
  • + `).join(''); + + const content = ` +

    🎓 Nouvelle inscription à votre formation

    +

    Bonjour ${params.formateurNom},

    + +

    Un nouvel apprenant vient de s'inscrire à votre formation.

    + +
    +

    Détails de l'inscription

    +

    Apprenant : ${params.apprenantPrenom} ${params.apprenantNom}

    +

    Fonction : ${fonctionLabel}

    + ${params.apprenantEtablissement ? `

    Établissement : ${params.apprenantEtablissement}

    ` : ''} +

    Formation : ${params.formationNom}

    +

    Séquence : ${params.sequenceNom}

    +
    + +
    +

    📊 État des inscriptions

    +

    ${params.nbInscrits} / ${params.capaciteMax} places occupées

    +
    +
    +
    +
    + +

    Dates de la formation :

    +
      ${datesHTML}
    + +

    Cordialement,
    L'équipe Formation

    + `; + + return sendEmail({ + to: params.formateurEmail, + subject: `Nouvelle inscription - ${params.formationNom} (${params.sequenceNom})`, + html: await getEmailTemplate(content, 'notification_formateur'), + }); +} + +/** + * Envoie une notification au formateur pour une annulation d'inscription + */ +export async function sendNotificationFormateurAnnulation(params: { + formateurEmail: string; + formateurNom: string; + apprenantNom: string; + apprenantPrenom: string; + apprenantFonction: string; + formationNom: string; + sequenceNom: string; + nbInscrits: number; + capaciteMax: number; + raisonAnnulation?: string; +}): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre'; + + const content = ` +

    ⚠️ Annulation d'inscription

    +

    Bonjour ${params.formateurNom},

    + +

    Un apprenant a annulé son inscription à votre formation.

    + +
    +

    Détails de l'annulation

    +

    Apprenant : ${params.apprenantPrenom} ${params.apprenantNom}

    +

    Fonction : ${fonctionLabel}

    +

    Formation : ${params.formationNom}

    +

    Séquence : ${params.sequenceNom}

    + ${params.raisonAnnulation ? `

    Raison : ${params.raisonAnnulation}

    ` : ''} +
    + +
    +

    📊 État des inscriptions après annulation

    +

    ${params.nbInscrits} / ${params.capaciteMax} places occupées

    +

    ${params.capaciteMax - params.nbInscrits} place(s) disponible(s)

    +
    + +

    Cordialement,
    L'équipe Formation

    + `; + + return sendEmail({ + to: params.formateurEmail, + subject: `Annulation d'inscription - ${params.formationNom} (${params.sequenceNom})`, + html: await getEmailTemplate(content, 'notification_formateur'), + }); +} + +/** + * Envoie une alerte aux admins quand la capacité maximale est atteinte + */ +export async function sendAlerteCapaciteAtteinte(params: { + adminEmails: string[]; + formationNom: string; + sequenceNom: string; + capaciteMax: number; + nbInscrits: number; + nbListeAttente: number; + formateurNom?: string; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; +}): Promise<{ sent: number; failed: number }> { + const datesHTML = params.dates.map(date => ` +
  • ${date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric' + })}
  • + `).join(''); + + const content = ` +

    🔔 Alerte : Capacité maximale atteinte

    + +
    +

    ⚠️ La séquence est complète

    +

    La capacité maximale de la séquence ${params.sequenceNom} a été atteinte.

    +
    + +
    +

    Détails de la séquence

    +

    Formation : ${params.formationNom}

    +

    Séquence : ${params.sequenceNom}

    + ${params.formateurNom ? `

    Formateur : ${params.formateurNom}

    ` : ''} +

    Capacité : ${params.nbInscrits} / ${params.capaciteMax} (100%)

    + ${params.nbListeAttente > 0 ? `

    Liste d'attente : ${params.nbListeAttente} personne(s)

    ` : ''} +
    + +

    Dates prévues :

    +
      ${datesHTML}
    + +
    +

    Actions possibles

    +
      +
    • Augmenter la capacité maximale de la séquence
    • +
    • Créer une nouvelle séquence pour cette formation
    • +
    • Contacter les personnes en liste d'attente pour les informer
    • +
    +
    + +

    Cordialement,
    Le système de gestion des formations

    + `; + + let sent = 0; + let failed = 0; + + for (const adminEmail of params.adminEmails) { + try { + await sendEmail({ + to: adminEmail, + subject: `🔔 Capacité atteinte - ${params.formationNom} (${params.sequenceNom})`, + html: await getEmailTemplate(content, 'alerte_admin'), + }); + sent++; + } catch (error) { + console.error(`Erreur envoi alerte à ${adminEmail}:`, error); + failed++; + } + } + + return { sent, failed }; +} + +/** + * Envoie une notification aux apprenants en liste d'attente quand une place se libère + */ +export async function sendNotificationPlaceDisponible(params: { + apprenantEmail: string; + apprenantNom: string; + apprenantPrenom: string; + apprenantFonction: string; + formationNom: string; + sequenceNom: string; + positionListeAttente: number; + lienConfirmation?: string; + delaiReponse?: number; // en heures +}): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom; + const delai = params.delaiReponse || 48; + + const content = ` +

    🎉 Une place s'est libérée !

    +

    Bonjour ${salutation},

    + +

    Bonne nouvelle ! Une place vient de se libérer pour la formation à laquelle vous êtes inscrit(e) en liste d'attente.

    + +
    +

    ✅ Vous pouvez maintenant confirmer votre inscription

    +

    Formation : ${params.formationNom}

    +

    Séquence : ${params.sequenceNom}

    +

    Votre position : ${params.positionListeAttente}${params.positionListeAttente === 1 ? 'er' : 'ème'} sur la liste d'attente

    +
    + + ${params.lienConfirmation ? ` + + ` : '

    Veuillez contacter le service RH pour confirmer votre inscription.

    '} + +
    +

    ⏰ Important

    +

    Vous avez ${delai} heures pour confirmer votre inscription.

    +

    Passé ce délai, la place sera proposée à la personne suivante sur la liste d'attente.

    +
    + +

    Cordialement,
    L'équipe Formation

    + `; + + const variables = { + nomApprenant: params.apprenantNom, + prenomApprenant: params.apprenantPrenom, + nomFormation: params.formationNom, + nomSequence: params.sequenceNom, + }; + + return sendEmail({ + to: params.apprenantEmail, + subject: `🎉 Place disponible - ${params.formationNom}`, + html: await getEmailTemplate(content, 'notification_liste_attente', variables), + }); +} + +/** + * Envoie une notification à un apprenant en liste d'attente pour l'informer d'une annulation de séquence + */ +export async function sendNotificationAnnulationListeAttente(params: { + apprenantEmail: string; + apprenantNom: string; + apprenantPrenom: string; + apprenantFonction: string; + formationNom: string; + sequenceNom: string; + raisonAnnulation?: string; + alternativesDisponibles?: Array<{ sequenceNom: string; nbPlaces: number }>; +}): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom; + + const alternativesHTML = params.alternativesDisponibles && params.alternativesDisponibles.length > 0 + ? ` +
    +

    📋 Séquences alternatives disponibles

    +
      + ${params.alternativesDisponibles.map(alt => ` +
    • ${alt.sequenceNom} - ${alt.nbPlaces} place(s) disponible(s)
    • + `).join('')} +
    +

    Contactez le service RH pour vous inscrire à une autre séquence.

    +
    + ` : ''; + + const content = ` +

    Information : Annulation de séquence

    +

    Bonjour ${salutation},

    + +

    Nous vous informons que la séquence de formation pour laquelle vous étiez en liste d'attente a été annulée.

    + +
    +

    Séquence annulée

    +

    Formation : ${params.formationNom}

    +

    Séquence : ${params.sequenceNom}

    + ${params.raisonAnnulation ? `

    Raison : ${params.raisonAnnulation}

    ` : ''} +
    + + ${alternativesHTML} + +

    Nous nous excusons pour ce désagrément et restons à votre disposition pour toute question.

    + +

    Cordialement,
    L'équipe Formation

    + `; + + return sendEmail({ + to: params.apprenantEmail, + subject: `Information : Annulation de séquence - ${params.formationNom}`, + html: await getEmailTemplate(content, 'notification_annulation'), + }); +} + +/** + * Envoie une notification au formateur quand la capacité maximale est atteinte + */ +export async function sendNotificationFormateurCapaciteAtteinte(params: { + formateurEmail: string; + formateurNom: string; + formationNom: string; + sequenceNom: string; + capaciteMax: number; + nbInscrits: number; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; +}): Promise { + const datesHTML = params.dates.map(date => ` +
  • Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric' + })}
  • + `).join(''); + + const content = ` +

    ⚠️ Capacité maximale atteinte

    +

    Bonjour ${params.formateurNom},

    + +

    La capacité maximale de votre formation a été atteinte.

    + +
    +

    Détails de la séquence

    +

    Formation : ${params.formationNom}

    +

    Séquence : ${params.sequenceNom}

    +

    Capacité : ${params.nbInscrits} / ${params.capaciteMax}

    +
    + +

    Dates de la formation :

    +
      ${datesHTML}
    + +

    Les nouvelles inscriptions seront automatiquement placées en liste d'attente.

    + +

    Cordialement,
    L'équipe Formation

    + `; + + return sendEmail({ + to: params.formateurEmail, + subject: `⚠️ Capacité maximale atteinte - ${params.formationNom} (${params.sequenceNom})`, + html: await getEmailTemplate(content, 'notification_formateur'), + }); +} + +/** + * Envoie une notification au formateur quand une place se libère + */ +export async function sendNotificationFormateurPlaceDisponible(params: { + formateurEmail: string; + formateurNom: string; + formationNom: string; + sequenceNom: string; + capaciteMax: number; + nbInscrits: number; + placesDisponibles: number; +}): Promise { + const content = ` +

    ✅ Place disponible

    +

    Bonjour ${params.formateurNom},

    + +

    Une place s'est libérée dans votre formation suite à une annulation.

    + +
    +

    Détails de la séquence

    +

    Formation : ${params.formationNom}

    +

    Séquence : ${params.sequenceNom}

    +

    Places occupées : ${params.nbInscrits} / ${params.capaciteMax}

    +

    Places disponibles : ${params.placesDisponibles}

    +
    + +

    De nouvelles inscriptions peuvent maintenant être acceptées.

    + +

    Cordialement,
    L'équipe Formation

    + `; + + return sendEmail({ + to: params.formateurEmail, + subject: `✅ Place disponible - ${params.formationNom} (${params.sequenceNom})`, + html: await getEmailTemplate(content, 'notification_formateur'), + }); +} + +/** + * Envoie une attestation de formation par email + */ +export async function sendAttestationEmail(params: { + to: string; + apprenantNom: string; + apprenantPrenom: string; + formationNom: string; + pdfUrl: string; +}): Promise { + const content = ` +

    Votre attestation de formation

    + +

    Bonjour ${params.apprenantPrenom} ${params.apprenantNom},

    + +

    Nous avons le plaisir de vous transmettre votre attestation de formation pour :

    + +
    +

    ${params.formationNom}

    +
    + +

    Vous pouvez télécharger votre attestation en cliquant sur le lien ci-dessous :

    + + + +

    Cette attestation certifie votre participation à la formation et peut être utilisée pour votre dossier professionnel.

    + +

    Nous vous remercions pour votre participation et vous souhaitons une excellente continuation.

    + +

    Cordialement,
    L'équipe Formation

    + `; + + return sendEmail({ + to: params.to, + subject: `Votre attestation de formation - ${params.formationNom}`, + html: await getEmailTemplate(content, 'attestation'), + }); +} diff --git a/deployment-package/source_server/emailTemplateGenerator.ts b/deployment-package/source_server/emailTemplateGenerator.ts new file mode 100644 index 0000000..9143f92 --- /dev/null +++ b/deployment-package/source_server/emailTemplateGenerator.ts @@ -0,0 +1,180 @@ +/** + * Générateur de templates d'emails personnalisés + * Utilise les templates stockés en base de données + */ + +import * as db from "./db"; +import { EmailTemplate } from "../drizzle/schema"; +import { replaceEmailVariables, EmailVariables } from "./emailTemplateUtils"; + +/** + * Génère le HTML d'un email en utilisant le template personnalisé + * @param templateType - Type de template (à utiliser) + * @param content - Contenu de l'email (legacy, sera remplacé par bodyContent) + * @param variables - Variables à remplacer dans le template + */ +export async function generateEmailFromTemplate( + templateType: string, + content: string, + variables?: EmailVariables +): Promise { + // Récupérer le template depuis la base de données + const template = await db.getEmailTemplateByType(templateType); + + // Si pas de template trouvé, utiliser le template par défaut + if (!template) { + const defaultContent = variables ? replaceEmailVariables(content, variables) : content; + return getDefaultEmailTemplate(defaultContent); + } + + return buildEmailHTML(template, content, variables); +} + +/** + * Construit le HTML de l'email avec le template + */ +function buildEmailHTML(template: EmailTemplate, content: string, variables?: EmailVariables): string { + // Utiliser bodyContent si disponible, sinon utiliser content (legacy) + let emailBody = template.bodyContent || content; + + // Remplacer les variables si fournies + if (variables) { + emailBody = replaceEmailVariables(emailBody, variables); + // Remplacer aussi dans le titre et le pied de page + template = { + ...template, + headerTitle: replaceEmailVariables(template.headerTitle, variables), + footerText: template.footerText ? replaceEmailVariables(template.footerText, variables) : template.footerText, + }; + } + return ` + + + + + + + +
    +
    + ${template.logoUrl ? `Logo` : ''} +

    ${template.headerTitle}

    +
    +
    + ${emailBody} +
    + +
    + + + `.trim(); +} + +/** + * Template par défaut si aucun template personnalisé n'est trouvé + */ +function getDefaultEmailTemplate(content: string): string { + return ` + + + + + + + +
    +
    +

    Formation Manager Itinova

    +
    +
    + ${content} +
    + +
    + + + `.trim(); +} diff --git a/deployment-package/source_server/emailTemplateUtils.ts b/deployment-package/source_server/emailTemplateUtils.ts new file mode 100644 index 0000000..ba6d593 --- /dev/null +++ b/deployment-package/source_server/emailTemplateUtils.ts @@ -0,0 +1,66 @@ +/** + * Utilitaires pour le remplacement des variables dans les templates d'emails + */ + +export interface EmailVariables { + nomApprenant?: string; + prenomApprenant?: string; + nomFormation?: string; + nomSequence?: string; + dateDebut?: string; + dateFin?: string; + datesHTML?: string; // HTML formaté de toutes les dates de la séquence + lieu?: string; + formateur?: string; + lienInscription?: string; + [key: string]: string | undefined; +} + +/** + * Remplace les variables {{variable}} dans un texte par leurs valeurs + * @param template - Le texte contenant les variables à remplacer + * @param variables - Un objet contenant les valeurs des variables + * @returns Le texte avec les variables remplacées + */ +export function replaceEmailVariables( + template: string, + variables: EmailVariables +): string { + let result = template; + + // Remplacer chaque variable trouvée dans le template + Object.keys(variables).forEach((key) => { + const value = variables[key]; + if (value !== undefined && value !== null) { + // Remplacer toutes les occurrences de {{key}} par la valeur + const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g'); + result = result.replace(regex, value); + } + }); + + // Nettoyer les variables non remplacées (optionnel) + // result = result.replace(/\{\{[^}]+\}\}/g, ''); + + return result; +} + +/** + * Génère un aperçu d'email avec des données d'exemple + * @param template - Le template d'email + * @returns Le template avec des données d'exemple + */ +export function generateEmailPreview(template: string): string { + const exampleVariables: EmailVariables = { + nomApprenant: 'Dupont', + prenomApprenant: 'Marie', + nomFormation: 'Formation Management', + nomSequence: 'Séquence 1 - Introduction', + dateDebut: '15/01/2025', + dateFin: '17/01/2025', + lieu: 'Salle de formation A - Bâtiment principal', + formateur: 'Jean Martin', + lienInscription: 'https://exemple.com/inscription/abc123', + }; + + return replaceEmailVariables(template, exampleVariables); +} diff --git a/deployment-package/source_server/etablissementsDb.ts b/deployment-package/source_server/etablissementsDb.ts new file mode 100644 index 0000000..4ac3a44 --- /dev/null +++ b/deployment-package/source_server/etablissementsDb.ts @@ -0,0 +1,110 @@ +import { eq, sql } from "drizzle-orm"; +import { getDb } from "./db"; +import { apprenants, inscriptions } from "../drizzle/schema"; + +export async function getEtablissementsStats() { + const db = await getDb(); + if (!db) return []; + + // Récupérer tous les apprenants groupés par établissement + const apprenantsData = await db.select().from(apprenants); + + // Grouper par code établissement + const etablissementsMap = new Map(); + + for (const apprenant of apprenantsData) { + const code = apprenant.codeEtablissement; + + if (!etablissementsMap.has(code)) { + etablissementsMap.set(code, { + codeEtablissement: code, + nombreApprenants: 0, + apprenantsActifs: 0, + apprenantIds: [], + }); + } + + const etab = etablissementsMap.get(code)!; + etab.nombreApprenants++; + etab.apprenantIds.push(apprenant.id); + } + + // Pour chaque établissement, compter les apprenants actifs + const etablissementsStats = await Promise.all( + Array.from(etablissementsMap.values()).map(async (etab) => { + // Compter le nombre d'apprenants ayant au moins une inscription + const apprenantsActifsCount = await Promise.all( + etab.apprenantIds.map(async (apprenantId) => { + const inscriptionsData = await db + .select() + .from(inscriptions) + .where(eq(inscriptions.apprenantId, apprenantId)) + .limit(1); + return inscriptionsData.length > 0 ? 1 : 0; + }) + ); + + const nombreActifs = apprenantsActifsCount.reduce((sum: number, count) => sum + count, 0); + const tauxParticipation = etab.nombreApprenants > 0 + ? Math.round((nombreActifs / etab.nombreApprenants) * 100) + : 0; + + return { + codeEtablissement: etab.codeEtablissement, + nombreApprenants: etab.nombreApprenants, + apprenantsActifs: nombreActifs, + tauxParticipation, + }; + }) + ); + + // Trier par nombre d'apprenants décroissant + return etablissementsStats.sort((a, b) => b.nombreApprenants - a.nombreApprenants); +} + +export async function getEtablissementDetail(codeEtablissement: string) { + const db = await getDb(); + if (!db) return null; + + // Récupérer tous les apprenants de cet établissement + const apprenantsData = await db + .select() + .from(apprenants) + .where(eq(apprenants.codeEtablissement, codeEtablissement)); + + // Pour chaque apprenant, récupérer ses inscriptions + const apprenantsWithInscriptions = await Promise.all( + apprenantsData.map(async (apprenant) => { + const inscriptionsData = await db + .select() + .from(inscriptions) + .where(eq(inscriptions.apprenantId, apprenant.id)); + + return { + ...apprenant, + inscriptions: inscriptionsData, + }; + }) + ); + + const nombreApprenants = apprenantsData.length; + const apprenantsActifs = apprenantsWithInscriptions.filter( + (a) => a.inscriptions.length > 0 + ).length; + const tauxParticipation = nombreApprenants > 0 + ? Math.round((apprenantsActifs / nombreApprenants) * 100) + : 0; + + return { + codeEtablissement, + nombreApprenants, + apprenantsActifs, + tauxParticipation, + apprenants: apprenantsWithInscriptions, + }; +} diff --git a/deployment-package/source_server/exportService.ts b/deployment-package/source_server/exportService.ts new file mode 100644 index 0000000..c85f5e1 --- /dev/null +++ b/deployment-package/source_server/exportService.ts @@ -0,0 +1,277 @@ +/** + * Service d'export des données en Excel et PDF + */ + +import * as XLSX from 'xlsx'; +import { jsPDF } from 'jspdf'; +import autoTable from 'jspdf-autotable'; +import * as fs from 'fs'; +import * as path from 'path'; + +interface InscriptionExport { + nom: string; + prenom: string; + email: string; + codeEtablissement: string; + fonction: string; + statut: string; + dateInscription: Date; +} + +interface SequenceInfo { + formationNom: string; + sequenceNom: string; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; + lieu: string; + publicCible: string; + inscriptions: InscriptionExport[]; +} + +interface ApprenantPresence { + nom: string; + prenom: string; + codeEtablissement: string; + fonction: string; +} + +interface FeuillePresenceInfo { + formationNom: string; + sequenceNom: string; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; + lieu: string; + publicCible: string; + formateur?: string; + apprenants: ApprenantPresence[]; +} + +/** + * Génère un fichier Excel avec la liste des inscrits + */ +export function generateExcelExport(sequenceInfo: SequenceInfo): Buffer { + // Préparer les données + const data = sequenceInfo.inscriptions.map(i => ({ + 'Nom': i.nom, + 'Prénom': i.prenom, + 'Email': i.email, + 'Code établissement': i.codeEtablissement, + 'Fonction': i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre', + 'Statut': i.statut === 'confirmee' ? 'Confirmée' : i.statut === 'liste_attente' ? 'Liste d\'attente' : 'Annulée', + 'Date d\'inscription': i.dateInscription.toLocaleDateString('fr-FR'), + })); + + // Créer le workbook + const ws = XLSX.utils.json_to_sheet(data); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, 'Inscrits'); + + // Ajouter une feuille d'informations + const infoData = [ + { 'Information': 'Formation', 'Valeur': sequenceInfo.formationNom }, + { 'Information': 'Séquence', 'Valeur': sequenceInfo.sequenceNom }, + { 'Information': 'Lieu', 'Valeur': sequenceInfo.lieu }, + { 'Information': 'Public cible', 'Valeur': sequenceInfo.publicCible === 'directeur' ? 'Directeurs' : sequenceInfo.publicCible === 'chef_service' ? 'Chefs de service' : sequenceInfo.publicCible === 'tous' ? 'Tous' : 'Autre' }, + { 'Information': 'Nombre d\'inscrits', 'Valeur': sequenceInfo.inscriptions.length.toString() }, + ]; + + // Ajouter les dates + sequenceInfo.dates.forEach(date => { + infoData.push({ + 'Information': `Date ${date.ordre}`, + 'Valeur': date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + }); + }); + + const wsInfo = XLSX.utils.json_to_sheet(infoData); + XLSX.utils.book_append_sheet(wb, wsInfo, 'Informations'); + + // Générer le buffer + return Buffer.from(XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })); +} + +/** + * Génère un PDF avec la liste des inscrits + */ +export function generatePDFExport(sequenceInfo: SequenceInfo): Buffer { + const doc = new jsPDF(); + + // Titre + doc.setFontSize(18); + doc.text('Liste des inscrits', 14, 20); + + // Informations de séquence + doc.setFontSize(10); + let yPos = 35; + doc.text(`Formation : ${sequenceInfo.formationNom}`, 14, yPos); + yPos += 6; + doc.text(`Séquence : ${sequenceInfo.sequenceNom}`, 14, yPos); + yPos += 6; + doc.text(`Lieu : ${sequenceInfo.lieu}`, 14, yPos); + yPos += 6; + doc.text(`Public cible : ${sequenceInfo.publicCible === 'directeur' ? 'Directeurs' : sequenceInfo.publicCible === 'chef_service' ? 'Chefs de service' : sequenceInfo.publicCible === 'tous' ? 'Tous' : 'Autre'}`, 14, yPos); + yPos += 8; + + // Dates de formation + doc.setFontSize(9); + sequenceInfo.dates.forEach(date => { + doc.text(`Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })}`, 14, yPos); + yPos += 5; + }); + + // Tableau des inscrits + const tableData = sequenceInfo.inscriptions.map(i => [ + i.nom, + i.prenom, + i.email, + i.codeEtablissement, + i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre', + i.statut === 'confirmee' ? 'Confirmée' : i.statut === 'liste_attente' ? 'Liste d\'attente' : 'Annulée', + ]); + + autoTable(doc, { + startY: yPos + 10, + head: [['Nom', 'Prénom', 'Email', 'Code étab.', 'Fonction', 'Statut']], + body: tableData, + styles: { fontSize: 9 }, + headStyles: { fillColor: [37, 99, 235] }, + }); + + return Buffer.from(doc.output('arraybuffer')); +} + +/** + * Génère une feuille de présence PDF avec signatures matin/après-midi pour chaque journée + */ +export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffer { + const doc = new jsPDF(); + + // Charger le logo + let logoData: string | null = null; + + try { + // Utiliser un chemin absolu depuis la racine du projet + const logoPath = path.resolve(process.cwd(), 'client/public/itinova-logo.png'); + const logoBuffer = fs.readFileSync(logoPath); + logoData = `data:image/png;base64,${logoBuffer.toString('base64')}`; + } catch (error) { + console.warn('Logo Itinova non trouvé, génération sans logo:', error); + } + + // Fonction pour générer une page de feuille de présence pour une journée + const generatePageForDate = (dateInfo: { dateDebut: Date; dateFin: Date; ordre: number }, isFirstPage: boolean) => { + if (!isFirstPage) { + doc.addPage(); + } + + // Ajouter le logo en haut à droite si disponible + if (logoData) { + doc.addImage(logoData, 'PNG', 160, 10, 40, 15); + } + + // Titre + doc.setFontSize(18); + doc.text('Feuille de présence', 14, 20); + + // Informations de séquence + doc.setFontSize(10); + let yPos = 35; + doc.text(`Formation : ${feuilleInfo.formationNom}`, 14, yPos); + yPos += 6; + doc.text(`Séquence : ${feuilleInfo.sequenceNom}`, 14, yPos); + yPos += 6; + doc.text(`Lieu : ${feuilleInfo.lieu}`, 14, yPos); + yPos += 6; + + // Ajouter le formateur si disponible + if (feuilleInfo.formateur) { + doc.text(`Formateur : ${feuilleInfo.formateur}`, 14, yPos); + yPos += 6; + } + + doc.text(`Public cible : ${feuilleInfo.publicCible === 'directeur' ? 'Directeurs' : feuilleInfo.publicCible === 'chef_service' ? 'Chefs de service' : feuilleInfo.publicCible === 'tous' ? 'Tous' : 'Autre'}`, 14, yPos); + yPos += 8; + + // Date de la journée + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text(`Date ${dateInfo.ordre} : ${dateInfo.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })}`, 14, yPos); + doc.setFont('helvetica', 'normal'); + yPos += 10; + + // Tableau de présence avec signatures matin et après-midi + const tableData = feuilleInfo.apprenants.map(i => [ + i.nom, + i.prenom, + i.codeEtablissement, + i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre', + '', // Signature matin + '', // Signature après-midi + ]); + + autoTable(doc, { + startY: yPos, + head: [['Nom', 'Prénom', 'Code étab.', 'Fonction', 'Signature\nMatin', 'Signature\nAprès-midi']], + body: tableData, + styles: { + fontSize: 9, + cellPadding: 3, + minCellHeight: 10, + }, + headStyles: { + fillColor: [37, 99, 235], + halign: 'center', + }, + columnStyles: { + 0: { cellWidth: 30 }, // Nom + 1: { cellWidth: 30 }, // Prénom + 2: { cellWidth: 25 }, // Code établissement + 3: { cellWidth: 30 }, // Fonction + 4: { cellWidth: 35 }, // Signature matin + 5: { cellWidth: 35 }, // Signature après-midi + }, + }); + + // Ajouter un champ de signature pour le formateur en bas de page + const finalY = (doc as any).lastAutoTable.finalY || yPos + 50; + const signatureY = finalY + 20; + + doc.setFontSize(10); + doc.setFont('helvetica', 'bold'); + doc.text('Signature du formateur :', 14, signatureY); + doc.setFont('helvetica', 'normal'); + + // Dessiner une ligne pour la signature + doc.line(60, signatureY, 120, signatureY); + + // Ajouter la date + doc.setFontSize(9); + doc.text('Date : _______________', 140, signatureY); + }; + + // Générer une page pour chaque date + feuilleInfo.dates.forEach((date, index) => { + generatePageForDate(date, index === 0); + }); + + return Buffer.from(doc.output('arraybuffer')); +} diff --git a/deployment-package/source_server/formateurDb.ts b/deployment-package/source_server/formateurDb.ts new file mode 100644 index 0000000..1e5ae2e --- /dev/null +++ b/deployment-package/source_server/formateurDb.ts @@ -0,0 +1,402 @@ +import { eq, sql, and, gte, lte, desc } from "drizzle-orm"; +import { getDb } from "./db"; +import { + sequences, + formations, + formateurs, + datesFormation, + inscriptions, + apprenants, + supportsFormation +} from "../drizzle/schema"; + +/** + * Récupère le calendrier des interventions d'un formateur + * @param formateurId - ID du formateur + * @param dateDebut - Date de début optionnelle + * @param dateFin - Date de fin optionnelle + */ +export async function getCalendrierFormateur(formateurId: number, dateDebut?: Date, dateFin?: Date) { + const db = await getDb(); + if (!db) return []; + + const conditions = [eq(sequences.formateurId, formateurId)]; + + if (dateDebut) { + conditions.push(gte(datesFormation.dateDebut, dateDebut)); + } + if (dateFin) { + conditions.push(lte(datesFormation.dateFin, dateFin)); + } + + const results = await db + .select({ + sequenceId: sequences.id, + sequenceNom: sequences.nom, + formationId: formations.id, + formationNom: formations.nom, + dateId: datesFormation.id, + dateDebut: datesFormation.dateDebut, + dateFin: datesFormation.dateFin, + ordre: datesFormation.ordre, + lieu: sequences.lieu, + nbInscrits: sql`( + SELECT COUNT(*) + FROM inscriptions + WHERE inscriptions.sequenceId = ${sequences.id} + AND inscriptions.statut IN ('confirmee', 'liste_attente') + )`, + }) + .from(sequences) + .innerJoin(formations, eq(sequences.formationId, formations.id)) + .innerJoin(datesFormation, eq(datesFormation.sequenceId, sequences.id)) + .where(conditions.length > 0 ? and(...conditions) : undefined) + .orderBy(datesFormation.dateDebut); + + return results; +} + +/** + * Récupère la liste des apprenants inscrits à une séquence + * @param sequenceId - ID de la séquence + */ +export async function getApprenantsSequence(sequenceId: number) { + const db = await getDb(); + if (!db) return []; + + const results = await db + .select({ + inscriptionId: inscriptions.id, + apprenantId: apprenants.id, + nom: apprenants.nom, + prenom: apprenants.prenom, + email: apprenants.email, + fonction: apprenants.fonction, + codeEtablissement: apprenants.codeEtablissement, + statut: inscriptions.statut, + dateInscription: inscriptions.dateInscription, + // presenceValidee n'existe pas dans le schéma actuel + }) + .from(inscriptions) + .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .where(eq(inscriptions.sequenceId, sequenceId)) + .orderBy(apprenants.nom, apprenants.prenom); + + return results; +} + +/** + * Récupère les supports de formation d'une séquence + * @param sequenceId - ID de la séquence + */ +export async function getSupportsSequence(sequenceId: number) { + const db = await getDb(); + if (!db) return []; + + const results = await db + .select({ + id: supportsFormation.id, + nomFichier: supportsFormation.nomFichier, + typeFichier: supportsFormation.typeFichier, + tailleFichier: supportsFormation.tailleFichier, + urlFichier: supportsFormation.urlFichier, + description: supportsFormation.description, + formateurNom: formateurs.nom, + createdAt: supportsFormation.createdAt, + }) + .from(supportsFormation) + .innerJoin(formateurs, eq(supportsFormation.formateurId, formateurs.id)) + .where(eq(supportsFormation.sequenceId, sequenceId)) + .orderBy(desc(supportsFormation.createdAt)); + + return results; +} + +/** + * Ajoute un support de formation + * @param support - Données du support à ajouter + */ +export async function ajouterSupport(support: { + sequenceId: number; + formateurId: number; + nomFichier: string; + typeFichier: string; + tailleFichier: number; + urlFichier: string; + s3Key: string; + description?: string; +}) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const [result] = await db.insert(supportsFormation).values(support); + return result; +} + +/** + * Supprime un support de formation + * @param supportId - ID du support à supprimer + * @param formateurId - ID du formateur (pour vérification) + */ +export async function supprimerSupport(supportId: number, formateurId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Vérifier que le support appartient bien au formateur + const [support] = await db + .select() + .from(supportsFormation) + .where( + and( + eq(supportsFormation.id, supportId), + eq(supportsFormation.formateurId, formateurId) + ) + ) + .limit(1); + + if (!support) { + throw new Error("Support non trouvé ou non autorisé"); + } + + await db.delete(supportsFormation).where(eq(supportsFormation.id, supportId)); + + return support; // Retourner le support pour récupérer la clé S3 +} + +/** + * Valide la présence d'un apprenant + * @param inscriptionId - ID de l'inscription + * @param present - true si présent, false sinon + */ +export async function validerPresence(inscriptionId: number, present: boolean) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db + .update(inscriptions) + .set({ statut: present ? 'confirmee' : 'annulee' }) + .where(eq(inscriptions.id, inscriptionId)); +} + +/** + * Récupère l'historique des formations d'un formateur + * @param formateurId - ID du formateur + * @param limit - Nombre de résultats à retourner (par défaut 50) + */ +export async function getHistoriqueFormateur(formateurId: number, limit: number = 50) { + const db = await getDb(); + if (!db) return []; + + const results = await db + .select({ + sequenceId: sequences.id, + sequenceNom: sequences.nom, + formationNom: formations.nom, + dateDebut: sql`MIN(${datesFormation.dateDebut})`, + dateFin: sql`MAX(${datesFormation.dateFin})`, + lieu: sequences.lieu, + nbInscrits: sql`( + SELECT COUNT(*) + FROM inscriptions + WHERE inscriptions.sequenceId = ${sequences.id} + AND inscriptions.statut IN ('confirmee', 'liste_attente') + )`, + nbPresents: sql`( + SELECT COUNT(*) + FROM inscriptions + WHERE inscriptions.sequenceId = ${sequences.id} + AND inscriptions.statut = 'confirmee' + )`, + }) + .from(sequences) + .innerJoin(formations, eq(sequences.formationId, formations.id)) + .innerJoin(datesFormation, eq(datesFormation.sequenceId, sequences.id)) + .where(eq(sequences.formateurId, formateurId)) + .groupBy(sequences.id, sequences.nom, formations.nom, sequences.lieu) + .orderBy(desc(sql`MIN(${datesFormation.dateDebut})`)) + .limit(limit); + + return results; +} + +/** + * Récupère les détails d'une séquence pour un formateur + * @param sequenceId - ID de la séquence + * @param formateurId - ID du formateur (pour vérification) + */ +export async function getDetailSequence(sequenceId: number, formateurId: number) { + const db = await getDb(); + if (!db) return null; + + const [result] = await db + .select({ + id: sequences.id, + nom: sequences.nom, + formationNom: formations.nom, + lieu: sequences.lieu, + capaciteMax: sequences.capaciteMax, + formateurNom: formateurs.nom, + }) + .from(sequences) + .innerJoin(formations, eq(sequences.formationId, formations.id)) + .innerJoin(formateurs, eq(sequences.formateurId, formateurs.id)) + .where( + and( + eq(sequences.id, sequenceId), + eq(sequences.formateurId, formateurId) + ) + ) + .limit(1); + + if (!result) return null; + + // Récupérer les dates de formation + const dates = await db + .select({ + id: datesFormation.id, + dateDebut: datesFormation.dateDebut, + dateFin: datesFormation.dateFin, + ordre: datesFormation.ordre, + }) + .from(datesFormation) + .where(eq(datesFormation.sequenceId, sequenceId)) + .orderBy(datesFormation.dateDebut); + + return { + ...result, + dates, + }; +} + +/** + * Récupérer les statistiques du tableau de bord pour un formateur + */ +export async function getFormateurDashboardStats(formateurId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Nombre total de séquences du formateur + const totalSequences = await db + .select({ count: sql`COUNT(*)` }) + .from(sequences) + .where(eq(sequences.formateurId, formateurId)); + + // Nombre total d'inscrits confirmés à toutes les séquences du formateur + const totalInscrits = await db + .select({ count: sql`COUNT(*)` }) + .from(inscriptions) + .innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id)) + .where( + and( + eq(sequences.formateurId, formateurId), + eq(inscriptions.statut, 'confirmee') + ) + ); + + // Nombre de séquences à venir (ayant au moins une date future) + const today = new Date(); + const sequencesAvenir = await db + .select({ sequenceId: datesFormation.sequenceId }) + .from(datesFormation) + .innerJoin(sequences, eq(datesFormation.sequenceId, sequences.id)) + .where( + and( + eq(sequences.formateurId, formateurId), + gte(datesFormation.dateDebut, today) + ) + ) + .groupBy(datesFormation.sequenceId); + + return { + totalSequences: totalSequences[0]?.count || 0, + totalInscrits: totalInscrits[0]?.count || 0, + sequencesAvenir: sequencesAvenir.length, + }; +} + +/** + * Récupérer les prochaines séquences du formateur avec leurs détails + */ +export async function getFormateurProchainesSequences(formateurId: number, limit: number = 10) { + const db = await getDb(); + if (!db) return []; + + const today = new Date(); + + // Récupérer les séquences du formateur ayant au moins une date future + const seqs = await db + .select() + .from(sequences) + .where(eq(sequences.formateurId, formateurId)); + + // Pour chaque séquence, récupérer les détails + const sequencesAvecDetails = await Promise.all( + seqs.map(async (seq) => { + // Récupérer toutes les dates de la séquence + const dates = await db + .select() + .from(datesFormation) + .where(eq(datesFormation.sequenceId, seq.id)) + .orderBy(datesFormation.dateDebut); + + // Vérifier si au moins une date est future + const hasFutureDate = dates.some(d => new Date(d.dateDebut) >= today); + + if (!hasFutureDate) return null; + + // Récupérer la formation + const formation = await db + .select() + .from(formations) + .where(eq(formations.id, seq.formationId)) + .limit(1); + + // Compter les inscrits confirmés + const inscritsCount = await db + .select({ count: sql`COUNT(*)` }) + .from(inscriptions) + .where( + and( + eq(inscriptions.sequenceId, seq.id), + eq(inscriptions.statut, 'confirmee') + ) + ); + + return { + ...seq, + formation: formation[0] || null, + dates, + nbInscrits: inscritsCount[0]?.count || 0, + prochaineDateDebut: dates.find(d => new Date(d.dateDebut) >= today)?.dateDebut || dates[0]?.dateDebut, + }; + }) + ); + + // Filtrer les séquences nulles et trier par prochaine date + const sequencesFiltrees = sequencesAvecDetails + .filter(s => s !== null) + .sort((a, b) => { + const dateA = new Date(a!.prochaineDateDebut); + const dateB = new Date(b!.prochaineDateDebut); + return dateA.getTime() - dateB.getTime(); + }) + .slice(0, limit); + + return sequencesFiltrees; +} + +/** + * Récupérer l'email du formateur à partir de son ID + */ +export async function getFormateurEmailById(formateurId: number): Promise { + const db = await getDb(); + if (!db) return null; + + const result = await db + .select({ email: formateurs.email }) + .from(formateurs) + .where(eq(formateurs.id, formateurId)) + .limit(1); + + return result[0]?.email || null; +} diff --git a/deployment-package/source_server/gestionAttestationsDb.ts b/deployment-package/source_server/gestionAttestationsDb.ts new file mode 100644 index 0000000..25e1374 --- /dev/null +++ b/deployment-package/source_server/gestionAttestationsDb.ts @@ -0,0 +1,202 @@ +import { eq, and, desc, sql } from "drizzle-orm"; +import { getDb } from "./db"; +import { formations, attestations, inscriptions, apprenants, sequences, datesFormation } from "../drizzle/schema"; + +/** + * Récupérer la configuration d'une formation + */ +export async function getFormationConfig(formationId: number) { + const db = await getDb(); + if (!db) return null; + + const results = await db + .select({ + id: formations.id, + nom: formations.nom, + modeAttestation: formations.modeAttestation, + modeEnvoi: formations.modeEnvoi, + }) + .from(formations) + .where(eq(formations.id, formationId)) + .limit(1); + + return results.length > 0 ? results[0] : null; +} + +/** + * Mettre à jour la configuration d'une formation + */ +export async function updateFormationConfig( + formationId: number, + modeAttestation: "auto" | "manuel", + modeEnvoi: "auto" | "manuel" +) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db + .update(formations) + .set({ + modeAttestation, + modeEnvoi, + updatedAt: new Date(), + }) + .where(eq(formations.id, formationId)); + + return true; +} + +/** + * Récupérer toutes les attestations d'une formation avec les informations des apprenants + */ +export async function getAttestationsByFormation(formationId: number) { + const db = await getDb(); + if (!db) return []; + + const results = await db + .select({ + attestation: attestations, + apprenant: apprenants, + sequence: sequences, + }) + .from(attestations) + .innerJoin(inscriptions, eq(attestations.inscriptionId, inscriptions.id)) + .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id)) + .where(eq(sequences.formationId, formationId)) + .orderBy(desc(attestations.createdAt)); + + return results; +} + +/** + * Uploader un document d'attestation pour un apprenant + */ +export async function uploadAttestationDocument( + inscriptionId: number, + documentUrl: string, + documentS3Key: string, + uploadedBy: number +) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Vérifier si une attestation existe déjà pour cette inscription + const existing = await db + .select() + .from(attestations) + .where(eq(attestations.inscriptionId, inscriptionId)) + .limit(1); + + if (existing.length > 0) { + // Mettre à jour l'attestation existante + await db + .update(attestations) + .set({ + documentUrl, + documentS3Key, + uploadedBy, + uploadedAt: new Date(), + }) + .where(eq(attestations.inscriptionId, inscriptionId)); + + return existing[0].id; + } else { + // Créer une nouvelle attestation + const result = await db + .insert(attestations) + .values({ + inscriptionId, + documentUrl, + documentS3Key, + uploadedBy, + uploadedAt: new Date(), + }) + .$returningId(); + + return result[0].id; + } +} + +/** + * Envoyer une attestation par email + */ +export async function markAttestationAsSent(attestationId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db + .update(attestations) + .set({ + emailEnvoye: true, + dateEnvoiEmail: new Date(), + }) + .where(eq(attestations.id, attestationId)); + + return true; +} + +/** + * Récupérer les apprenants d'une formation avec leur statut d'attestation + */ +export async function getApprenantsWithAttestationStatus(formationId: number) { + const db = await getDb(); + if (!db) return []; + + const results = await db + .select({ + apprenant: { + id: apprenants.id, + nom: apprenants.nom, + prenom: apprenants.prenom, + email: apprenants.email, + }, + inscription: { + id: inscriptions.id, + sequenceId: inscriptions.sequenceId, + }, + sequence: { + id: sequences.id, + nom: sequences.nom, + }, + attestation: { + id: attestations.id, + documentUrl: attestations.documentUrl, + emailEnvoye: attestations.emailEnvoye, + dateEnvoiEmail: attestations.dateEnvoiEmail, + }, + }) + .from(inscriptions) + .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id)) + .leftJoin(attestations, eq(attestations.inscriptionId, inscriptions.id)) + .where( + and( + eq(sequences.formationId, formationId), + eq(inscriptions.statut, "confirmee") + ) + ) + .orderBy(apprenants.nom, apprenants.prenom); + + return results; +} + +/** + * Supprimer un document d'attestation + */ +export async function deleteAttestationDocument(attestationId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db + .update(attestations) + .set({ + documentUrl: null, + documentS3Key: null, + uploadedBy: null, + uploadedAt: null, + }) + .where(eq(attestations.id, attestationId)); + + return true; +} diff --git a/deployment-package/source_server/icsGenerator.ts b/deployment-package/source_server/icsGenerator.ts new file mode 100644 index 0000000..b73fb2c --- /dev/null +++ b/deployment-package/source_server/icsGenerator.ts @@ -0,0 +1,109 @@ +/** + * Module de génération de fichiers ICS (iCalendar) pour les invitations Outlook + */ + +interface ICSEvent { + summary: string; + description?: string; + location: string; + startDate: Date; + endDate: Date; + attendeeEmail: string; + attendeeName: string; + organizerEmail?: string; + organizerName?: string; +} + +/** + * Formate une date au format iCalendar (YYYYMMDDTHHMMSSZ) + */ +function formatICSDate(date: Date): string { + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); + const hours = String(date.getUTCHours()).padStart(2, '0'); + const minutes = String(date.getUTCMinutes()).padStart(2, '0'); + const seconds = String(date.getUTCSeconds()).padStart(2, '0'); + + return `${year}${month}${day}T${hours}${minutes}${seconds}Z`; +} + +/** + * Génère un UID unique pour l'événement + */ +function generateUID(): string { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}@itinova.com`; +} + +/** + * Échappe les caractères spéciaux pour le format ICS + */ +function escapeICS(text: string): string { + return text + .replace(/\\/g, '\\\\') + .replace(/;/g, '\\;') + .replace(/,/g, '\\,') + .replace(/\n/g, '\\n'); +} + +/** + * Génère un fichier ICS pour une invitation Outlook + */ +export function generateICS(event: ICSEvent): string { + const now = new Date(); + const uid = generateUID(); + + const icsContent = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Itinova//Formation Manager//FR', + 'CALSCALE:GREGORIAN', + 'METHOD:REQUEST', + 'BEGIN:VEVENT', + `UID:${uid}`, + `DTSTAMP:${formatICSDate(now)}`, + `DTSTART:${formatICSDate(event.startDate)}`, + `DTEND:${formatICSDate(event.endDate)}`, + `SUMMARY:${escapeICS(event.summary)}`, + event.description ? `DESCRIPTION:${escapeICS(event.description)}` : '', + `LOCATION:${escapeICS(event.location)}`, + 'STATUS:CONFIRMED', + 'TRANSP:OPAQUE', // Marque comme "occupé" dans le calendrier + 'SEQUENCE:0', + `ORGANIZER;CN=${escapeICS(event.organizerName || 'Formation Itinova')}:mailto:${event.organizerEmail || 'formation@itinova.com'}`, + `ATTENDEE;CN=${escapeICS(event.attendeeName)};RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:${event.attendeeEmail}`, + 'BEGIN:VALARM', + 'TRIGGER:-P1D', // Rappel 1 jour avant + 'ACTION:DISPLAY', + `DESCRIPTION:Rappel: ${escapeICS(event.summary)}`, + 'END:VALARM', + 'END:VEVENT', + 'END:VCALENDAR', + ].filter(line => line !== '').join('\r\n'); + + return icsContent; +} + +/** + * Génère un fichier ICS pour une session de formation + */ +export function generateFormationICS(params: { + formationNom: string; + sessionNom: string; + dateDebut: Date; + dateFin: Date; + lieu: string; + apprenantNom: string; + apprenantPrenom: string; + apprenantEmail: string; +}): string { + return generateICS({ + summary: `Formation: ${params.formationNom} - ${params.sessionNom}`, + description: `Vous êtes inscrit à la formation "${params.formationNom}".\n\nSession: ${params.sessionNom}\n\nMerci de vous présenter à l'heure indiquée.`, + location: params.lieu, + startDate: params.dateDebut, + endDate: params.dateFin, + attendeeEmail: params.apprenantEmail, + attendeeName: `${params.apprenantPrenom} ${params.apprenantNom}`, + }); +} diff --git a/deployment-package/source_server/importExcel.ts b/deployment-package/source_server/importExcel.ts new file mode 100644 index 0000000..270fa15 --- /dev/null +++ b/deployment-package/source_server/importExcel.ts @@ -0,0 +1,331 @@ +import * as XLSX from 'xlsx'; +import { getDb } from './db'; +import { formations as formationsTable, sequences as sequencesTable, datesFormation } from '../drizzle/schema'; + +export interface FormationImport { + nom: string; + description: string; + publicCible: 'directeur' | 'chef_service' | 'tous' | 'autre'; +} + +export interface SequenceImport { + formationNom: string; + nom: string; + lieu: string; + publicCible: 'directeur' | 'chef_service' | 'tous' | 'autre'; + capaciteMax: number; + dateBlocage: string; + statut: 'ouverte' | 'bloquee' | 'terminee'; + dates: Array<{ debut: string; fin: string }>; +} + +export interface ImportResult { + success: boolean; + formationsCreated: number; + sequencesCreated: number; + errors: string[]; + warnings: string[]; +} + +/** + * Parse un fichier Excel et retourne les données structurées + */ +export function parseExcelFile(buffer: Buffer): { formations: FormationImport[]; sequences: SequenceImport[] } { + const workbook = XLSX.read(buffer, { type: 'buffer' }); + + const formations: FormationImport[] = []; + const sequences: SequenceImport[] = []; + + // Parser la feuille "Formations" + if (workbook.SheetNames.includes('Formations')) { + const sheet = workbook.Sheets['Formations']; + const data = XLSX.utils.sheet_to_json(sheet, { header: 1 }); + + // Ignorer la première ligne (en-têtes) et les lignes vides + for (let i = 1; i < data.length; i++) { + const row = data[i]; + if (!row || row.length === 0 || !row[0]) continue; + + // Arrêter si on atteint les instructions + if (String(row[0]).toUpperCase().includes('INSTRUCTIONS')) break; + + formations.push({ + nom: String(row[0] || '').trim(), + description: String(row[1] || '').trim(), + publicCible: normalizePublicCible(String(row[2] || '').trim()), + }); + } + } + + // Parser la feuille "Séquences" + if (workbook.SheetNames.includes('Séquences')) { + const sheet = workbook.Sheets['Séquences']; + const data = XLSX.utils.sheet_to_json(sheet, { header: 1 }); + + for (let i = 1; i < data.length; i++) { + const row = data[i]; + if (!row || row.length === 0 || !row[0]) continue; + + // Arrêter si on atteint les instructions + if (String(row[0]).toUpperCase().includes('INSTRUCTIONS')) break; + + const dates: Array<{ debut: string; fin: string }> = []; + + // Parser les 4 dates possibles (colonnes 7-14) + for (let d = 0; d < 4; d++) { + const debutIdx = 7 + (d * 2); + const finIdx = 8 + (d * 2); + + if (row[debutIdx] && row[finIdx]) { + dates.push({ + debut: parseExcelDate(row[debutIdx]), + fin: parseExcelDate(row[finIdx]), + }); + } + } + + sequences.push({ + formationNom: String(row[0] || '').trim(), + nom: String(row[1] || '').trim(), + lieu: String(row[2] || '').trim(), + publicCible: normalizePublicCible(String(row[3] || '').trim()), + capaciteMax: parseInt(String(row[4] || '12')), + dateBlocage: parseExcelDate(row[5]), + statut: normalizeStatut(String(row[6] || '').trim()), + dates, + }); + } + } + + return { formations, sequences }; +} + +/** + * Valide les données importées + */ +export function validateImportData( + formations: FormationImport[], + sequences: SequenceImport[] +): { valid: boolean; errors: string[]; warnings: string[] } { + const errors: string[] = []; + const warnings: string[] = []; + + // Valider les formations + formations.forEach((formation, index) => { + if (!formation.nom) { + errors.push(`Formation ligne ${index + 2}: Le nom est obligatoire`); + } + if (!formation.description) { + errors.push(`Formation ligne ${index + 2}: La description est obligatoire`); + } + if (!['directeur', 'chef_service', 'tous', 'autre'].includes(formation.publicCible)) { + errors.push(`Formation ligne ${index + 2}: Public cible invalide (${formation.publicCible})`); + } + }); + + // Créer un set des noms de formations pour validation des séquences + const formationNames = new Set(formations.map(f => f.nom.toLowerCase())); + + // Valider les séquences + sequences.forEach((sequence, index) => { + if (!sequence.formationNom) { + errors.push(`Séquence ligne ${index + 2}: Le nom de la formation est obligatoire`); + } else if (!formationNames.has(sequence.formationNom.toLowerCase())) { + errors.push(`Séquence ligne ${index + 2}: Formation "${sequence.formationNom}" non trouvée dans la feuille Formations`); + } + + if (!sequence.nom) { + errors.push(`Séquence ligne ${index + 2}: Le nom est obligatoire`); + } + if (!sequence.lieu) { + errors.push(`Séquence ligne ${index + 2}: Le lieu est obligatoire`); + } + if (!['directeur', 'chef_service', 'tous', 'autre'].includes(sequence.publicCible)) { + errors.push(`Séquence ligne ${index + 2}: Public cible invalide`); + } + if (!sequence.dateBlocage) { + errors.push(`Séquence ligne ${index + 2}: La date de blocage est obligatoire`); + } + if (!['ouverte', 'bloquee', 'terminee'].includes(sequence.statut)) { + errors.push(`Séquence ligne ${index + 2}: Statut invalide`); + } + if (sequence.dates.length === 0) { + errors.push(`Séquence ligne ${index + 2}: Au moins une date est obligatoire`); + } + if (sequence.dates.length > 4) { + warnings.push(`Séquence ligne ${index + 2}: Maximum 4 dates autorisées, les dates supplémentaires seront ignorées`); + } + }); + + return { + valid: errors.length === 0, + errors, + warnings, + }; +} + +/** + * Importe les données dans la base de données + */ +export async function importToDatabase( + formations: FormationImport[], + sequences: SequenceImport[] +): Promise { + const db = await getDb(); + if (!db) { + return { + success: false, + formationsCreated: 0, + sequencesCreated: 0, + errors: ['Connexion à la base de données impossible'], + warnings: [], + }; + } + + const errors: string[] = []; + const warnings: string[] = []; + let formationsCreated = 0; + let sequencesCreated = 0; + + try { + // Map pour stocker les IDs des formations créées + const formationIdMap = new Map(); + + // Importer les formations + console.log(`[Import] Début import de ${formations.length} formations`); + for (const formation of formations) { + try { + console.log(`[Import] Création formation: ${formation.nom}`); + const [result] = await db.insert(formationsTable).values({ + nom: formation.nom, + description: formation.description, + lienUnique: generateUniqueLien(), + }).$returningId(); + + const formationId = result.id; + formationIdMap.set(formation.nom.toLowerCase(), formationId); + console.log(`[Import] Formation créée avec ID ${formationId}, ajoutée à la map avec clé: "${formation.nom.toLowerCase()}"`); + formationsCreated++; + } catch (error: any) { + console.error(`[Import] Erreur formation "${formation.nom}":`, error); + errors.push(`Erreur lors de la création de la formation "${formation.nom}": ${error.message}`); + } + } + console.log(`[Import] Formations créées: ${formationsCreated}, Map size: ${formationIdMap.size}`); + console.log(`[Import] Clés dans la map:`, Array.from(formationIdMap.keys())); + + // Si des erreurs sont survenues lors de la création des formations, arrêter l'import + if (errors.length > 0) { + console.error(`[Import] Arrêt de l'import car ${errors.length} erreur(s) lors de la création des formations`); + return { + success: false, + formationsCreated, + sequencesCreated: 0, + errors, + warnings, + }; + } + + // Importer les séquences + console.log(`[Import] Début import de ${sequences.length} séquences`); + for (const sequence of sequences) { + try { + const searchKey = sequence.formationNom.toLowerCase(); + console.log(`[Import] Recherche formation pour séquence "${sequence.nom}" avec clé: "${searchKey}"`); + const formationId = formationIdMap.get(searchKey); + if (!formationId) { + errors.push(`Formation "${sequence.formationNom}" non trouvée pour la séquence "${sequence.nom}"`); + continue; + } + + const [result] = await db.insert(sequencesTable).values({ + formationId, + nom: sequence.nom, + lieu: sequence.lieu, + publicCible: sequence.publicCible, + capaciteMax: sequence.capaciteMax, + dateBlocage: new Date(sequence.dateBlocage), + statut: sequence.statut, + }).$returningId(); + + const sequenceId = result.id; + + // Insérer les dates de formation + for (let i = 0; i < Math.min(sequence.dates.length, 4); i++) { + const date = sequence.dates[i]; + await db.insert(datesFormation).values({ + sequenceId, + dateDebut: new Date(date.debut), + dateFin: new Date(date.fin), + ordre: i + 1, + }); + } + + sequencesCreated++; + } catch (error: any) { + errors.push(`Erreur lors de la création de la séquence "${sequence.nom}": ${error.message}`); + } + } + + return { + success: errors.length === 0, + formationsCreated, + sequencesCreated, + errors, + warnings, + }; + } catch (error: any) { + return { + success: false, + formationsCreated, + sequencesCreated, + errors: [`Erreur générale: ${error.message}`], + warnings, + }; + } +} + +/** + * Normalise le public cible + */ +function normalizePublicCible(value: string): 'directeur' | 'chef_service' | 'tous' | 'autre' { + const normalized = value.toLowerCase().trim(); + if (normalized === 'directeur') return 'directeur'; + if (normalized === 'chef_service' || normalized === 'chef de service') return 'chef_service'; + if (normalized === 'tous') return 'tous'; + return 'autre'; +} + +/** + * Normalise le statut + */ +function normalizeStatut(value: string): 'ouverte' | 'bloquee' | 'terminee' { + const normalized = value.toLowerCase().trim(); + if (normalized === 'ouverte') return 'ouverte'; + if (normalized === 'bloquee' || normalized === 'bloquée') return 'bloquee'; + if (normalized === 'terminee' || normalized === 'terminée') return 'terminee'; + return 'ouverte'; +} + +/** + * Parse une date Excel (peut être un nombre de série Excel ou une chaîne) + */ +function parseExcelDate(value: any): string { + if (!value) return ''; + + // Si c'est un nombre (date Excel) + if (typeof value === 'number') { + const date = XLSX.SSF.parse_date_code(value); + return `${date.y}-${String(date.m).padStart(2, '0')}-${String(date.d).padStart(2, '0')} ${String(date.H || 0).padStart(2, '0')}:${String(date.M || 0).padStart(2, '0')}`; + } + + // Si c'est déjà une chaîne + return String(value).trim(); +} + +/** + * Génère un lien unique pour une formation + */ +function generateUniqueLien(): string { + return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); +} diff --git a/deployment-package/source_server/inscriptionWithDates.ts b/deployment-package/source_server/inscriptionWithDates.ts new file mode 100644 index 0000000..cfcd627 --- /dev/null +++ b/deployment-package/source_server/inscriptionWithDates.ts @@ -0,0 +1,85 @@ +import { eq, ne, and } from "drizzle-orm"; +import { inscriptions, apprenants, sequences, datesFormation } from "../drizzle/schema"; +import { getDb } from "./db"; + +/** + * Récupérer les inscriptions d'une séquence avec les dates de formation + */ +export async function getInscriptionsWithDates(sequenceId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer les inscriptions + const results = await db + .select({ + inscription: inscriptions, + apprenant: apprenants, + }) + .from(inscriptions) + .leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .where( + and( + eq(inscriptions.sequenceId, sequenceId), + ne(inscriptions.statut, "annulee") + ) + ); + + // Récupérer les dates de formation pour cette séquence + const dates = await db + .select() + .from(datesFormation) + .where(eq(datesFormation.sequenceId, sequenceId)) + .orderBy(datesFormation.ordre); + + // Combiner les données + return results.map((r) => ({ + ...r, + dates, + })); +} + +/** + * Récupérer les inscriptions d'un apprenant avec les détails des séquences et dates + */ +export async function getInscriptionsByApprenantWithDates(apprenantId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer les inscriptions + const results = await db + .select({ + inscription: inscriptions, + sequence: sequences, + }) + .from(inscriptions) + .leftJoin(sequences, eq(inscriptions.sequenceId, sequences.id)) + .where( + and( + eq(inscriptions.apprenantId, apprenantId), + ne(inscriptions.statut, "annulee") + ) + ); + + // Pour chaque inscription, récupérer les dates de formation + const resultsWithDates = await Promise.all( + results.map(async (r) => { + if (!r.sequence) return { ...r, dates: [] }; + + const dates = await db + .select() + .from(datesFormation) + .where(eq(datesFormation.sequenceId, r.sequence.id)) + .orderBy(datesFormation.ordre); + + return { + ...r, + sequence: { + ...r.sequence, + dates, + }, + }; + }) + ); + + return resultsWithDates; +} diff --git a/deployment-package/source_server/notificationLogsDb.ts b/deployment-package/source_server/notificationLogsDb.ts new file mode 100644 index 0000000..e1f19a9 --- /dev/null +++ b/deployment-package/source_server/notificationLogsDb.ts @@ -0,0 +1,210 @@ +import { eq, desc, and, gte, lte, sql, like } from "drizzle-orm"; +import { getDb } from "./db"; +import { logsNotifications, sequences, apprenants, formateurs, formations } from "../drizzle/schema"; + +export type NotificationType = + | "remerciement" + | "notification_formateur_inscription" + | "notification_formateur_annulation" + | "alerte_capacite" + | "notification_liste_attente"; + +export interface LogNotificationInput { + type: NotificationType; + sequenceId?: number; + apprenantId?: number; + formateurId?: number; + emailDestinataire: string; + sujet: string; + statut: "success" | "failed"; + messageErreur?: string; + metadata?: Record; +} + +/** + * Enregistre un log de notification + */ +export async function logNotification(input: LogNotificationInput) { + const db = await getDb(); + if (!db) { + console.warn("[NotificationLogs] Database not available"); + return; + } + + try { + await db.insert(logsNotifications).values({ + type: input.type, + sequenceId: input.sequenceId || null, + apprenantId: input.apprenantId || null, + formateurId: input.formateurId || null, + emailDestinataire: input.emailDestinataire, + sujet: input.sujet, + statut: input.statut, + messageErreur: input.messageErreur || null, + metadata: input.metadata ? JSON.stringify(input.metadata) : null, + }); + console.log(`[NotificationLogs] Log créé: ${input.type} -> ${input.emailDestinataire} (${input.statut})`); + } catch (error) { + console.error("[NotificationLogs] Erreur lors de la création du log:", error); + } +} + +/** + * Récupère l'historique des notifications avec filtres + */ +export async function getNotificationLogs(filters?: { + type?: NotificationType; + dateDebut?: Date; + dateFin?: Date; + statut?: "success" | "failed"; + email?: string; + sequenceId?: number; + limit?: number; + offset?: number; +}) { + const db = await getDb(); + if (!db) return { logs: [], total: 0 }; + + const conditions = []; + + if (filters?.type) { + conditions.push(eq(logsNotifications.type, filters.type)); + } + if (filters?.dateDebut) { + conditions.push(gte(logsNotifications.dateEnvoi, filters.dateDebut)); + } + if (filters?.dateFin) { + conditions.push(lte(logsNotifications.dateEnvoi, filters.dateFin)); + } + if (filters?.statut) { + conditions.push(eq(logsNotifications.statut, filters.statut)); + } + if (filters?.email) { + conditions.push(like(logsNotifications.emailDestinataire, `%${filters.email}%`)); + } + if (filters?.sequenceId) { + conditions.push(eq(logsNotifications.sequenceId, filters.sequenceId)); + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined; + + // Récupérer les logs avec les informations associées + const logs = await db + .select({ + id: logsNotifications.id, + type: logsNotifications.type, + sequenceId: logsNotifications.sequenceId, + apprenantId: logsNotifications.apprenantId, + formateurId: logsNotifications.formateurId, + emailDestinataire: logsNotifications.emailDestinataire, + sujet: logsNotifications.sujet, + dateEnvoi: logsNotifications.dateEnvoi, + statut: logsNotifications.statut, + messageErreur: logsNotifications.messageErreur, + metadata: logsNotifications.metadata, + sequenceNom: sequences.nom, + formationNom: formations.nom, + apprenantNom: apprenants.nom, + apprenantPrenom: apprenants.prenom, + formateurNom: formateurs.nom, + }) + .from(logsNotifications) + .leftJoin(sequences, eq(logsNotifications.sequenceId, sequences.id)) + .leftJoin(formations, eq(sequences.formationId, formations.id)) + .leftJoin(apprenants, eq(logsNotifications.apprenantId, apprenants.id)) + .leftJoin(formateurs, eq(logsNotifications.formateurId, formateurs.id)) + .where(whereClause) + .orderBy(desc(logsNotifications.dateEnvoi)) + .limit(filters?.limit || 50) + .offset(filters?.offset || 0); + + // Compter le total + const [countResult] = await db + .select({ count: sql`COUNT(*)` }) + .from(logsNotifications) + .where(whereClause); + + return { + logs, + total: countResult?.count || 0, + }; +} + +/** + * Récupère les statistiques des notifications + */ +export async function getNotificationStats(filters?: { + dateDebut?: Date; + dateFin?: Date; +}) { + const db = await getDb(); + if (!db) return null; + + const conditions = []; + if (filters?.dateDebut) { + conditions.push(gte(logsNotifications.dateEnvoi, filters.dateDebut)); + } + if (filters?.dateFin) { + conditions.push(lte(logsNotifications.dateEnvoi, filters.dateFin)); + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined; + + // Statistiques globales + const [globalStats] = await db + .select({ + total: sql`COUNT(*)`, + success: sql`SUM(CASE WHEN statut = 'success' THEN 1 ELSE 0 END)`, + failed: sql`SUM(CASE WHEN statut = 'failed' THEN 1 ELSE 0 END)`, + }) + .from(logsNotifications) + .where(whereClause); + + // Statistiques par type + const statsByType = await db + .select({ + type: logsNotifications.type, + total: sql`COUNT(*)`, + success: sql`SUM(CASE WHEN statut = 'success' THEN 1 ELSE 0 END)`, + failed: sql`SUM(CASE WHEN statut = 'failed' THEN 1 ELSE 0 END)`, + }) + .from(logsNotifications) + .where(whereClause) + .groupBy(logsNotifications.type); + + // Évolution par jour (7 derniers jours) + const evolutionParJour = await db + .select({ + date: sql`DATE(dateEnvoi)`, + total: sql`COUNT(*)`, + success: sql`SUM(CASE WHEN statut = 'success' THEN 1 ELSE 0 END)`, + failed: sql`SUM(CASE WHEN statut = 'failed' THEN 1 ELSE 0 END)`, + }) + .from(logsNotifications) + .where(whereClause) + .groupBy(sql`DATE(dateEnvoi)`) + .orderBy(sql`DATE(dateEnvoi)`) + .limit(30); + + return { + global: { + total: globalStats?.total || 0, + success: globalStats?.success || 0, + failed: globalStats?.failed || 0, + tauxSucces: globalStats?.total ? Math.round((globalStats.success / globalStats.total) * 100) : 0, + }, + parType: statsByType, + evolutionParJour, + }; +} + +/** + * Labels pour les types de notifications + */ +export const notificationTypeLabels: Record = { + remerciement: "Remerciement post-formation", + notification_formateur_inscription: "Notification formateur (inscription)", + notification_formateur_annulation: "Notification formateur (annulation)", + alerte_capacite: "Alerte capacité atteinte", + notification_liste_attente: "Notification liste d'attente", +}; diff --git a/deployment-package/source_server/parametresDb.ts b/deployment-package/source_server/parametresDb.ts new file mode 100644 index 0000000..92df487 --- /dev/null +++ b/deployment-package/source_server/parametresDb.ts @@ -0,0 +1,134 @@ +import { eq, sql } from "drizzle-orm"; +import { getDb } from "./db"; +import { parametres, historiqueParametres } from "../drizzle/schema"; + +/** + * Récupérer les paramètres de l'application + * S'il n'y a pas de paramètres, en créer un avec les valeurs par défaut + */ +export async function getParametres() { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db.select().from(parametres).limit(1); + + if (result.length === 0) { + // Créer les paramètres par défaut + await db.insert(parametres).values({ + urlPublique: "https://formations.itinova.org", + }); + + const newResult = await db.select().from(parametres).limit(1); + return newResult[0]; + } + + return result[0]; +} + +/** + * Mettre à jour les paramètres de l'application + * Enregistre automatiquement l'historique des modifications + */ +export async function updateParametres( + updates: Partial<{ + urlPublique: string; + delaiExpirationQR: number; + dureeValiditeToken: number; + notificationsActives: boolean; + envoiAutomatiqueAttestations: boolean; + }>, + userId: number, + userName: string +) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const params = await getParametres(); + + // Enregistrer l'historique pour chaque champ modifié + for (const [key, value] of Object.entries(updates)) { + const oldValue = params[key as keyof typeof params]; + if (oldValue !== value) { + await db.insert(historiqueParametres).values({ + userId, + userName, + champModifie: key, + ancienneValeur: oldValue !== null && oldValue !== undefined ? String(oldValue) : null, + nouvelleValeur: value !== null && value !== undefined ? String(value) : null, + }); + } + } + + // Mettre à jour les paramètres + await db.update(parametres) + .set(updates) + .where(eq(parametres.id, params.id)); + + return getParametres(); +} + +/** + * Mettre à jour l'URL publique (fonction de compatibilité) + */ +export async function updateUrlPublique(urlPublique: string, userId?: number, userName?: string) { + return updateParametres( + { urlPublique }, + userId || 1, + userName || "Système" + ); +} + +/** + * Récupérer l'historique des modifications des paramètres + */ +export async function getHistoriqueParametres(limit: number = 50) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + return db.select() + .from(historiqueParametres) + .orderBy(sql`${historiqueParametres.dateModification} DESC`) + .limit(limit); +} + +/** + * Tester l'accessibilité d'une URL + */ +export async function testerUrl(url: string): Promise<{ accessible: boolean; message: string; statusCode?: number }> { + try { + // Vérifier le format de l'URL + const urlObj = new URL(url); + if (!['http:', 'https:'].includes(urlObj.protocol)) { + return { + accessible: false, + message: "L'URL doit commencer par http:// ou https://" + }; + } + + // Tenter une requête HEAD pour vérifier l'accessibilité + const response = await fetch(url, { + method: 'HEAD', + signal: AbortSignal.timeout(5000), // Timeout de 5 secondes + }); + + return { + accessible: response.ok, + message: response.ok + ? "L'URL est accessible" + : `L'URL a retourné une erreur HTTP ${response.status}`, + statusCode: response.status + }; + } catch (error) { + if (error instanceof TypeError && error.message.includes('Invalid URL')) { + return { + accessible: false, + message: "Format d'URL invalide" + }; + } + + return { + accessible: false, + message: `Erreur lors du test: ${error instanceof Error ? error.message : 'Erreur inconnue'}` + }; + } +} diff --git a/deployment-package/source_server/presenceDb.ts b/deployment-package/source_server/presenceDb.ts new file mode 100644 index 0000000..adb49f5 --- /dev/null +++ b/deployment-package/source_server/presenceDb.ts @@ -0,0 +1,149 @@ +import { eq, and } from "drizzle-orm"; +import { presences, inscriptions, datesFormation, sequences, apprenants } from "../drizzle/schema"; +import { getDb } from "./db"; + +/** + * Valider la présence d'un apprenant (par QR code ou manuellement) + */ +export async function validerPresence(params: { + inscriptionId: number; + dateFormationId: number; + modeValidation: "qrcode" | "manuel"; + validateurId?: number; + commentaire?: string; +}) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Vérifier si la présence existe déjà + const presenceExistante = await db + .select() + .from(presences) + .where( + and( + eq(presences.inscriptionId, params.inscriptionId), + eq(presences.dateFormationId, params.dateFormationId) + ) + ) + .limit(1); + + if (presenceExistante.length > 0) { + throw new Error("Présence déjà validée pour cette date"); + } + + // Créer la présence + await db.insert(presences).values({ + inscriptionId: params.inscriptionId, + dateFormationId: params.dateFormationId, + heurePresence: new Date(), + modeValidation: params.modeValidation, + validateurId: params.validateurId, + commentaire: params.commentaire, + }); + + return { success: true }; +} + +/** + * Récupérer toutes les présences pour une séquence + */ +export async function getPresencesBySequence(sequenceId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db + .select({ + presenceId: presences.id, + inscriptionId: presences.inscriptionId, + dateFormationId: presences.dateFormationId, + heurePresence: presences.heurePresence, + modeValidation: presences.modeValidation, + validateurId: presences.validateurId, + commentaire: presences.commentaire, + apprenantId: apprenants.id, + apprenantNom: apprenants.nom, + apprenantPrenom: apprenants.prenom, + apprenantEmail: apprenants.email, + dateDebut: datesFormation.dateDebut, + dateFin: datesFormation.dateFin, + }) + .from(presences) + .innerJoin(inscriptions, eq(presences.inscriptionId, inscriptions.id)) + .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .innerJoin(datesFormation, eq(presences.dateFormationId, datesFormation.id)) + .where(eq(datesFormation.sequenceId, sequenceId)); + + return result; +} + +/** + * Récupérer les présences pour une inscription spécifique + */ +export async function getPresencesByInscription(inscriptionId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db + .select({ + presenceId: presences.id, + dateFormationId: presences.dateFormationId, + heurePresence: presences.heurePresence, + modeValidation: presences.modeValidation, + validateurId: presences.validateurId, + commentaire: presences.commentaire, + dateDebut: datesFormation.dateDebut, + dateFin: datesFormation.dateFin, + }) + .from(presences) + .innerJoin(datesFormation, eq(presences.dateFormationId, datesFormation.id)) + .where(eq(presences.inscriptionId, inscriptionId)); + + return result; +} + +/** + * Vérifier si toutes les présences sont validées pour une inscription + */ +export async function checkAllPresencesValidated(inscriptionId: number): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Récupérer l'inscription avec la séquence + const inscription = await db + .select({ + sequenceId: inscriptions.sequenceId, + }) + .from(inscriptions) + .where(eq(inscriptions.id, inscriptionId)) + .limit(1); + + if (inscription.length === 0) { + throw new Error("Inscription not found"); + } + + // Compter le nombre de dates de formation pour cette séquence + const datesCount = await db + .select({ count: datesFormation.id }) + .from(datesFormation) + .where(eq(datesFormation.sequenceId, inscription[0].sequenceId)); + + // Compter le nombre de présences validées pour cette inscription + const presencesCount = await db + .select({ count: presences.id }) + .from(presences) + .where(eq(presences.inscriptionId, inscriptionId)); + + return presencesCount.length === datesCount.length; +} + +/** + * Supprimer une présence + */ +export async function supprimerPresence(presenceId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + await db.delete(presences).where(eq(presences.id, presenceId)); + + return { success: true }; +} diff --git a/deployment-package/source_server/qrCodeGenerator.ts b/deployment-package/source_server/qrCodeGenerator.ts new file mode 100644 index 0000000..0d081a3 --- /dev/null +++ b/deployment-package/source_server/qrCodeGenerator.ts @@ -0,0 +1,63 @@ +import QRCode from "qrcode"; +import { randomBytes } from "crypto"; +import { getParametres } from "./parametresDb"; + +/** + * Générer un token unique pour le QR code + */ +export function generateQRToken(): string { + return randomBytes(32).toString("hex"); +} + +/** + * Générer un QR code en base64 à partir d'un token + * @param token Le token unique de la séquence + * @returns Une image QR code en base64 (data URL) + */ +export async function generateQRCodeDataURL(token: string): Promise { + // URL complète pour scanner le QR code + const parametres = await getParametres(); + const url = `${parametres.urlPublique}/emargement/${token}`; + + try { + const qrCodeDataURL = await QRCode.toDataURL(url, { + errorCorrectionLevel: "H", + type: "image/png", + width: 400, + margin: 2, + color: { + dark: "#000000", + light: "#FFFFFF", + }, + }); + + return qrCodeDataURL; + } catch (error) { + console.error("Erreur lors de la génération du QR code:", error); + throw new Error("Impossible de générer le QR code"); + } +} + +/** + * Générer un QR code en buffer PNG + * @param token Le token unique de la séquence + * @returns Un buffer PNG du QR code + */ +export async function generateQRCodeBuffer(token: string): Promise { + const parametres = await getParametres(); + const url = `${parametres.urlPublique}/emargement/${token}`; + + try { + const buffer = await QRCode.toBuffer(url, { + errorCorrectionLevel: "H", + type: "png", + width: 400, + margin: 2, + }); + + return buffer; + } catch (error) { + console.error("Erreur lors de la génération du QR code:", error); + throw new Error("Impossible de générer le QR code"); + } +} diff --git a/deployment-package/source_server/questionnaireDb.ts b/deployment-package/source_server/questionnaireDb.ts new file mode 100644 index 0000000..068a3d6 --- /dev/null +++ b/deployment-package/source_server/questionnaireDb.ts @@ -0,0 +1,250 @@ +import { eq, and, desc, sql } from "drizzle-orm"; +import { getDb } from "./db"; +import { + questionnaires, + questions, + reponsesQuestionnaires, + reponsesQuestions, + envoisQuestionnaires, + type Questionnaire, + type InsertQuestionnaire, + type Question, + type InsertQuestion, + type ReponseQuestionnaire, + type InsertReponseQuestionnaire, + type ReponseQuestion, + type InsertReponseQuestion, + type EnvoiQuestionnaire, + type InsertEnvoiQuestionnaire, +} from "../drizzle/schema"; + +// ========== QUESTIONNAIRES ========== + +export async function getAllQuestionnaires() { + const db = await getDb(); + if (!db) return []; + return await db.select().from(questionnaires).orderBy(desc(questionnaires.createdAt)); +} + +export async function getQuestionnaireById(id: number) { + const db = await getDb(); + if (!db) return null; + const result = await db.select().from(questionnaires).where(eq(questionnaires.id, id)).limit(1); + return result[0] || null; +} + +export async function createQuestionnaire(data: InsertQuestionnaire) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(questionnaires).values(data); + return result[0].insertId; +} + +export async function updateQuestionnaire(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.update(questionnaires).set(data).where(eq(questionnaires.id, id)); +} + +export async function deleteQuestionnaire(id: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + // Supprimer d'abord les questions associées + await db.delete(questions).where(eq(questions.questionnaireId, id)); + // Puis le questionnaire + await db.delete(questionnaires).where(eq(questionnaires.id, id)); +} + +// ========== QUESTIONS ========== + +export async function getQuestionsByQuestionnaireId(questionnaireId: number) { + const db = await getDb(); + if (!db) return []; + return await db + .select() + .from(questions) + .where(eq(questions.questionnaireId, questionnaireId)) + .orderBy(questions.ordre); +} + +export async function createQuestion(data: InsertQuestion) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(questions).values(data); + return result[0].insertId; +} + +export async function updateQuestion(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.update(questions).set(data).where(eq(questions.id, id)); +} + +export async function deleteQuestion(id: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.delete(questions).where(eq(questions.id, id)); +} + +// ========== RÉPONSES QUESTIONNAIRES ========== + +export async function createReponseQuestionnaire(data: InsertReponseQuestionnaire) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(reponsesQuestionnaires).values(data); + return result[0].insertId; +} + +export async function getReponseQuestionnaire(questionnaireId: number, apprenantId: number, sequenceId: number) { + const db = await getDb(); + if (!db) return null; + const result = await db + .select() + .from(reponsesQuestionnaires) + .where( + and( + eq(reponsesQuestionnaires.questionnaireId, questionnaireId), + eq(reponsesQuestionnaires.apprenantId, apprenantId), + eq(reponsesQuestionnaires.sequenceId, sequenceId) + ) + ) + .limit(1); + return result[0] || null; +} + +export async function updateReponseQuestionnaire(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.update(reponsesQuestionnaires).set(data).where(eq(reponsesQuestionnaires.id, id)); +} + +// ========== RÉPONSES QUESTIONS ========== + +export async function createReponseQuestion(data: InsertReponseQuestion) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(reponsesQuestions).values(data); + return result[0].insertId; +} + +export async function getReponsesByReponseQuestionnaireId(reponseQuestionnaireId: number) { + const db = await getDb(); + if (!db) return []; + return await db + .select() + .from(reponsesQuestions) + .where(eq(reponsesQuestions.reponseQuestionnaireId, reponseQuestionnaireId)); +} + +// ========== ENVOIS QUESTIONNAIRES ========== + +export async function createEnvoiQuestionnaire(data: InsertEnvoiQuestionnaire) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(envoisQuestionnaires).values(data); + return result[0].insertId; +} + +export async function getEnvoiByToken(token: string) { + const db = await getDb(); + if (!db) return null; + const result = await db + .select() + .from(envoisQuestionnaires) + .where(eq(envoisQuestionnaires.token, token)) + .limit(1); + return result[0] || null; +} + +export async function updateEnvoiQuestionnaire(id: number, data: Partial) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.update(envoisQuestionnaires).set(data).where(eq(envoisQuestionnaires.id, id)); +} + +export async function checkEnvoiExists(questionnaireId: number, apprenantId: number, sequenceId: number) { + const db = await getDb(); + if (!db) return false; + const result = await db + .select() + .from(envoisQuestionnaires) + .where( + and( + eq(envoisQuestionnaires.questionnaireId, questionnaireId), + eq(envoisQuestionnaires.apprenantId, apprenantId), + eq(envoisQuestionnaires.sequenceId, sequenceId) + ) + ) + .limit(1); + return result.length > 0; +} + +// ========== STATISTIQUES ========== + +/** + * Récupère les statistiques d'un questionnaire + */ +export async function getQuestionnaireStats(questionnaireId: number) { + const db = await getDb(); + if (!db) return null; + + // Nombre total d'envois + const envoisResult = await db + .select({ count: sql`count(*)` }) + .from(envoisQuestionnaires) + .where(eq(envoisQuestionnaires.questionnaireId, questionnaireId)); + const totalEnvois = envoisResult[0]?.count || 0; + + // Nombre de réponses + const reponsesResult = await db + .select({ count: sql`count(*)` }) + .from(reponsesQuestionnaires) + .where( + and( + eq(reponsesQuestionnaires.questionnaireId, questionnaireId), + eq(reponsesQuestionnaires.complete, true) + ) + ); + const totalReponses = reponsesResult[0]?.count || 0; + + // Taux de réponse + const tauxReponse = totalEnvois > 0 ? (totalReponses / totalEnvois) * 100 : 0; + + return { + totalEnvois, + totalReponses, + tauxReponse: Math.round(tauxReponse * 10) / 10, // 1 décimale + }; +} + +/** + * Récupère les réponses détaillées d'un questionnaire pour analyse + */ +export async function getReponsesDetailleesQuestionnaire(questionnaireId: number) { + const db = await getDb(); + if (!db) return []; + + const result = await db + .select({ + reponseQuestionnaireId: reponsesQuestionnaires.id, + apprenantId: reponsesQuestionnaires.apprenantId, + sequenceId: reponsesQuestionnaires.sequenceId, + dateReponse: reponsesQuestionnaires.dateReponse, + questionId: reponsesQuestions.questionId, + reponseNumerique: reponsesQuestions.reponseNumerique, + reponseTexte: reponsesQuestions.reponseTexte, + }) + .from(reponsesQuestionnaires) + .leftJoin( + reponsesQuestions, + eq(reponsesQuestions.reponseQuestionnaireId, reponsesQuestionnaires.id) + ) + .where( + and( + eq(reponsesQuestionnaires.questionnaireId, questionnaireId), + eq(reponsesQuestionnaires.complete, true) + ) + ); + + return result; +} diff --git a/deployment-package/source_server/questionnaireExport.ts b/deployment-package/source_server/questionnaireExport.ts new file mode 100644 index 0000000..5257898 --- /dev/null +++ b/deployment-package/source_server/questionnaireExport.ts @@ -0,0 +1,174 @@ +import ExcelJS from 'exceljs'; +import { jsPDF } from 'jspdf'; +import { getQuestionnaireById, getQuestionsByQuestionnaireId, getQuestionnaireStats, getReponsesDetailleesQuestionnaire } from './questionnaireDb'; +import { getDb } from './db'; +import { sql } from 'drizzle-orm'; + +/** + * Génère un fichier Excel avec les statistiques du questionnaire + */ +export async function exportQuestionnaireToExcel(questionnaireId: number): Promise { + const questionnaire = await getQuestionnaireById(questionnaireId); + const stats = await getQuestionnaireStats(questionnaireId); + const questionsData = await getQuestionsByQuestionnaireId(questionnaireId); + const reponsesData = await getReponsesDetailleesQuestionnaire(questionnaireId); + + if (!questionnaire || !stats) { + throw new Error('Questionnaire introuvable'); + } + + const workbook = new ExcelJS.Workbook(); + + // Feuille 1: Statistiques globales + const statsSheet = workbook.addWorksheet('Statistiques'); + + statsSheet.addRow(['Questionnaire', questionnaire.titre]); + statsSheet.addRow(['Type', questionnaire.type]); + statsSheet.addRow(['']); + statsSheet.addRow(['Nombre d\'envois', stats.totalEnvois]); + statsSheet.addRow(['Nombre de réponses', stats.totalReponses]); + statsSheet.addRow(['Taux de réponse', `${stats.tauxReponse.toFixed(1)}%`]); + statsSheet.addRow(['']); + + // Style pour l'en-tête + statsSheet.getColumn(1).width = 30; + statsSheet.getColumn(2).width = 40; + statsSheet.getRow(1).font = { bold: true, size: 14 }; + + // Feuille 2: Statistiques par question + const questionsSheet = workbook.addWorksheet('Par question'); + + questionsSheet.addRow(['Question', 'Type', 'Ordre']); + questionsSheet.getRow(1).font = { bold: true }; + questionsSheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' } + }; + + questionsData.forEach((q: any) => { + questionsSheet.addRow([ + q.texte, + q.typeQuestion, + q.ordre + ]); + }); + + questionsSheet.getColumn(1).width = 60; + questionsSheet.getColumn(2).width = 20; + questionsSheet.getColumn(3).width = 10; + + // Feuille 3: Réponses brutes + const reponsesSheet = workbook.addWorksheet('Réponses brutes'); + + reponsesSheet.addRow(['Date réponse', 'Apprenant ID', 'Séquence ID', 'Question ID', 'Réponse numérique', 'Réponse texte']); + reponsesSheet.getRow(1).font = { bold: true }; + reponsesSheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' } + }; + + reponsesData.forEach((r: any) => { + reponsesSheet.addRow([ + r.dateReponse ? new Date(r.dateReponse).toLocaleDateString('fr-FR') : '', + r.apprenantId, + r.sequenceId, + r.questionId, + r.reponseNumerique || '', + r.reponseTexte || '' + ]); + }); + + reponsesSheet.getColumn(1).width = 15; + reponsesSheet.getColumn(2).width = 15; + reponsesSheet.getColumn(3).width = 15; + reponsesSheet.getColumn(4).width = 15; + reponsesSheet.getColumn(5).width = 20; + reponsesSheet.getColumn(6).width = 50; + + // Générer le buffer + const buffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(buffer); +} + +/** + * Génère un fichier PDF avec les statistiques du questionnaire + */ +export async function exportQuestionnaireToPDF(questionnaireId: number): Promise { + const questionnaire = await getQuestionnaireById(questionnaireId); + const stats = await getQuestionnaireStats(questionnaireId); + const questionsData = await getQuestionsByQuestionnaireId(questionnaireId); + + if (!questionnaire || !stats) { + throw new Error('Questionnaire introuvable'); + } + + const doc = new jsPDF(); + let yPos = 20; + + // Titre + doc.setFontSize(18); + doc.setFont('helvetica', 'bold'); + doc.text(questionnaire.titre, 20, yPos); + yPos += 10; + + // Type + doc.setFontSize(12); + doc.setFont('helvetica', 'normal'); + doc.text(`Type: ${questionnaire.type}`, 20, yPos); + yPos += 15; + + // Statistiques globales + doc.setFont('helvetica', 'bold'); + doc.text('Statistiques globales', 20, yPos); + yPos += 8; + + doc.setFont('helvetica', 'normal'); + doc.text(`Nombre d'envois: ${stats.totalEnvois}`, 20, yPos); + yPos += 6; + doc.text(`Nombre de réponses: ${stats.totalReponses}`, 20, yPos); + yPos += 6; + doc.text(`Taux de réponse: ${stats.tauxReponse.toFixed(1)}%`, 20, yPos); + yPos += 15; + + // Liste des questions + doc.setFont('helvetica', 'bold'); + doc.text('Questions du questionnaire', 20, yPos); + yPos += 10; + + questionsData.forEach((q: any, index: number) => { + // Vérifier si on doit ajouter une nouvelle page + if (yPos > 250) { + doc.addPage(); + yPos = 20; + } + + doc.setFont('helvetica', 'bold'); + doc.setFontSize(11); + const questionText = `${index + 1}. ${q.texte}`; + const lines = doc.splitTextToSize(questionText, 170); + doc.text(lines, 20, yPos); + yPos += lines.length * 6; + + doc.setFont('helvetica', 'normal'); + doc.setFontSize(10); + doc.text(`Type: ${q.typeQuestion}`, 25, yPos); + yPos += 8; + }); + + // Pied de page + const pageCount = doc.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(8); + doc.setFont('helvetica', 'normal'); + doc.text( + `Page ${i} sur ${pageCount} - Généré le ${new Date().toLocaleDateString('fr-FR')}`, + 20, + 290 + ); + } + + return Buffer.from(doc.output('arraybuffer')); +} diff --git a/deployment-package/source_server/questionnaireScheduler.ts b/deployment-package/source_server/questionnaireScheduler.ts new file mode 100644 index 0000000..f476d6f --- /dev/null +++ b/deployment-package/source_server/questionnaireScheduler.ts @@ -0,0 +1,248 @@ +import { getDb } from "./db"; +import { eq, and, lte, sql } from "drizzle-orm"; +import { + questionnaires, + sequences, + inscriptions, + apprenants, + datesFormation, + envoisQuestionnaires, +} from "../drizzle/schema"; +import { + getAllQuestionnaires, + checkEnvoiExists, + createEnvoiQuestionnaire, +} from "./questionnaireDb"; +import { sendEmail } from "./_core/emailSender"; +import crypto from "crypto"; + +/** + * Génère un token unique pour accéder au questionnaire + */ +function generateToken(): string { + return crypto.randomBytes(32).toString("hex"); +} + +/** + * Envoie un questionnaire à un apprenant pour une séquence donnée + */ +async function envoyerQuestionnaire( + questionnaireId: number, + apprenantId: number, + sequenceId: number, + apprenantEmail: string, + apprenantNom: string, + apprenantPrenom: string, + questionnairenom: string, + sequenceNom: string +) { + // Vérifier si le questionnaire n'a pas déjà été envoyé + const dejaEnvoye = await checkEnvoiExists(questionnaireId, apprenantId, sequenceId); + if (dejaEnvoye) { + console.log( + `[Questionnaires] Questionnaire ${questionnaireId} déjà envoyé à l'apprenant ${apprenantId} pour la séquence ${sequenceId}` + ); + return false; + } + + // Générer un token unique + const token = generateToken(); + + // Créer l'envoi + await createEnvoiQuestionnaire({ + questionnaireId, + apprenantId, + sequenceId, + token, + dateEnvoi: new Date(), + dateReponse: null, + }); + + // Envoyer l'email + const lienQuestionnaire = `${process.env.VITE_FRONTEND_URL || "http://localhost:3000"}/questionnaire/${token}`; + + const emailSent = await sendEmail({ + to: apprenantEmail, + subject: `Questionnaire : ${questionnairenom}`, + html: ` +

    Bonjour ${apprenantPrenom} ${apprenantNom},

    + +

    Nous vous remercions d'avoir participé à la formation ${sequenceNom}.

    + +

    Afin d'améliorer continuellement la qualité de nos formations, nous vous invitons à répondre à ce questionnaire :

    + +

    + + Répondre au questionnaire + +

    + +

    Ce questionnaire ne vous prendra que quelques minutes.

    + +

    Merci pour votre participation !

    + +

    + Si le bouton ne fonctionne pas, copiez ce lien dans votre navigateur :
    + ${lienQuestionnaire} +

    + `, + }); + + if (emailSent) { + console.log( + `[Questionnaires] Questionnaire ${questionnaireId} envoyé à ${apprenantEmail} pour la séquence ${sequenceId}` + ); + return true; + } else { + console.error( + `[Questionnaires] Échec de l'envoi du questionnaire ${questionnaireId} à ${apprenantEmail}` + ); + return false; + } +} + +/** + * Traite les envois automatiques de questionnaires post-formation + */ +export async function processEnvoisAutomatiques() { + console.log("[Questionnaires] Démarrage du traitement des envois automatiques..."); + + const db = await getDb(); + if (!db) { + console.log("[Questionnaires] Base de données non disponible"); + return { success: false, message: "Base de données non disponible" }; + } + + try { + // Récupérer tous les questionnaires actifs avec envoi automatique + const questionnairesActifs = await getAllQuestionnaires(); + const questionnairesAEnvoyer = questionnairesActifs.filter( + (q) => q.actif && q.envoiAutomatique + ); + + if (questionnairesAEnvoyer.length === 0) { + console.log("[Questionnaires] Aucun questionnaire avec envoi automatique configuré"); + return { success: true, message: "Aucun questionnaire à envoyer", count: 0 }; + } + + let totalEnvoyes = 0; + + for (const questionnaire of questionnairesAEnvoyer) { + console.log( + `[Questionnaires] Traitement du questionnaire "${questionnaire.titre}" (délai: J+${questionnaire.delaiEnvoiJours})` + ); + + // Calculer la date cible (aujourd'hui - délai) + const dateCible = new Date(); + dateCible.setDate(dateCible.getDate() - questionnaire.delaiEnvoiJours); + dateCible.setHours(0, 0, 0, 0); + + // Récupérer les séquences terminées à la date cible + const sequencesTerminees = await db + .select({ + sequenceId: sequences.id, + sequenceNom: sequences.nom, + formationId: sequences.formationId, + }) + .from(sequences) + .innerJoin(datesFormation, eq(datesFormation.sequenceId, sequences.id)) + .where(lte(datesFormation.dateFin, dateCible)) + .groupBy(sequences.id); + + console.log( + `[Questionnaires] ${sequencesTerminees.length} séquence(s) terminée(s) trouvée(s) pour le ${dateCible.toLocaleDateString("fr-FR")}` + ); + + // Pour chaque séquence terminée + for (const seq of sequencesTerminees) { + // Vérifier si le questionnaire est lié à une formation spécifique + if (questionnaire.formationId && questionnaire.formationId !== seq.formationId) { + continue; + } + + // Récupérer les apprenants inscrits et validés + const apprenantsInscrits = await db + .select({ + apprenantId: apprenants.id, + apprenantEmail: apprenants.email, + apprenantNom: apprenants.nom, + apprenantPrenom: apprenants.prenom, + }) + .from(inscriptions) + .innerJoin(apprenants, eq(apprenants.id, inscriptions.apprenantId)) + .where( + and( + eq(inscriptions.sequenceId, seq.sequenceId), + eq(inscriptions.statut, "confirmee") + ) + ); + + console.log( + `[Questionnaires] ${apprenantsInscrits.length} apprenant(s) inscrit(s) à la séquence ${seq.sequenceId}` + ); + + // Envoyer le questionnaire à chaque apprenant + for (const apprenant of apprenantsInscrits) { + if (!apprenant.apprenantEmail) { + console.log( + `[Questionnaires] Apprenant ${apprenant.apprenantId} sans email, envoi ignoré` + ); + continue; + } + + const envoye = await envoyerQuestionnaire( + questionnaire.id, + apprenant.apprenantId, + seq.sequenceId, + apprenant.apprenantEmail, + apprenant.apprenantNom, + apprenant.apprenantPrenom, + questionnaire.titre, + seq.sequenceNom + ); + + if (envoye) { + totalEnvoyes++; + } + } + } + } + + console.log(`[Questionnaires] Traitement terminé - ${totalEnvoyes} questionnaire(s) envoyé(s)`); + return { + success: true, + message: `${totalEnvoyes} questionnaire(s) envoyé(s)`, + count: totalEnvoyes, + }; + } catch (error: any) { + console.error("[Questionnaires] Erreur lors du traitement :", error); + return { success: false, message: error.message, count: 0 }; + } +} + +/** + * Initialise le scheduler pour les envois automatiques de questionnaires + * Exécute le traitement tous les jours à 9h00 + */ +export function initQuestionnaireScheduler() { + console.log("[Questionnaires] Initialisation du scheduler d'envoi automatique"); + + // Exécuter immédiatement au démarrage + processEnvoisAutomatiques(); + + // Puis exécuter tous les jours à 9h00 + const interval = setInterval( + () => { + const now = new Date(); + if (now.getHours() === 9 && now.getMinutes() === 0) { + processEnvoisAutomatiques(); + } + }, + 60 * 1000 // Vérifier toutes les minutes + ); + + console.log("[Questionnaires] Scheduler initialisé - vérification quotidienne à 9h00"); + + return interval; +} diff --git a/deployment-package/source_server/questionnaireSuiviDb.ts b/deployment-package/source_server/questionnaireSuiviDb.ts new file mode 100644 index 0000000..8dcd1ba --- /dev/null +++ b/deployment-package/source_server/questionnaireSuiviDb.ts @@ -0,0 +1,164 @@ +import { eq, sql, and, gte, lte } from "drizzle-orm"; +import { getDb } from "./db"; +import { + questionnaires, + envoisQuestionnaires, + reponsesQuestionnaires, + reponsesQuestions, + questions, + sequences, + formations, + formateurs +} from "../drizzle/schema"; + +/** + * Statistiques de taux de réponse par formation + */ +export async function getStatsByFormation() { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const results = await db + .select({ + formationId: formations.id, + formationNom: formations.nom, + totalEnvois: sql`COUNT(DISTINCT ${envoisQuestionnaires.id})`, + totalReponses: sql`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 END)`, + }) + .from(envoisQuestionnaires) + .innerJoin(sequences, eq(envoisQuestionnaires.sequenceId, sequences.id)) + .innerJoin(formations, eq(sequences.formationId, formations.id)) + .groupBy(formations.id, formations.nom); + + return results.map(r => ({ + formationId: r.formationId, + formationNom: r.formationNom, + totalEnvois: Number(r.totalEnvois), + totalReponses: Number(r.totalReponses), + tauxReponse: Number(r.totalEnvois) > 0 + ? Math.round((Number(r.totalReponses) / Number(r.totalEnvois)) * 100) + : 0 + })); +} + +/** + * Évolution temporelle des réponses (par mois) + */ +export async function getEvolutionTemporelle(startDate?: Date, endDate?: Date) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const conditions = []; + if (startDate) { + conditions.push(gte(envoisQuestionnaires.dateEnvoi, startDate)); + } + if (endDate) { + conditions.push(lte(envoisQuestionnaires.dateEnvoi, endDate)); + } + + const results = await db + .select({ + mois: sql`DATE_FORMAT(${envoisQuestionnaires.dateEnvoi}, '%Y-%m')`, + totalEnvois: sql`COUNT(DISTINCT ${envoisQuestionnaires.id})`, + totalReponses: sql`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 END)`, + }) + .from(envoisQuestionnaires) + .where(conditions.length > 0 ? and(...conditions) : undefined) + .groupBy(sql`DATE_FORMAT(${envoisQuestionnaires.dateEnvoi}, '%Y-%m')`) + .orderBy(sql`DATE_FORMAT(${envoisQuestionnaires.dateEnvoi}, '%Y-%m')`); + + return results.map(r => ({ + mois: r.mois, + totalEnvois: Number(r.totalEnvois), + totalReponses: Number(r.totalReponses), + tauxReponse: Number(r.totalEnvois) > 0 + ? Math.round((Number(r.totalReponses) / Number(r.totalEnvois)) * 100) + : 0 + })); +} + +/** + * Comparaison par formateur + */ +export async function getStatsByFormateur() { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Première requête : statistiques d'envois et réponses par formateur + const statsEnvois = await db + .select({ + formateurId: formateurs.id, + formateurNom: formateurs.nom, + totalEnvois: sql`COUNT(DISTINCT ${envoisQuestionnaires.id})`, + totalReponses: sql`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 END)`, + }) + .from(envoisQuestionnaires) + .innerJoin(sequences, eq(envoisQuestionnaires.sequenceId, sequences.id)) + .innerJoin(formateurs, eq(sequences.formateurId, formateurs.id)) + .groupBy(formateurs.id, formateurs.nom); + + // Pour chaque formateur, calculer la moyenne de satisfaction + const results = await Promise.all(statsEnvois.map(async (stat) => { + // Calculer la moyenne des réponses de type échelle pour ce formateur + const satisfactionResult = await db + .select({ + moyenne: sql`AVG(CAST(${reponsesQuestions.reponseNumerique} AS DECIMAL(10,2)))`, + }) + .from(reponsesQuestionnaires) + .innerJoin(sequences, eq(reponsesQuestionnaires.sequenceId, sequences.id)) + .innerJoin(reponsesQuestions, eq(reponsesQuestionnaires.id, reponsesQuestions.reponseQuestionnaireId)) + .innerJoin(questions, eq(reponsesQuestions.questionId, questions.id)) + .where( + and( + eq(sequences.formateurId, stat.formateurId), + eq(reponsesQuestionnaires.complete, true), + eq(questions.typeQuestion, 'echelle') + ) + ); + + const moyenneSatisfaction = satisfactionResult[0]?.moyenne + ? Number(satisfactionResult[0].moyenne).toFixed(1) + : null; + + return { + formateurId: stat.formateurId, + formateurNom: stat.formateurNom || "Non spécifié", + totalEnvois: Number(stat.totalEnvois), + totalReponses: Number(stat.totalReponses), + tauxReponse: Number(stat.totalEnvois) > 0 + ? Math.round((Number(stat.totalReponses) / Number(stat.totalEnvois)) * 100) + : 0, + moyenneSatisfaction + }; + })); + + return results; +} + +/** + * Statistiques globales pour le tableau de bord + */ +export async function getStatsGlobales() { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const results = await db + .select({ + totalQuestionnaires: sql`COUNT(DISTINCT ${questionnaires.id})`, + totalEnvois: sql`COUNT(DISTINCT ${envoisQuestionnaires.id})`, + totalReponses: sql`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 END)`, + }) + .from(questionnaires) + .leftJoin(envoisQuestionnaires, eq(questionnaires.id, envoisQuestionnaires.questionnaireId)); + + const stats = results[0]; + const totalEnvois = Number(stats.totalEnvois) || 0; + const totalReponses = Number(stats.totalReponses) || 0; + + return { + totalQuestionnaires: Number(stats.totalQuestionnaires) || 0, + totalEnvois, + totalReponses, + tauxReponseGlobal: totalEnvois > 0 ? Number(((totalReponses / totalEnvois) * 100).toFixed(1)) : 0 + }; +} diff --git a/deployment-package/source_server/rappelDb.ts b/deployment-package/source_server/rappelDb.ts new file mode 100644 index 0000000..1f7da15 --- /dev/null +++ b/deployment-package/source_server/rappelDb.ts @@ -0,0 +1,328 @@ +import { getDb } from "./db"; +import { logsRappels, InsertLogRappel, LogRappel } from "../drizzle/schema"; +import { eq, and } from "drizzle-orm"; + +/** + * Enregistre l'envoi d'un rappel dans les logs + */ +export async function logRappelEnvoye(data: { + rappelId: number; + sequenceId: number; + apprenantId: number; + emailDestinataire: string; + typeRappel: string; + statut: "success" | "failed"; + messageErreur?: string; + dateSequence: Date; +}): Promise { + const db = await getDb(); + if (!db) { + console.warn("[LogsRappels] Base de données non disponible"); + return; + } + + try { + await db.insert(logsRappels).values({ + rappelId: data.rappelId, + sequenceId: data.sequenceId, + apprenantId: data.apprenantId, + emailDestinataire: data.emailDestinataire, + typeRappel: data.typeRappel, + dateEnvoi: new Date(), + statut: data.statut, + messageErreur: data.messageErreur || null, + dateSequence: data.dateSequence, + } as InsertLogRappel); + } catch (error) { + console.error("[LogsRappels] Erreur lors de l'enregistrement du log:", error); + } +} + +/** + * Vérifie si un rappel a déjà été envoyé à un apprenant pour une séquence donnée + */ +export async function rappelDejaEnvoye( + rappelId: number, + sequenceId: number, + apprenantId: number +): Promise { + const db = await getDb(); + if (!db) { + console.warn("[LogsRappels] Base de données non disponible"); + return false; + } + + try { + const logs = await db + .select() + .from(logsRappels) + .where( + and( + eq(logsRappels.rappelId, rappelId), + eq(logsRappels.sequenceId, sequenceId), + eq(logsRappels.apprenantId, apprenantId), + eq(logsRappels.statut, "success") + ) + ) + .limit(1); + + return logs.length > 0; + } catch (error) { + console.error("[LogsRappels] Erreur lors de la vérification des doublons:", error); + return false; + } +} + +/** + * Récupère tous les logs de rappels pour une séquence donnée + */ +export async function getLogsRappelsBySequence(sequenceId: number): Promise { + const db = await getDb(); + if (!db) return []; + + try { + return await db + .select() + .from(logsRappels) + .where(eq(logsRappels.sequenceId, sequenceId)) + .orderBy(logsRappels.dateEnvoi); + } catch (error) { + console.error("[LogsRappels] Erreur lors de la récupération des logs:", error); + return []; + } +} + +/** + * Récupère tous les logs de rappels échoués récents (dernières 24h) + */ +export async function getLogsRappelsEchoues(): Promise { + const db = await getDb(); + if (!db) return []; + + try { + const hier = new Date(); + hier.setDate(hier.getDate() - 1); + + return await db + .select() + .from(logsRappels) + .where( + and( + eq(logsRappels.statut, "failed"), + // Note: Drizzle ne supporte pas directement les comparaisons de dates + // On récupère tous les échecs et on filtre en JS + ) + ) + .orderBy(logsRappels.dateEnvoi); + } catch (error) { + console.error("[LogsRappels] Erreur lors de la récupération des échecs:", error); + return []; + } +} + +/** + * Compte le nombre de rappels envoyés avec succès pour une séquence + */ +export async function countRappelsEnvoyesSequence(sequenceId: number): Promise { + const db = await getDb(); + if (!db) return 0; + + try { + const logs = await db + .select() + .from(logsRappels) + .where( + and( + eq(logsRappels.sequenceId, sequenceId), + eq(logsRappels.statut, "success") + ) + ); + + return logs.length; + } catch (error) { + console.error("[LogsRappels] Erreur lors du comptage:", error); + return 0; + } +} + +/** + * Récupère les logs de rappels avec filtres + */ +export async function getLogsRappelsWithFilters(filters: { + dateDebut?: Date; + dateFin?: Date; + sequenceId?: number; + statut?: "success" | "failed"; + email?: string; + limit?: number; + offset?: number; +}): Promise { + const db = await getDb(); + if (!db) return []; + + try { + let query = db.select().from(logsRappels); + + const conditions = []; + if (filters.sequenceId) { + conditions.push(eq(logsRappels.sequenceId, filters.sequenceId)); + } + if (filters.statut) { + conditions.push(eq(logsRappels.statut, filters.statut)); + } + if (filters.email) { + // Note: Drizzle ne supporte pas LIKE directement, on filtre en JS + } + + if (conditions.length > 0) { + query = query.where(and(...conditions)) as any; + } + + let logs = await query.orderBy(logsRappels.dateEnvoi).limit(filters.limit || 100).offset(filters.offset || 0); + + // Filtrer par email si nécessaire + if (filters.email) { + logs = logs.filter(log => log.emailDestinataire.toLowerCase().includes(filters.email!.toLowerCase())); + } + + // Filtrer par date si nécessaire + if (filters.dateDebut) { + logs = logs.filter(log => new Date(log.dateEnvoi) >= filters.dateDebut!); + } + if (filters.dateFin) { + logs = logs.filter(log => new Date(log.dateEnvoi) <= filters.dateFin!); + } + + return logs; + } catch (error) { + console.error("[LogsRappels] Erreur lors de la récupération avec filtres:", error); + return []; + } +} + +/** + * Calcule les statistiques des rappels + */ +export async function getStatsRappels(): Promise<{ + totalEnvoyes: number; + totalSucces: number; + totalEchecs: number; + tauxSucces: number; + emailsProblematiques: Array<{ email: string; nbEchecs: number }>; +}> { + const db = await getDb(); + if (!db) { + return { + totalEnvoyes: 0, + totalSucces: 0, + totalEchecs: 0, + tauxSucces: 0, + emailsProblematiques: [], + }; + } + + try { + const logs = await db.select().from(logsRappels); + + const totalEnvoyes = logs.length; + const totalSucces = logs.filter(l => l.statut === "success").length; + const totalEchecs = logs.filter(l => l.statut === "failed").length; + const tauxSucces = totalEnvoyes > 0 ? (totalSucces / totalEnvoyes) * 100 : 0; + + // Compter les échecs par email + const echecsParEmail = new Map(); + logs.filter(l => l.statut === "failed").forEach(log => { + const count = echecsParEmail.get(log.emailDestinataire) || 0; + echecsParEmail.set(log.emailDestinataire, count + 1); + }); + + const emailsProblematiques = Array.from(echecsParEmail.entries()) + .map(([email, nbEchecs]) => ({ email, nbEchecs })) + .sort((a, b) => b.nbEchecs - a.nbEchecs) + .slice(0, 10); // Top 10 + + return { + totalEnvoyes, + totalSucces, + totalEchecs, + tauxSucces, + emailsProblematiques, + }; + } catch (error) { + console.error("[LogsRappels] Erreur lors du calcul des statistiques:", error); + return { + totalEnvoyes: 0, + totalSucces: 0, + totalEchecs: 0, + tauxSucces: 0, + emailsProblematiques: [], + }; + } +} + +/** + * Récupère l'évolution des envois par jour + */ +export async function getEvolutionEnvois(): Promise> { + const db = await getDb(); + if (!db) return []; + + try { + const logs = await db.select().from(logsRappels).orderBy(logsRappels.dateEnvoi); + + // Grouper par jour + const parJour = new Map(); + + logs.forEach(log => { + const dateStr = new Date(log.dateEnvoi).toISOString().split('T')[0]; + const stats = parJour.get(dateStr) || { nbEnvoyes: 0, nbSucces: 0, nbEchecs: 0 }; + stats.nbEnvoyes++; + if (log.statut === "success") stats.nbSucces++; + else stats.nbEchecs++; + parJour.set(dateStr, stats); + }); + + return Array.from(parJour.entries()) + .map(([date, stats]) => ({ date, ...stats })) + .sort((a, b) => a.date.localeCompare(b.date)); + } catch (error) { + console.error("[LogsRappels] Erreur lors de la récupération de l'évolution:", error); + return []; + } +} + +/** + * Récupère les logs échoués à réessayer + */ +export async function getLogsAReessayer(): Promise { + const db = await getDb(); + if (!db) return []; + + try { + const maintenant = new Date(); + const logs = await db + .select() + .from(logsRappels) + .where( + and( + eq(logsRappels.statut, "failed"), + // Limiter à 3 tentatives maximum + ) + ); + + // Filtrer en JS pour les conditions complexes + return logs.filter(log => { + if (log.nbTentatives >= 3) return false; + if (!log.prochainEssai) return true; // Premier essai + return new Date(log.prochainEssai) <= maintenant; + }); + } catch (error) { + console.error("[LogsRappels] Erreur lors de la récupération des logs à réessayer:", error); + return []; + } +} diff --git a/deployment-package/source_server/rappelRetry.ts b/deployment-package/source_server/rappelRetry.ts new file mode 100644 index 0000000..63cd7da --- /dev/null +++ b/deployment-package/source_server/rappelRetry.ts @@ -0,0 +1,226 @@ +import { getDb } from "./db"; +import { logsRappels, sequences, inscriptions, apprenants, formations, datesFormation, rappels, formateurs } from "../drizzle/schema"; +import { eq, and } from "drizzle-orm"; +import { sendRappelJ7Email, sendRappelJ1Email } from "./emailService"; +import { getLogsAReessayer } from "./rappelDb"; +import { notifyOwner } from "./_core/notification"; + +/** + * Calcule le délai avant le prochain essai selon le nombre de tentatives + * Délai exponentiel : 1h, 4h, 24h + */ +function calculerProchainEssai(nbTentatives: number): Date { + const maintenant = new Date(); + let delaiHeures = 1; // 1 heure par défaut + + if (nbTentatives === 1) { + delaiHeures = 1; // 1ère tentative échouée → réessayer dans 1h + } else if (nbTentatives === 2) { + delaiHeures = 4; // 2ème tentative échouée → réessayer dans 4h + } else { + delaiHeures = 24; // 3ème tentative échouée → réessayer dans 24h (mais on arrête à 3) + } + + maintenant.setHours(maintenant.getHours() + delaiHeures); + return maintenant; +} + +/** + * Réessaye d'envoyer les rappels échoués + */ +export async function processRappelRetry() { + console.log("[Rappels Retry] Démarrage du processus de réessai"); + + const db = await getDb(); + if (!db) { + console.error("[Rappels Retry] Base de données non disponible"); + return; + } + + try { + // Récupérer les logs à réessayer + const logsAReessayer = await getLogsAReessayer(); + + if (logsAReessayer.length === 0) { + console.log("[Rappels Retry] Aucun rappel à réessayer"); + return; + } + + console.log(`[Rappels Retry] ${logsAReessayer.length} rappel(s) à réessayer`); + + let nbSucces = 0; + let nbEchecs = 0; + + for (const log of logsAReessayer) { + try { + // Récupérer les informations de la séquence + const sequence = await db + .select() + .from(sequences) + .where(eq(sequences.id, log.sequenceId)) + .limit(1); + + if (sequence.length === 0) { + console.warn(`[Rappels Retry] Séquence ${log.sequenceId} introuvable`); + continue; + } + + const seq = sequence[0]; + + // Récupérer la formation + const formation = await db + .select() + .from(formations) + .where(eq(formations.id, seq.formationId)) + .limit(1); + + if (formation.length === 0) { + console.warn(`[Rappels Retry] Formation ${seq.formationId} introuvable`); + continue; + } + + // Récupérer l'apprenant + const apprenant = await db + .select() + .from(apprenants) + .where(eq(apprenants.id, log.apprenantId)) + .limit(1); + + if (apprenant.length === 0 || !apprenant[0].email) { + console.warn(`[Rappels Retry] Apprenant ${log.apprenantId} introuvable ou sans email`); + continue; + } + + // Récupérer les dates de la séquence + const dates = await db + .select() + .from(datesFormation) + .where(eq(datesFormation.sequenceId, seq.id)) + .orderBy(datesFormation.ordre); + + // Récupérer le formateur si disponible + let formateurNom = ""; + if (seq.formateurId) { + const formateur = await db + .select() + .from(formateurs) + .where(eq(formateurs.id, seq.formateurId)) + .limit(1); + if (formateur.length > 0) { + formateurNom = formateur[0].nom; + } + } + + // Réessayer l'envoi + try { + if (log.typeRappel === "rappel") { + await sendRappelJ7Email({ + apprenantEmail: apprenant[0].email, + apprenantPrenom: apprenant[0].prenom, + apprenantNom: apprenant[0].nom, + apprenantFonction: apprenant[0].fonction, + formationNom: formation[0].nom, + sequenceNom: seq.nom, + dates: dates.map(d => ({ + dateDebut: d.dateDebut, + dateFin: d.dateFin, + ordre: d.ordre + })), + lieu: seq.lieu || "", + }); + } else if (log.typeRappel === "rappelJ1") { + await sendRappelJ1Email({ + apprenantEmail: apprenant[0].email, + apprenantPrenom: apprenant[0].prenom, + apprenantNom: apprenant[0].nom, + apprenantFonction: apprenant[0].fonction, + formationNom: formation[0].nom, + sequenceNom: seq.nom, + dates: dates.map(d => ({ + dateDebut: d.dateDebut, + dateFin: d.dateFin, + ordre: d.ordre + })), + lieu: seq.lieu || "", + }); + } + + // Succès : mettre à jour le log + await db + .update(logsRappels) + .set({ + statut: "success", + nbTentatives: log.nbTentatives + 1, + prochainEssai: null, + }) + .where(eq(logsRappels.id, log.id)); + + console.log(`[Rappels Retry] Succès pour ${apprenant[0].email} après ${log.nbTentatives + 1} tentative(s)`); + nbSucces++; + } catch (emailError: any) { + const messageErreur = emailError?.message || String(emailError); + const nouvelleTentative = log.nbTentatives + 1; + + if (nouvelleTentative >= 3) { + // Abandon après 3 tentatives + await db + .update(logsRappels) + .set({ + statut: "failed", + nbTentatives: nouvelleTentative, + messageErreur: `Abandon après 3 tentatives: ${messageErreur}`, + prochainEssai: null, + }) + .where(eq(logsRappels.id, log.id)); + + console.error(`[Rappels Retry] Abandon pour ${apprenant[0].email} après 3 tentatives`); + + // Notifier l'admin + await notifyOwner({ + title: `❌ Abandon d'envoi de rappel`, + content: `Le rappel pour ${apprenant[0].email} (${seq.nom}) a échoué 3 fois et a été abandonné.\\n\\nDernière erreur: ${messageErreur}`, + }); + } else { + // Planifier un nouvel essai + const prochainEssai = calculerProchainEssai(nouvelleTentative); + await db + .update(logsRappels) + .set({ + nbTentatives: nouvelleTentative, + messageErreur: messageErreur, + prochainEssai: prochainEssai, + }) + .where(eq(logsRappels.id, log.id)); + + console.log(`[Rappels Retry] Échec pour ${apprenant[0].email}, tentative ${nouvelleTentative}/3. Prochain essai: ${prochainEssai.toLocaleString()}`); + } + nbEchecs++; + } + } catch (error) { + console.error(`[Rappels Retry] Erreur lors du traitement du log ${log.id}:`, error); + } + } + + console.log(`[Rappels Retry] Traitement terminé: ${nbSucces} succès, ${nbEchecs} échecs`); + } catch (error) { + console.error("[Rappels Retry] Erreur lors du processus de retry:", error); + } +} + +/** + * Initialise le scheduler de retry + * Vérifie toutes les heures s'il y a des rappels à réessayer + */ +export function initRappelRetryScheduler() { + console.log("[Rappels Retry] Initialisation du scheduler de réessai automatique"); + + // Exécuter immédiatement au démarrage + processRappelRetry(); + + // Puis exécuter toutes les heures + setInterval(() => { + processRappelRetry(); + }, 60 * 60 * 1000); // 1 heure en millisecondes + + console.log("[Rappels Retry] Scheduler de réessai initialisé - vérification toutes les heures"); +} diff --git a/deployment-package/source_server/rappelScheduler.ts b/deployment-package/source_server/rappelScheduler.ts new file mode 100644 index 0000000..e1a1869 --- /dev/null +++ b/deployment-package/source_server/rappelScheduler.ts @@ -0,0 +1,294 @@ +import { getDb } from "./db"; +import { rappels, sequences, inscriptions, apprenants, formations, datesFormation, formateurs } from "../drizzle/schema"; +import { eq, and, gte, lte, sql, inArray } from "drizzle-orm"; +import { sendRappelJ7Email, sendRappelJ1Email } from "./emailService"; +import { logRappelEnvoye, rappelDejaEnvoye } from "./rappelDb"; +import { notifyOwner } from "./_core/notification"; +import { processRemerciementsAutomatiques } from "./remerciementScheduler"; + +/** + * Job automatique qui vérifie quotidiennement les séquences à venir + * et envoie les rappels J-7 et J-1 aux apprenants inscrits + */ +export async function processRappelsAutomatiques() { + console.log("[Rappels] Démarrage du traitement des rappels automatiques..."); + + const db = await getDb(); + if (!db) { + console.error("[Rappels] Base de données non disponible"); + return; + } + + try { + // Récupérer tous les rappels actifs + const rappelsActifs = await db + .select() + .from(rappels) + .where(eq(rappels.actif, true)); + + console.log(`[Rappels] ${rappelsActifs.length} rappel(s) actif(s) trouvé(s)`); + + for (const rappel of rappelsActifs) { + await processRappel(rappel); + } + + console.log("[Rappels] Traitement terminé"); + } catch (error) { + console.error("[Rappels] Erreur lors du traitement des rappels:", error); + } +} + +async function processRappel(rappel: any) { + console.log(`[Rappels] Traitement du rappel: ${rappel.nom} (${rappel.timing === 'pre_formation' ? 'J-' : 'J+'}${rappel.joursAvant})`); + + const db = await getDb(); + if (!db) return; + + try { + // Calculer la date cible selon le timing (pré ou post-formation) + const dateCible = new Date(); + if (rappel.timing === 'pre_formation') { + dateCible.setDate(dateCible.getDate() + rappel.joursAvant); + } else { + dateCible.setDate(dateCible.getDate() - rappel.joursAvant); + } + dateCible.setHours(0, 0, 0, 0); + + const dateCibleFin = new Date(dateCible); + dateCibleFin.setHours(23, 59, 59, 999); + + console.log(`[Rappels] Recherche des dates de formation le ${dateCible.toLocaleDateString()}`); + + // Vérifier si ce rappel est associé à des dates spécifiques + const datesAssociees = rappel.dateFormationIds || []; + const filtreDates = datesAssociees.length > 0; + + console.log(`[Rappels] ${filtreDates ? `Filtré sur ${datesAssociees.length} date(s) spécifique(s)` : 'Toutes les dates'}`); + + // Récupérer les dates de formation concernées + let datesConcernees; + if (filtreDates) { + // Seulement les dates associées à ce rappel + datesConcernees = await db + .select({ + dateFormation: datesFormation, + sequence: sequences, + formation: formations, + formateur: formateurs, + }) + .from(datesFormation) + .leftJoin(sequences, eq(datesFormation.sequenceId, sequences.id)) + .leftJoin(formations, eq(sequences.formationId, formations.id)) + .leftJoin(formateurs, eq(sequences.formateurId, formateurs.id)) + .where( + and( + inArray(datesFormation.id, datesAssociees), + gte(datesFormation.dateDebut, dateCible), + lte(datesFormation.dateDebut, dateCibleFin) + ) + ); + } else { + // Toutes les dates correspondant à la date cible + datesConcernees = await db + .select({ + dateFormation: datesFormation, + sequence: sequences, + formation: formations, + formateur: formateurs, + }) + .from(datesFormation) + .leftJoin(sequences, eq(datesFormation.sequenceId, sequences.id)) + .leftJoin(formations, eq(sequences.formationId, formations.id)) + .leftJoin(formateurs, eq(sequences.formateurId, formateurs.id)) + .where( + and( + gte(datesFormation.dateDebut, dateCible), + lte(datesFormation.dateDebut, dateCibleFin) + ) + ); + } + + console.log(`[Rappels] ${datesConcernees.length} date(s) de formation trouvée(s)`); + + // Grouper les dates par séquence pour envoyer un seul email par apprenant + const sequencesMap = new Map(); + + for (const { dateFormation, sequence, formation, formateur } of datesConcernees) { + if (!dateFormation || !sequence || !formation) continue; + + if (!sequencesMap.has(sequence.id)) { + sequencesMap.set(sequence.id, { + sequence, + formation, + formateur, + dates: [], + }); + } + sequencesMap.get(sequence.id)!.dates.push(dateFormation); + } + + console.log(`[Rappels] ${sequencesMap.size} séquence(s) concernée(s)`); + + for (const { sequence, formation, formateur, dates: datesRappel } of Array.from(sequencesMap.values())) { + if (!sequence || !formation) continue; + + // Récupérer toutes les dates de cette séquence pour l'email + const toutesLesDates = await db + .select() + .from(datesFormation) + .where(eq(datesFormation.sequenceId, sequence.id)) + .orderBy(datesFormation.ordre); + + const formateurNom = formateur?.nom || ""; + + // Récupérer tous les apprenants inscrits à cette séquence + const inscrits = await db + .select({ + inscription: inscriptions, + apprenant: apprenants, + }) + .from(inscriptions) + .leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .where( + and( + eq(inscriptions.sequenceId, sequence.id), + eq(inscriptions.statut, "confirmee") + ) + ); + + console.log(`[Rappels] ${inscrits.length} apprenant(s) inscrit(s) à la séquence ${sequence.nom}`); + + // Envoyer le rappel à chaque apprenant + let nbEnvoyes = 0; + let nbEchecs = 0; + const echecs: Array<{ email: string; erreur: string }> = []; + + for (const { apprenant } of inscrits) { + if (!apprenant || !apprenant.email) continue; + + // Vérifier si le rappel a déjà été envoyé + const dejaEnvoye = await rappelDejaEnvoye(rappel.id, sequence.id, apprenant.id); + if (dejaEnvoye) { + console.log(`[Rappels] Rappel déjà envoyé à ${apprenant.email} pour la séquence ${sequence.nom} - ignoré`); + continue; + } + + try { + // Choisir la fonction d'envoi selon le type de rappel + if (rappel.templateType === "rappel") { + // Rappel J-7 + await sendRappelJ7Email({ + apprenantEmail: apprenant.email, + apprenantPrenom: apprenant.prenom, + apprenantNom: apprenant.nom, + apprenantFonction: apprenant.fonction, + formationNom: formation.nom, + sequenceNom: sequence.nom, + dates: toutesLesDates.map(d => ({ + dateDebut: d.dateDebut, + dateFin: d.dateFin, + ordre: d.ordre + })), + lieu: sequence.lieu || "", + formateur: formateurNom, + attachmentUrl: rappel.urlFichier || undefined, + attachmentFilename: rappel.nomFichier || undefined, + attachmentMimeType: rappel.typeFichier || undefined, + }); + console.log(`[Rappels] Email J-7 envoyé à ${apprenant.email} pour la séquence ${sequence.nom}`); + } else if (rappel.templateType === "rappelJ1") { + // Rappel J-1 + await sendRappelJ1Email({ + apprenantEmail: apprenant.email, + apprenantPrenom: apprenant.prenom, + apprenantNom: apprenant.nom, + apprenantFonction: apprenant.fonction, + formationNom: formation.nom, + sequenceNom: sequence.nom, + dates: toutesLesDates.map(d => ({ + dateDebut: d.dateDebut, + dateFin: d.dateFin, + ordre: d.ordre + })), + lieu: sequence.lieu || "", + formateur: formateurNom, + attachmentUrl: rappel.urlFichier || undefined, + attachmentFilename: rappel.nomFichier || undefined, + attachmentMimeType: rappel.typeFichier || undefined, + }); + console.log(`[Rappels] Email J-1 envoyé à ${apprenant.email} pour la séquence ${sequence.nom}`); + } + + // Logger le succès + await logRappelEnvoye({ + rappelId: rappel.id, + sequenceId: sequence.id, + apprenantId: apprenant.id, + emailDestinataire: apprenant.email, + typeRappel: rappel.templateType, + statut: "success", + dateSequence: toutesLesDates[0].dateDebut, + }); + nbEnvoyes++; + } catch (emailError: any) { + const messageErreur = emailError?.message || String(emailError); + console.error(`[Rappels] Erreur lors de l'envoi à ${apprenant.email}:`, emailError); + + // Logger l'échec + await logRappelEnvoye({ + rappelId: rappel.id, + sequenceId: sequence.id, + apprenantId: apprenant.id, + emailDestinataire: apprenant.email, + typeRappel: rappel.templateType, + statut: "failed", + messageErreur: messageErreur, + dateSequence: toutesLesDates[0].dateDebut, + }); + nbEchecs++; + echecs.push({ email: apprenant.email, erreur: messageErreur }); + } + } + + // Notifier l'administrateur en cas d'échecs + if (nbEchecs > 0) { + const listeEchecs = echecs.map(e => `- ${e.email}: ${e.erreur}`).join("\n"); + await notifyOwner({ + title: `⚠️ Échecs d'envoi de rappels`, + content: `${nbEchecs} rappel(s) n'ont pas pu être envoyés pour la séquence "${sequence.nom}" (${formation.nom}):\n\n${listeEchecs}\n\nRappel: ${rappel.nom} (J-${rappel.joursAvant})\nEnvois réussis: ${nbEnvoyes}`, + }); + } + + console.log(`[Rappels] Séquence ${sequence.nom}: ${nbEnvoyes} envoi(s) réussi(s), ${nbEchecs} échec(s)`); + } + + // Mettre à jour la date de dernière exécution + await db + .update(rappels) + .set({ derniereExecution: new Date() }) + .where(eq(rappels.id, rappel.id)); + + } catch (error) { + console.error(`[Rappels] Erreur lors du traitement du rappel ${rappel.nom}:`, error); + } +} + +/** + * Initialise le scheduler pour exécuter les rappels automatiques + * Vérifie toutes les heures s'il y a des rappels à envoyer + */ +export function initRappelScheduler() { + console.log("[Rappels] Initialisation du scheduler de rappels automatiques"); + + // Exécuter immédiatement au démarrage + processRappelsAutomatiques(); + processRemerciementsAutomatiques(); + + // Puis exécuter toutes les heures + setInterval(() => { + processRappelsAutomatiques(); + processRemerciementsAutomatiques(); + }, 60 * 60 * 1000); // 1 heure en millisecondes + + console.log("[Rappels] Scheduler initialisé - vérification toutes les heures (rappels + remerciements)"); +} diff --git a/deployment-package/source_server/remerciementScheduler.ts b/deployment-package/source_server/remerciementScheduler.ts new file mode 100644 index 0000000..3c12bb3 --- /dev/null +++ b/deployment-package/source_server/remerciementScheduler.ts @@ -0,0 +1,351 @@ +import { getDb } from "./db"; +import { sequences, inscriptions, apprenants, formations, datesFormation, formateurs, questionnaires, envoisQuestionnaires, logsNotifications } from "../drizzle/schema"; +import { eq, and, lte, sql } from "drizzle-orm"; +import { sendRemerciementPostFormation } from "./emailService"; +import { logNotification } from "./notificationLogsDb"; +import crypto from "crypto"; + +/** + * Génère un token unique pour accéder au questionnaire + */ +function generateToken(): string { + return crypto.randomBytes(32).toString("hex"); +} + +/** + * Récupère ou crée un lien questionnaire pour un apprenant + */ +async function getOrCreateQuestionnaireLink( + apprenantId: number, + sequenceId: number +): Promise { + const db = await getDb(); + if (!db) return null; + + try { + // Chercher un questionnaire de satisfaction actif + const [questionnaire] = await db + .select() + .from(questionnaires) + .where( + and( + eq(questionnaires.type, "satisfaction"), + eq(questionnaires.actif, true) + ) + ) + .limit(1); + + if (!questionnaire) { + console.log("[Remerciements] Aucun questionnaire de satisfaction actif trouvé"); + return null; + } + + // Vérifier si un envoi existe déjà + const [envoiExistant] = await db + .select() + .from(envoisQuestionnaires) + .where( + and( + eq(envoisQuestionnaires.questionnaireId, questionnaire.id), + eq(envoisQuestionnaires.apprenantId, apprenantId), + eq(envoisQuestionnaires.sequenceId, sequenceId) + ) + ) + .limit(1); + + if (envoiExistant) { + // Retourner le lien existant + const baseUrl = process.env.VITE_FRONTEND_URL || "http://localhost:3000"; + return `${baseUrl}/questionnaire/${envoiExistant.token}`; + } + + // Créer un nouvel envoi + const token = generateToken(); + await db.insert(envoisQuestionnaires).values({ + questionnaireId: questionnaire.id, + apprenantId, + sequenceId, + token, + dateEnvoi: new Date(), + dateReponse: null, + }); + + const baseUrl = process.env.VITE_FRONTEND_URL || "http://localhost:3000"; + return `${baseUrl}/questionnaire/${token}`; + } catch (error) { + console.error("[Remerciements] Erreur lors de la création du lien questionnaire:", error); + return null; + } +} + +/** + * Vérifie si un remerciement a déjà été envoyé pour une inscription en vérifiant dans la base de données + */ +async function remerciementDejaEnvoye(apprenantId: number, sequenceId: number): Promise { + const db = await getDb(); + if (!db) return false; + + try { + const [existing] = await db + .select({ id: logsNotifications.id }) + .from(logsNotifications) + .where( + and( + eq(logsNotifications.type, "remerciement"), + eq(logsNotifications.apprenantId, apprenantId), + eq(logsNotifications.sequenceId, sequenceId), + eq(logsNotifications.statut, "success") + ) + ) + .limit(1); + + return !!existing; + } catch (error) { + console.error("[Remerciements] Erreur lors de la vérification du log:", error); + return false; + } +} + +/** + * Job automatique qui vérifie quotidiennement les séquences terminées + * et envoie les emails de remerciement aux apprenants + */ +export async function processRemerciementsAutomatiques() { + console.log("[Remerciements] Démarrage du traitement des remerciements post-formation..."); + + const db = await getDb(); + if (!db) { + console.error("[Remerciements] Base de données non disponible"); + return; + } + + try { + const now = new Date(); + const hier = new Date(); + hier.setDate(hier.getDate() - 1); + hier.setHours(23, 59, 59, 999); + + // Récupérer les séquences dont la dernière date est passée (terminées hier ou avant) + const sequencesTerminees = await db + .select({ + sequence: sequences, + formation: formations, + formateur: formateurs, + derniereDate: sql`MAX(${datesFormation.dateFin})`.as('derniereDate'), + }) + .from(sequences) + .innerJoin(formations, eq(sequences.formationId, formations.id)) + .leftJoin(formateurs, eq(sequences.formateurId, formateurs.id)) + .innerJoin(datesFormation, eq(sequences.id, datesFormation.sequenceId)) + .where(eq(sequences.statut, 'ouverte')) // Séquences encore ouvertes (pas encore marquées terminées) + .groupBy(sequences.id, formations.id, formateurs.id) + .having(lte(sql`MAX(${datesFormation.dateFin})`, hier)); + + console.log(`[Remerciements] ${sequencesTerminees.length} séquence(s) terminée(s) trouvée(s)`); + + for (const { sequence, formation, formateur, derniereDate } of sequencesTerminees) { + await processRemerciementSequence(sequence, formation, formateur, derniereDate); + } + + console.log("[Remerciements] Traitement terminé"); + } catch (error) { + console.error("[Remerciements] Erreur lors du traitement des remerciements:", error); + } +} + +async function processRemerciementSequence( + sequence: any, + formation: any, + formateur: any | null, + derniereDate: Date +) { + console.log(`[Remerciements] Traitement de la séquence: ${sequence.nom} (terminée le ${derniereDate})`); + + const db = await getDb(); + if (!db) return; + + try { + // Récupérer les inscriptions confirmées pour cette séquence + const inscriptionsConfirmees = await db + .select({ + inscription: inscriptions, + apprenant: apprenants, + }) + .from(inscriptions) + .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .where( + and( + eq(inscriptions.sequenceId, sequence.id), + eq(inscriptions.statut, 'confirmee') + ) + ); + + console.log(`[Remerciements] ${inscriptionsConfirmees.length} inscription(s) confirmée(s) pour ${sequence.nom}`); + + let envoyesCount = 0; + let errorsCount = 0; + + for (const { inscription, apprenant } of inscriptionsConfirmees) { + // Vérifier si le remerciement a déjà été envoyé (vérification en base de données) + const dejaEnvoye = await remerciementDejaEnvoye(apprenant.id, sequence.id); + if (dejaEnvoye) { + console.log(`[Remerciements] Remerciement déjà envoyé pour ${apprenant.email}`); + continue; + } + + try { + // Générer le lien questionnaire + const lienQuestionnaire = await getOrCreateQuestionnaireLink(apprenant.id, sequence.id); + + await sendRemerciementPostFormation({ + apprenantEmail: apprenant.email, + apprenantNom: apprenant.nom, + apprenantPrenom: apprenant.prenom, + apprenantFonction: apprenant.fonction, + formationNom: formation.nom, + sequenceNom: sequence.nom, + formateurNom: formateur?.nom, + lienQuestionnaire: lienQuestionnaire || undefined, + }); + + // Logger la notification (sert aussi de marqueur pour éviter les doublons) + await logNotification({ + type: "remerciement", + sequenceId: sequence.id, + apprenantId: apprenant.id, + emailDestinataire: apprenant.email, + sujet: `Merci pour votre participation - ${formation.nom}`, + statut: "success", + metadata: { lienQuestionnaire }, + }); + + envoyesCount++; + console.log(`[Remerciements] Email envoyé à ${apprenant.email}`); + } catch (error: any) { + // Logger l'échec + await logNotification({ + type: "remerciement", + sequenceId: sequence.id, + apprenantId: apprenant.id, + emailDestinataire: apprenant.email, + sujet: `Merci pour votre participation - ${formation.nom}`, + statut: "failed", + messageErreur: error.message, + }); + + errorsCount++; + console.error(`[Remerciements] Erreur envoi à ${apprenant.email}:`, error); + } + } + + console.log(`[Remerciements] Séquence ${sequence.nom}: ${envoyesCount} envoyé(s), ${errorsCount} erreur(s)`); + + // Optionnel: Marquer la séquence comme terminée + // await db.update(sequences).set({ statut: 'terminee' }).where(eq(sequences.id, sequence.id)); + + } catch (error) { + console.error(`[Remerciements] Erreur pour la séquence ${sequence.nom}:`, error); + } +} + +/** + * Envoie manuellement un email de remerciement pour une séquence spécifique + * Utilisé par l'interface admin pour envoyer les remerciements à la demande + */ +export async function envoyerRemerciementsSequence(sequenceId: number): Promise<{ sent: number; failed: number }> { + const db = await getDb(); + if (!db) { + throw new Error("Base de données non disponible"); + } + + // Récupérer la séquence avec formation et formateur + const sequenceData = await db + .select({ + sequence: sequences, + formation: formations, + formateur: formateurs, + }) + .from(sequences) + .innerJoin(formations, eq(sequences.formationId, formations.id)) + .leftJoin(formateurs, eq(sequences.formateurId, formateurs.id)) + .where(eq(sequences.id, sequenceId)) + .limit(1); + + if (sequenceData.length === 0) { + throw new Error("Séquence introuvable"); + } + + const { sequence, formation, formateur } = sequenceData[0]; + + // Récupérer les inscriptions confirmées + const inscriptionsConfirmees = await db + .select({ + inscription: inscriptions, + apprenant: apprenants, + }) + .from(inscriptions) + .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .where( + and( + eq(inscriptions.sequenceId, sequenceId), + eq(inscriptions.statut, 'confirmee') + ) + ); + + let sent = 0; + let failed = 0; + + for (const { inscription, apprenant } of inscriptionsConfirmees) { + // Vérifier si le remerciement a déjà été envoyé + const dejaEnvoye = await remerciementDejaEnvoye(apprenant.id, sequenceId); + if (dejaEnvoye) { + console.log(`[Remerciements] Remerciement déjà envoyé pour ${apprenant.email}, ignoré`); + continue; + } + + try { + // Générer le lien questionnaire + const lienQuestionnaire = await getOrCreateQuestionnaireLink(apprenant.id, sequenceId); + + await sendRemerciementPostFormation({ + apprenantEmail: apprenant.email, + apprenantNom: apprenant.nom, + apprenantPrenom: apprenant.prenom, + apprenantFonction: apprenant.fonction, + formationNom: formation.nom, + sequenceNom: sequence.nom, + formateurNom: formateur?.nom, + lienQuestionnaire: lienQuestionnaire || undefined, + }); + + // Logger la notification + await logNotification({ + type: "remerciement", + sequenceId, + apprenantId: apprenant.id, + emailDestinataire: apprenant.email, + sujet: `Merci pour votre participation - ${formation.nom}`, + statut: "success", + metadata: { lienQuestionnaire }, + }); + + sent++; + } catch (error: any) { + // Logger l'échec + await logNotification({ + type: "remerciement", + sequenceId, + apprenantId: apprenant.id, + emailDestinataire: apprenant.email, + sujet: `Merci pour votre participation - ${formation.nom}`, + statut: "failed", + messageErreur: error.message, + }); + + console.error(`[Remerciements] Erreur envoi à ${apprenant.email}:`, error); + failed++; + } + } + + return { sent, failed }; +} diff --git a/deployment-package/source_server/routers.ts b/deployment-package/source_server/routers.ts new file mode 100644 index 0000000..5ee1383 --- /dev/null +++ b/deployment-package/source_server/routers.ts @@ -0,0 +1,2380 @@ +import { COOKIE_NAME } from "@shared/const"; +import { getSessionCookieOptions } from "./_core/cookies"; +import { systemRouter } from "./_core/systemRouter"; +import { publicProcedure, protectedProcedure, router } from "./_core/trpc"; +import { parseLocalDateTime } from "./dateUtils"; +import { z } from "zod"; +import * as db from "./db"; +import * as analyticsDb from "./analyticsDb"; +import { generateAnalyticsExcel, generateAnalyticsPDF } from "./analyticsExportService"; +import { TRPCError } from "@trpc/server"; +import { sendInscriptionConfirmation, sendGroupEmail, sendNotificationFormateurNouvelleInscription, sendNotificationFormateurAnnulation, sendAlerteCapaciteAtteinte, sendNotificationPlaceDisponible, sendRemerciementPostFormation } from "./emailService"; +import { logNotification } from "./notificationLogsDb"; +import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService"; +import { parseExcelFile, validateImportData, importToDatabase } from "./importExcel"; + +// Procédure admin uniquement +const adminProcedure = protectedProcedure.use(({ ctx, next }) => { + if (ctx.user.role !== 'admin') { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux administrateurs' }); + } + return next({ ctx }); +}); + +export const appRouter = router({ + system: systemRouter, + auth: router({ + me: publicProcedure.query(opts => opts.ctx.user), + logout: publicProcedure.mutation(({ ctx }) => { + const cookieOptions = getSessionCookieOptions(ctx.req); + ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 }); + return { + success: true, + } as const; + }), + }), + + // ===== FORMATIONS ===== + formations: router({ + list: protectedProcedure.query(async ({ ctx }) => { + // Si l'utilisateur est formateur, ne montrer que les formations pour lesquelles il a des séquences + if (ctx.user.role === 'formateur' && ctx.user.formateurId) { + return db.getFormationsByFormateur(ctx.user.formateurId); + } else if (ctx.user.role === 'admin') { + return db.getFormations(); + } else { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } + }), + + getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { + return db.getFormationById(input.id); + }), + + getByLien: publicProcedure.input(z.object({ lien: z.string() })).query(async ({ input }) => { + return db.getFormationByLien(input.lien); + }), + + create: adminProcedure.input(z.object({ + nom: z.string().min(1), + description: z.string().optional(), + lienUnique: z.string().min(1), + actif: z.boolean().optional(), + })).mutation(async ({ input }) => { + await db.createFormation(input); + return { success: true }; + }), + + update: adminProcedure.input(z.object({ + id: z.number(), + nom: z.string().min(1).optional(), + description: z.string().optional(), + lienUnique: z.string().min(1).optional(), + actif: z.boolean().optional(), + })).mutation(async ({ input }) => { + const { id, ...data } = input; + await db.updateFormation(id, data); + return { success: true }; + }), + + delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { + await db.deleteFormation(input.id); + return { success: true }; + }), + + importExcel: adminProcedure.input(z.object({ + fileBase64: z.string(), + })).mutation(async ({ input }) => { + try { + // Décoder le fichier base64 + const buffer = Buffer.from(input.fileBase64, 'base64'); + + // Parser le fichier Excel + const { formations, sequences } = parseExcelFile(buffer); + + // Valider les données + const validation = validateImportData(formations, sequences); + + if (!validation.valid) { + return { + success: false, + formationsCreated: 0, + sequencesCreated: 0, + errors: validation.errors, + warnings: validation.warnings, + }; + } + + // Importer dans la base de données + const result = await importToDatabase(formations, sequences); + + return { + ...result, + warnings: [...validation.warnings, ...result.warnings], + }; + } catch (error: any) { + return { + success: false, + formationsCreated: 0, + sequencesCreated: 0, + errors: [`Erreur lors du traitement du fichier: ${error.message}`], + warnings: [], + }; + } + }), + + previewExcel: adminProcedure.input(z.object({ + fileBase64: z.string(), + })).mutation(async ({ input }) => { + try { + const buffer = Buffer.from(input.fileBase64, 'base64'); + const { formations, sequences } = parseExcelFile(buffer); + const validation = validateImportData(formations, sequences); + + return { + formations, + sequences, + validation, + }; + } catch (error: any) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `Erreur lors de la lecture du fichier: ${error.message}`, + }); + } + }), + }), + + // ===== APPRENANTS ===== + apprenants: router({ + list: protectedProcedure.query(async ({ ctx }) => { + // Si l'utilisateur est formateur, ne montrer que les apprenants inscrits à ses séquences + if (ctx.user.role === 'formateur' && ctx.user.formateurId) { + return db.getApprenantsByFormateur(ctx.user.formateurId); + } else if (ctx.user.role === 'admin') { + return db.getApprenants(); + } else { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } + }), + + getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { + return db.getApprenantById(input.id); + }), + + getByEmail: publicProcedure.input(z.object({ email: z.string() })).query(async ({ input }) => { + return db.getApprenantByEmail(input.email); + }), + + create: adminProcedure.input(z.object({ + nom: z.string().min(1), + prenom: z.string().min(1), + email: z.string().email(), + codeEtablissement: z.string().min(1), + fonction: z.enum(["directeur", "chef_service", "autre"]), + })).mutation(async ({ input }) => { + await db.createApprenant(input); + return { success: true }; + }), + + // Procédure publique pour créer un apprenant lors de l'inscription + createPublic: publicProcedure.input(z.object({ + nom: z.string().min(1), + prenom: z.string().min(1), + email: z.string().email(), + codeEtablissement: z.string().min(1), + fonction: z.enum(["directeur", "chef_service", "autre"]), + })).mutation(async ({ input }) => { + await db.createApprenant(input); + return { success: true }; + }), + + update: adminProcedure.input(z.object({ + id: z.number(), + nom: z.string().min(1).optional(), + prenom: z.string().min(1).optional(), + email: z.string().email().optional(), + codeEtablissement: z.string().min(1).optional(), + fonction: z.enum(["directeur", "chef_service", "autre"]).optional(), + })).mutation(async ({ input }) => { + const { id, ...data } = input; + await db.updateApprenant(id, data); + return { success: true }; + }), + + delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { + await db.deleteApprenant(input.id); + return { success: true }; + }), + + getInscriptions: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { + const results = await db.getInscriptionsByApprenant(input.id); + // Récupérer les informations de formation et séquence pour chaque inscription + const inscriptionsAvecDetails = await Promise.all( + results.map(async (result: any) => { + const insc = result.inscription; + if (!insc) return null; + const sequence = await db.getSequenceById(insc.sequenceId); + if (!sequence) return null; + const formation = await db.getFormationById(sequence.formationId); + const dates = await db.getDatesBySequence(insc.sequenceId); + return { + ...insc, + sequence: { ...sequence, dates }, + formation, + }; + }) + ); + return inscriptionsAvecDetails.filter(i => i !== null); + }), + }), + + // ===== FORMATEURS ===== + formateurs: router({ + list: adminProcedure.query(async () => { + return db.getFormateurs(); + }), + + // Tableau de bord formateur + dashboardStats: protectedProcedure.query(async ({ ctx }) => { + if (ctx.user.role !== 'formateur' || !ctx.user.formateurId) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux formateurs' }); + } + const formateurDb = await import('./formateurDb'); + return formateurDb.getFormateurDashboardStats(ctx.user.formateurId); + }), + + prochainesSequences: protectedProcedure + .input(z.object({ limit: z.number().optional().default(10) })) + .query(async ({ ctx, input }) => { + if (ctx.user.role !== 'formateur' || !ctx.user.formateurId) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux formateurs' }); + } + const formateurDb = await import('./formateurDb'); + return formateurDb.getFormateurProchainesSequences(ctx.user.formateurId, input.limit); + }), + + getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { + return db.getFormateurById(input.id); + }), + + create: adminProcedure.input(z.object({ + nom: z.string().min(1), + email: z.string().email().optional(), + })).mutation(async ({ input }) => { + await db.createFormateur(input); + return { success: true }; + }), + + update: adminProcedure.input(z.object({ + id: z.number(), + nom: z.string().min(1).optional(), + email: z.string().email().nullable().optional(), + })).mutation(async ({ input }) => { + const { id, ...data } = input; + await db.updateFormateur(id, data); + return { success: true }; + }), + + delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { + await db.deleteFormateur(input.id); + return { success: true }; + }), + }), + + // ===== SÉQUENCES ===== + sequences: router({ + list: protectedProcedure.query(async ({ ctx }) => { + let seqs; + + // Si l'utilisateur est formateur, ne montrer que ses séquences + if (ctx.user.role === 'formateur' && ctx.user.formateurId) { + seqs = await db.getSequencesByFormateur(ctx.user.formateurId); + } else if (ctx.user.role === 'admin') { + seqs = await db.getSequencesWithFormation(); + } else { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } + // Récupérer les dates et le nombre d'inscrits pour chaque séquence + const sequencesAvecDates = await Promise.all( + seqs.map(async (seq) => { + const dates = await db.getDatesBySequence(seq.id); + const nbInscrits = await db.countInscriptionsBySequence(seq.id, 'confirmee'); + + // Récupérer les rappels associés à chaque date + const datesAvecRappels = await Promise.all( + dates.map(async (date) => { + const rappels = await db.getRappelsByDateFormation(date.id); + return { ...date, rappels }; + }) + ); + + return { ...seq, dates: datesAvecRappels, nbInscrits }; + }) + ); + return sequencesAvecDates; + }), + + listByFormation: publicProcedure.input(z.object({ formationId: z.number() })).query(async ({ input }) => { + const seqs = await db.getSequencesByFormation(input.formationId); + // Récupérer les dates et le nombre d'inscrits pour chaque séquence + const sequencesAvecDates = await Promise.all( + seqs.map(async (seq) => { + const dates = await db.getDatesBySequence(seq.id); + const nbInscrits = await db.countInscriptionsBySequence(seq.id, 'confirmee'); + return { ...seq, dates, nbInscrits }; + }) + ); + return sequencesAvecDates; + }), + + getById: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { + const seq = await db.getSequenceById(input.id); + if (!seq) return null; + const dates = await db.getDatesBySequence(input.id); + const nbInscrits = await db.countInscriptionsBySequence(input.id, 'confirmee'); + return { ...seq, dates, nbInscrits }; + }), + + create: adminProcedure.input(z.object({ + formationId: z.number(), + nom: z.string().min(1), + lieu: z.string().min(1), + publicCible: z.enum(["directeur", "chef_service", "tous", "autre"]), + formateurId: z.number().nullable().optional(), + capaciteMax: z.number().default(12), + dateBlocage: z.string(), + statut: z.enum(["ouverte", "bloquee", "terminee"]).optional(), + dates: z.array(z.object({ + dateDebut: z.string(), + dateFin: z.string(), + ordre: z.number(), + })).min(1).max(4), + })).mutation(async ({ input }) => { + const { dates, ...sequenceData } = input; + + // Créer la séquence + const result = await db.createSequence({ + ...sequenceData, + dateBlocage: parseLocalDateTime(sequenceData.dateBlocage), + }); + + // Récupérer l'ID de la séquence créée + const sequenceId = Number(result[0].insertId); + + // Créer les dates de formation + for (const date of dates) { + await db.createDateFormation({ + sequenceId, + dateDebut: parseLocalDateTime(date.dateDebut), + dateFin: parseLocalDateTime(date.dateFin), + ordre: date.ordre, + }); + } + + return { success: true, sequenceId }; + }), + + update: adminProcedure.input(z.object({ + id: z.number(), + formationId: z.number(), + nom: z.string().min(1), + lieu: z.string().min(1), + publicCible: z.enum(["directeur", "chef_service", "tous", "autre"]), + formateurId: z.number().nullable().optional(), + capaciteMax: z.number(), + dateBlocage: z.string(), + statut: z.enum(["ouverte", "bloquee", "terminee"]), + dates: z.array(z.object({ + dateDebut: z.string(), + dateFin: z.string(), + ordre: z.number(), + })).min(1).max(4), + })).mutation(async ({ input }) => { + const { id, dates, ...sequenceData } = input; + + console.log('[UPDATE SEQUENCE] Input reçu:', JSON.stringify(input, null, 2)); + + // Mettre à jour la séquence + const updateData: any = { + ...sequenceData, + dateBlocage: parseLocalDateTime(sequenceData.dateBlocage), + }; + + console.log('[UPDATE SEQUENCE] UpdateData:', JSON.stringify(updateData, null, 2)); + await db.updateSequence(id, updateData); + console.log('[UPDATE SEQUENCE] Séquence mise à jour avec succès'); + + // Supprimer les anciennes dates + await db.deleteDatesBySequence(id); + console.log('[UPDATE SEQUENCE] Anciennes dates supprimées'); + + // Créer les nouvelles dates + for (const date of dates) { + console.log('[UPDATE SEQUENCE] Création date:', date); + await db.createDateFormation({ + sequenceId: id, + dateDebut: parseLocalDateTime(date.dateDebut), + dateFin: parseLocalDateTime(date.dateFin), + ordre: date.ordre, + }); + } + console.log('[UPDATE SEQUENCE] Toutes les dates ont été créées'); + + return { success: true }; + }), + + delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { + // Supprimer d'abord les dates associées + await db.deleteDatesBySequence(input.id); + // Puis supprimer la séquence + await db.deleteSequence(input.id); + return { success: true }; + }), + }), + + // ===== INSCRIPTIONS ===== + inscriptions: router({ + // Liste des inscriptions avec dates de formation (pour formateurs) + listWithDates: protectedProcedure + .input(z.object({ sequenceId: z.number() })) + .query(async ({ input }) => { + const { getInscriptionsWithDates } = await import("./inscriptionWithDates"); + return getInscriptionsWithDates(input.sequenceId); + }), + + listAll: adminProcedure.query(async () => { + return db.getAllInscriptions(); + }), + + listBySequence: adminProcedure.input(z.object({ sequenceId: z.number() })).query(async ({ input }) => { + return db.getInscriptionsBySequence(input.sequenceId); + }), + + listByApprenant: publicProcedure.input(z.object({ apprenantId: z.number() })).query(async ({ input }) => { + return db.getInscriptionsByApprenant(input.apprenantId); + }), + + listByApprenantWithDates: publicProcedure + .input(z.object({ apprenantId: z.number() })) + .query(async ({ input }) => { + const { getInscriptionsByApprenantWithDates } = await import("./inscriptionWithDates"); + return getInscriptionsByApprenantWithDates(input.apprenantId); + }), + + checkExisting: publicProcedure.input(z.object({ + apprenantId: z.number(), + sequenceId: z.number(), + })).query(async ({ input }) => { + return db.checkExistingInscription(input.apprenantId, input.sequenceId); + }), + + inscrire: publicProcedure.input(z.object({ + apprenantId: z.number(), + sequenceId: z.number(), + })).mutation(async ({ input }) => { + // Vérifier si la séquence existe + const sequence = await db.getSequenceById(input.sequenceId); + if (!sequence) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' }); + } + + // Vérifier si la séquence est bloquée + const now = new Date(); + if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'Les inscriptions sont fermées pour cette séquence (J-15 dépassé)' + }); + } + + // Vérifier si l'apprenant a déjà une inscription + const existing = await db.checkExistingInscription(input.apprenantId, input.sequenceId); + if (existing) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'Vous êtes déjà inscrit à cette séquence' + }); + } + + // Vérifier la capacité + const nbInscrits = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee'); + const statut = nbInscrits >= sequence.capaciteMax ? 'liste_attente' : 'confirmee'; + + await db.createInscription({ + apprenantId: input.apprenantId, + sequenceId: input.sequenceId, + statut, + }); + + // Envoyer l'email de confirmation avec invitations Outlook pour toutes les dates + const inscriptionSequence = await db.getSequenceById(input.sequenceId); + const inscriptionApprenant = await db.getApprenantById(input.apprenantId); + const inscriptionFormation = inscriptionSequence ? await db.getFormationById(inscriptionSequence.formationId) : null; + const dates = inscriptionSequence ? await db.getDatesBySequence(inscriptionSequence.id) : []; + + if (inscriptionSequence && inscriptionApprenant && inscriptionFormation && dates.length > 0) { + // Utiliser la première date pour l'email principal + const premiereDate = dates[0]; + + await sendInscriptionConfirmation({ + apprenantEmail: inscriptionApprenant.email, + apprenantNom: inscriptionApprenant.nom, + apprenantPrenom: inscriptionApprenant.prenom, + apprenantFonction: inscriptionApprenant.fonction, + formationNom: inscriptionFormation.nom, + sequenceNom: inscriptionSequence.nom, + dates: dates.map(d => ({ + dateDebut: new Date(d.dateDebut), + dateFin: new Date(d.dateFin), + ordre: d.ordre, + })), + lieu: inscriptionSequence.lieu, + statut, + }); + + // Notification au formateur si présent + if (inscriptionSequence.formateurId) { + const formateur = await db.getFormateurById(inscriptionSequence.formateurId); + if (formateur && formateur.email) { + const nbInscritsActuel = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee'); + try { + await sendNotificationFormateurNouvelleInscription({ + formateurEmail: formateur.email, + formateurNom: formateur.nom, + apprenantNom: inscriptionApprenant.nom, + apprenantPrenom: inscriptionApprenant.prenom, + apprenantFonction: inscriptionApprenant.fonction, + apprenantEtablissement: inscriptionApprenant.codeEtablissement, + formationNom: inscriptionFormation.nom, + sequenceNom: inscriptionSequence.nom, + nbInscrits: nbInscritsActuel, + capaciteMax: inscriptionSequence.capaciteMax, + dates: dates.map(d => ({ + dateDebut: new Date(d.dateDebut), + dateFin: new Date(d.dateFin), + ordre: d.ordre, + })), + }); + // Logger la notification + await logNotification({ + type: "notification_formateur_inscription", + sequenceId: input.sequenceId, + apprenantId: inscriptionApprenant.id, + formateurId: formateur.id, + emailDestinataire: formateur.email, + sujet: `Nouvelle inscription - ${inscriptionFormation.nom}`, + statut: "success", + }); + console.log(`[Notification] Email envoyé au formateur ${formateur.email} pour nouvelle inscription`); + } catch (e: any) { + // Logger l'échec + await logNotification({ + type: "notification_formateur_inscription", + sequenceId: input.sequenceId, + apprenantId: inscriptionApprenant.id, + formateurId: formateur.id, + emailDestinataire: formateur.email, + sujet: `Nouvelle inscription - ${inscriptionFormation.nom}`, + statut: "failed", + messageErreur: e.message, + }); + console.error(`[Notification] Erreur envoi email formateur:`, e); + } + } + } + + // Alerte capacité atteinte pour les admins ET le formateur + const nbInscritsApres = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee'); + if (nbInscritsApres >= inscriptionSequence.capaciteMax) { + try { + const admins = await db.getAdminUsers(); + const adminEmails = admins.filter(a => a.email).map(a => a.email!); + if (adminEmails.length > 0) { + const nbListeAttente = await db.countInscriptionsBySequence(input.sequenceId, 'liste_attente'); + let formateurNom: string | undefined; + if (inscriptionSequence.formateurId) { + const formateur = await db.getFormateurById(inscriptionSequence.formateurId); + formateurNom = formateur?.nom; + } + await sendAlerteCapaciteAtteinte({ + adminEmails, + formationNom: inscriptionFormation.nom, + sequenceNom: inscriptionSequence.nom, + capaciteMax: inscriptionSequence.capaciteMax, + nbInscrits: nbInscritsApres, + nbListeAttente, + formateurNom, + dates: dates.map(d => ({ + dateDebut: new Date(d.dateDebut), + dateFin: new Date(d.dateFin), + ordre: d.ordre, + })), + }); + console.log(`[Notification] Alerte capacité atteinte envoyée à ${adminEmails.length} admin(s)`); + } + + // Notification au formateur pour capacité atteinte + if (inscriptionSequence.formateurId) { + const formateur = await db.getFormateurById(inscriptionSequence.formateurId); + if (formateur && formateur.email) { + const { sendNotificationFormateurCapaciteAtteinte } = await import('./emailService'); + await sendNotificationFormateurCapaciteAtteinte({ + formateurEmail: formateur.email, + formateurNom: formateur.nom, + formationNom: inscriptionFormation.nom, + sequenceNom: inscriptionSequence.nom, + capaciteMax: inscriptionSequence.capaciteMax, + nbInscrits: nbInscritsApres, + dates: dates.map(d => ({ + dateDebut: new Date(d.dateDebut), + dateFin: new Date(d.dateFin), + ordre: d.ordre, + })), + }); + console.log(`[Notification] Alerte capacité atteinte envoyée au formateur ${formateur.email}`); + } + } + } catch (e) { + console.error(`[Notification] Erreur envoi alerte capacité:`, e); + } + } + } + + return { success: true, statut }; + }), + + desinscrire: publicProcedure.input(z.object({ + apprenantId: z.number(), + sequenceId: z.number(), + })).mutation(async ({ input }) => { + // Vérifier si la séquence existe + const sequence = await db.getSequenceById(input.sequenceId); + if (!sequence) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' }); + } + + // Vérifier si la séquence est bloquée + const now = new Date(); + if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'Les désinscriptions sont fermées pour cette séquence (J-15 dépassé)' + }); + } + + // Trouver l'inscription + const inscription = await db.checkExistingInscription(input.apprenantId, input.sequenceId); + if (!inscription) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Inscription introuvable' }); + } + + // Récupérer les infos avant la mise à jour pour les notifications + const apprenant = await db.getApprenantById(input.apprenantId); + const formation = await db.getFormationById(sequence.formationId); + + await db.updateInscription(inscription.id, { statut: 'annulee' }); + + // Notifications après désinscription + if (apprenant && formation) { + // Notification au formateur + if (sequence.formateurId) { + const formateur = await db.getFormateurById(sequence.formateurId); + if (formateur && formateur.email) { + const nbInscritsApres = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee'); + try { + await sendNotificationFormateurAnnulation({ + formateurEmail: formateur.email, + formateurNom: formateur.nom, + apprenantNom: apprenant.nom, + apprenantPrenom: apprenant.prenom, + apprenantFonction: apprenant.fonction || '', + formationNom: formation.nom, + sequenceNom: sequence.nom, + nbInscrits: nbInscritsApres, + capaciteMax: sequence.capaciteMax, + }); + // Logger la notification + await logNotification({ + type: "notification_formateur_annulation", + sequenceId: input.sequenceId, + apprenantId: apprenant.id, + formateurId: formateur.id, + emailDestinataire: formateur.email, + sujet: `Annulation d'inscription - ${formation.nom}`, + statut: "success", + }); + console.log(`[Notification] Email d'annulation envoyé au formateur ${formateur.email}`); + } catch (e: any) { + // Logger l'échec + await logNotification({ + type: "notification_formateur_annulation", + sequenceId: input.sequenceId, + apprenantId: apprenant.id, + formateurId: formateur.id, + emailDestinataire: formateur.email, + sujet: `Annulation d'inscription - ${formation.nom}`, + statut: "failed", + messageErreur: e.message, + }); + console.error(`[Notification] Erreur envoi email formateur:`, e); + } + } + } + + // Notifier le premier en liste d'attente qu'une place s'est libérée + const inscriptionsListeAttente = await db.getInscriptionsBySequence(input.sequenceId); + const premierEnAttente = inscriptionsListeAttente + .filter(i => i.inscription.statut === 'liste_attente') + .sort((a, b) => new Date(a.inscription.dateInscription).getTime() - new Date(b.inscription.dateInscription).getTime())[0]; + + if (premierEnAttente && premierEnAttente.apprenant) { + try { + await sendNotificationPlaceDisponible({ + apprenantEmail: premierEnAttente.apprenant.email, + apprenantNom: premierEnAttente.apprenant.nom, + apprenantPrenom: premierEnAttente.apprenant.prenom, + apprenantFonction: premierEnAttente.apprenant.fonction, + formationNom: formation.nom, + sequenceNom: sequence.nom, + positionListeAttente: 1, + delaiReponse: 48, + }); + // Logger la notification + await logNotification({ + type: "notification_liste_attente", + sequenceId: input.sequenceId, + apprenantId: premierEnAttente.apprenant.id, + emailDestinataire: premierEnAttente.apprenant.email, + sujet: `Place disponible - ${formation.nom}`, + statut: "success", + }); + console.log(`[Notification] Email place disponible envoyé à ${premierEnAttente.apprenant.email}`); + } catch (e: any) { + // Logger l'échec + await logNotification({ + type: "notification_liste_attente", + sequenceId: input.sequenceId, + apprenantId: premierEnAttente.apprenant.id, + emailDestinataire: premierEnAttente.apprenant.email, + sujet: `Place disponible - ${formation.nom}`, + statut: "failed", + messageErreur: e.message, + }); + console.error(`[Notification] Erreur envoi email liste d'attente:`, e); + } + } + } + + return { success: true }; + }), + + updateStatut: adminProcedure.input(z.object({ + id: z.number(), + statut: z.enum(['confirmee', 'liste_attente', 'annulee']), + })).mutation(async ({ input }) => { + // Récupérer l'inscription avant modification + const inscriptionAvant = await db.getInscriptionById(input.id); + const ancienStatut = inscriptionAvant?.statut; + + await db.updateInscription(input.id, { statut: input.statut }); + + // Si passage à annulée, notifier le formateur + if (input.statut === 'annulee' && ancienStatut !== 'annulee' && inscriptionAvant) { + const sequence = await db.getSequenceById(inscriptionAvant.sequenceId); + const apprenant = await db.getApprenantById(inscriptionAvant.apprenantId); + + if (sequence && apprenant && sequence.formateurId) { + const formateur = await db.getFormateurById(sequence.formateurId); + const formation = await db.getFormationById(sequence.formationId); + + if (formateur && formateur.email && formation) { + const nbInscritsApres = await db.countInscriptionsBySequence(sequence.id, 'confirmee'); + + try { + await sendNotificationFormateurAnnulation({ + formateurEmail: formateur.email, + formateurNom: formateur.nom, + apprenantNom: apprenant.nom, + apprenantPrenom: apprenant.prenom, + apprenantFonction: apprenant.fonction || '', + formationNom: formation.nom, + sequenceNom: sequence.nom, + nbInscrits: nbInscritsApres, + capaciteMax: sequence.capaciteMax, + }); + console.log(`[Notification] Annulation envoyée au formateur ${formateur.email}`); + + // Si une place se libère (passage de capacité max à disponible) + if (nbInscritsApres < sequence.capaciteMax && (nbInscritsApres + 1) >= sequence.capaciteMax) { + const { sendNotificationFormateurPlaceDisponible } = await import('./emailService'); + await sendNotificationFormateurPlaceDisponible({ + formateurEmail: formateur.email, + formateurNom: formateur.nom, + formationNom: formation.nom, + sequenceNom: sequence.nom, + capaciteMax: sequence.capaciteMax, + nbInscrits: nbInscritsApres, + placesDisponibles: sequence.capaciteMax - nbInscritsApres, + }); + console.log(`[Notification] Place disponible envoyée au formateur ${formateur.email}`); + } + } catch (e) { + console.error(`[Notification] Erreur envoi notification formateur:`, e); + } + } + } + } + + return { success: true }; + }), + + sendGroupEmail: adminProcedure.input(z.object({ sequenceId: z.number(), type: z.enum(['teaser', 'rappel', 'rappel_j1']) })).mutation(async ({ input }) => { + const sequence = await db.getSequenceById(input.sequenceId); + if (!sequence) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' }); + } + + const formation = await db.getFormationById(sequence.formationId); + if (!formation) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' }); + } + + const dates = await db.getDatesBySequence(sequence.id); + if (dates.length === 0) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Aucune date trouvée pour cette séquence' }); + } + + const inscriptions = await db.getInscriptionsBySequence(input.sequenceId); + const recipients = inscriptions + .filter(i => i.inscription.statut === 'confirmee' && i.apprenant) + .map(i => ({ + email: i.apprenant!.email, + prenom: i.apprenant!.prenom, + nom: i.apprenant!.nom, + fonction: i.apprenant!.fonction, + })); + + console.log(`[Email Group] Envoi de ${recipients.length} emails de type ${input.type}`); + console.log(`[Email Group] Destinataires:`, recipients.map(r => r.email)); + + let formateurNom: string | undefined; + if (sequence.formateurId) { + const formateur = await db.getFormateurById(sequence.formateurId); + formateurNom = formateur?.nom; + } + + const result = await sendGroupEmail({ + recipients, + formationNom: formation.nom, + sequenceNom: sequence.nom, + type: input.type, + dates: dates.map(d => ({ + dateDebut: new Date(d.dateDebut), + dateFin: new Date(d.dateFin), + ordre: d.ordre, + })), + lieu: sequence.lieu, + formateur: formateurNom, + }); + + console.log(`[Email Group] Résultat: ${result.sent} envoyés, ${result.failed} échoués`); + + return { + ...result, + message: `${result.sent} email(s) envoyé(s) avec succès, ${result.failed} échoué(s)` + }; + }), + + emailPreview: adminProcedure + .input(z.object({ + sequenceId: z.number(), + type: z.enum(['teaser', 'rappel', 'rappel_j1']), + apprenantId: z.number().optional(), + })) + .query(async ({ input }) => { + const { generateEmailPreview } = await import('./emailPreview'); + return await generateEmailPreview(input); + }), + + exportExcel: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => { + const sequence = await db.getSequenceById(input.sequenceId); + if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' }); + + const formation = await db.getFormationById(sequence.formationId); + if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' }); + + const inscriptions = await db.getInscriptionsBySequence(input.sequenceId); + const dates = await db.getDatesBySequence(input.sequenceId); + + const data = inscriptions + .filter(i => i.apprenant) + .map(i => ({ + nom: i.apprenant!.nom, + prenom: i.apprenant!.prenom, + email: i.apprenant!.email, + codeEtablissement: i.apprenant!.codeEtablissement, + fonction: i.apprenant!.fonction, + statut: i.inscription.statut, + dateInscription: i.inscription.dateInscription, + })); + + const buffer = await generateExcelExport({ + formationNom: formation.nom, + sequenceNom: sequence.nom, + dates: dates.map(d => ({ + dateDebut: new Date(d.dateDebut), + dateFin: new Date(d.dateFin), + ordre: d.ordre, + })), + lieu: sequence.lieu, + publicCible: sequence.publicCible, + inscriptions: data, + }); + + return { + buffer: buffer.toString('base64'), + filename: `inscriptions_${sequence.nom.replace(/\s+/g, '_')}.xlsx`, + }; + }), + + exportPDF: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => { + const sequence = await db.getSequenceById(input.sequenceId); + if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' }); + + const formation = await db.getFormationById(sequence.formationId); + if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' }); + + const inscriptions = await db.getInscriptionsBySequence(input.sequenceId); + const dates = await db.getDatesBySequence(input.sequenceId); + + const data = inscriptions + .filter(i => i.apprenant) + .map(i => ({ + nom: i.apprenant!.nom, + prenom: i.apprenant!.prenom, + email: i.apprenant!.email, + codeEtablissement: i.apprenant!.codeEtablissement, + fonction: i.apprenant!.fonction, + statut: i.inscription.statut, + dateInscription: i.inscription.dateInscription, + })); + + const buffer = await generatePDFExport({ + formationNom: formation.nom, + sequenceNom: sequence.nom, + dates: dates.map(d => ({ + dateDebut: new Date(d.dateDebut), + dateFin: new Date(d.dateFin), + ordre: d.ordre, + })), + lieu: sequence.lieu, + publicCible: sequence.publicCible, + inscriptions: data, + }); + + return { + buffer: buffer.toString('base64'), + filename: `inscriptions_${sequence.nom.replace(/\s+/g, '_')}.pdf`, + }; + }), + + exportFeuillePresence: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => { + const sequence = await db.getSequenceById(input.sequenceId); + if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' }); + + const formation = await db.getFormationById(sequence.formationId); + if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' }); + + const inscriptions = await db.getInscriptionsBySequence(input.sequenceId); + const dates = await db.getDatesBySequence(input.sequenceId); + + // Récupérer le formateur si défini + let formateurNom: string | undefined = undefined; + if (sequence.formateurId) { + const formateur = await db.getFormateurById(sequence.formateurId); + if (formateur) { + formateurNom = formateur.nom; + } + } + + const data = inscriptions + .filter(i => i.inscription.statut === 'confirmee' && i.apprenant) + .map(i => ({ + nom: i.apprenant!.nom, + prenom: i.apprenant!.prenom, + codeEtablissement: i.apprenant!.codeEtablissement, + fonction: i.apprenant!.fonction, + })); + + const buffer = await generateFeuillePresence({ + formationNom: formation.nom, + sequenceNom: sequence.nom, + dates: dates.map(d => ({ + dateDebut: new Date(d.dateDebut), + dateFin: new Date(d.dateFin), + ordre: d.ordre, + })), + lieu: sequence.lieu, + publicCible: sequence.publicCible, + formateur: formateurNom, + apprenants: data, + }); + + return { + buffer: buffer.toString('base64'), + filename: `feuille_presence_${sequence.nom.replace(/\s+/g, '_')}.pdf`, + }; + }), + }), + + // ===== GESTION DES UTILISATEURS ===== + users: router({ + list: adminProcedure.query(async () => { + return db.getAllUsers(); + }), + + listWithFormateurs: adminProcedure.query(async () => { + const dbInstance = await import("./db").then(m => m.getDb()); + if (!dbInstance) throw new Error("Base de données non disponible"); + + const { formateurs, users } = await import("../drizzle/schema"); + const { eq } = await import("drizzle-orm"); + + // Récupérer tous les formateurs + const allFormateurs = await dbInstance.select().from(formateurs); + + // Récupérer tous les utilisateurs + const allUsers = await dbInstance.select().from(users); + + // Créer une map des formateurs liés à des comptes + const formateurIdToUser = new Map(); + allUsers.forEach(user => { + if (user.formateurId) { + formateurIdToUser.set(user.formateurId, user); + } + }); + + // Combiner les données + return { + users: allUsers, + formateurs: allFormateurs.map(formateur => ({ + ...formateur, + hasAccount: formateurIdToUser.has(formateur.id), + user: formateurIdToUser.get(formateur.id) || null, + })), + }; + }), + + getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { + return db.getUserById(input.id); + }), + + create: adminProcedure.input(z.object({ + openId: z.string().min(1), + name: z.string().optional(), + email: z.string().email().optional(), + username: z.string().min(3).optional(), + password: z.string().min(6).optional(), + role: z.enum(["user", "admin", "formateur"]).default("user"), + isActive: z.boolean().default(true), + formateurId: z.number().optional(), + })).mutation(async ({ input }) => { + await db.createUser(input); + return { success: true }; + }), + + update: adminProcedure.input(z.object({ + id: z.number(), + name: z.string().optional(), + email: z.string().email().optional(), + username: z.string().min(3).optional(), + password: z.string().min(6).optional(), + role: z.enum(["user", "admin", "formateur"]).optional(), + isActive: z.boolean().optional(), + formateurId: z.number().optional(), + })).mutation(async ({ input }) => { + const { id, ...data } = input; + await db.updateUser(id, data); + return { success: true }; + }), + + toggleStatus: adminProcedure.input(z.object({ + id: z.number(), + isActive: z.boolean(), + })).mutation(async ({ input }) => { + await db.toggleUserStatus(input.id, input.isActive); + return { success: true }; + }), + + delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { + await db.deleteUser(input.id); + return { success: true }; + }), + + requestPasswordReset: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { + const user = await db.getUserById(input.id); + if (!user) { + throw new Error("Utilisateur introuvable"); + } + + if (!user.email) { + throw new Error("Cet utilisateur n'a pas d'adresse email"); + } + + // Générer un token unique + const crypto = await import('crypto'); + const token = crypto.randomBytes(32).toString('hex'); + + // Définir l'expiration à 24h + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); + + // Sauvegarder le token en base + await db.createPasswordResetToken(user.id, token, expiresAt); + + // Générer le lien de réinitialisation + // TODO: Remplacer par l'URL réelle de votre application en production + const resetLink = `https://votre-domaine.com/reset-password?token=${token}`; + + // Envoyer l'email + const emailService = await import('./emailService'); + const emailSent = await emailService.sendPasswordResetEmail({ + apprenantEmail: user.email, + apprenantNom: user.name || 'Utilisateur', + apprenantPrenom: '', + resetLink, + }); + + if (!emailSent) { + throw new Error("Échec de l'envoi de l'email"); + } + + return { success: true, message: "Email de réinitialisation envoyé" }; + }), + }), + + // ===== GESTION DES TEMPLATES D'EMAILS ===== + emailTemplates: router({ + list: adminProcedure.query(async () => { + return db.getAllEmailTemplates(); + }), + + getByType: adminProcedure + .input(z.object({ type: z.string() })) + .query(async ({ input }) => { + return db.getEmailTemplateByType(input.type); + }), + + upsert: adminProcedure + .input(z.object({ + type: z.string(), + name: z.string(), + logoUrl: z.string().nullable(), + primaryColor: z.string(), + headerBgColor: z.string(), + headerTextColor: z.string(), + headerTitle: z.string(), + bodyContent: z.string().nullable(), + footerText: z.string().nullable(), + active: z.boolean(), + })) + .mutation(async ({ input }) => { + return db.upsertEmailTemplate(input); + }), + + delete: adminProcedure + .input(z.object({ type: z.string() })) + .mutation(async ({ input }) => { + await db.deleteEmailTemplate(input.type); + return { success: true }; + }), + + initializeDefaults: adminProcedure.mutation(async () => { + await db.initializeDefaultEmailTemplates(); + return { success: true }; + }), + }), + + // ===== GESTION DES RAPPELS ===== + rappels: router({ + list: adminProcedure.query(async () => { + return db.getRappels(); + }), + + create: adminProcedure + .input(z.object({ + nom: z.string(), + templateType: z.enum(["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]), + timing: z.enum(["pre_formation", "post_formation"]).default("pre_formation"), + joursAvant: z.number(), + heureEnvoi: z.string().default("09:00"), + actif: z.boolean(), + dateFormationIds: z.array(z.number()).optional(), + fichier: z.object({ + nomFichier: z.string(), + urlFichier: z.string(), + s3Key: z.string(), + typeFichier: z.string(), + tailleFichier: z.number(), + }).nullable().optional(), + })) + .mutation(async ({ input }) => { + const { dateFormationIds, ...rappelData } = input; + const result: any = await db.createRappel(rappelData); + + // Créer les associations rappel-date si des dates sont spécifiées + if (dateFormationIds && dateFormationIds.length > 0) { + const rappelId = result.insertId || result[0]?.insertId; + if (rappelId) { + await db.createRappelDates(Number(rappelId), dateFormationIds); + } + } + + return result; + }), + + update: adminProcedure + .input(z.object({ + id: z.number(), + nom: z.string().optional(), + templateType: z.enum(["rappel", "rappelJ1", "rappel3", "rappel4", "rappel5", "rappel6"]).optional(), + timing: z.enum(["pre_formation", "post_formation"]).optional(), + joursAvant: z.number().optional(), + heureEnvoi: z.string().optional(), + actif: z.boolean().optional(), + dateFormationIds: z.array(z.number()).optional(), + fichier: z.object({ + nomFichier: z.string(), + urlFichier: z.string(), + s3Key: z.string(), + typeFichier: z.string(), + tailleFichier: z.number(), + }).nullable().optional(), + })) + .mutation(async ({ input }) => { + const { id, dateFormationIds, ...data } = input; + await db.updateRappel(id, data); + + // Mettre à jour les associations rappel-date si spécifiées + if (dateFormationIds !== undefined) { + await db.deleteRappelDates(id); + if (dateFormationIds.length > 0) { + await db.createRappelDates(id, dateFormationIds); + } + } + + return { success: true }; + }), + + delete: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + await db.deleteRappel(input.id); + return { success: true }; + }), + + toggleActif: adminProcedure + .input(z.object({ id: z.number(), actif: z.boolean() })) + .mutation(async ({ input }) => { + await db.updateRappel(input.id, { actif: input.actif }); + return { success: true }; + }), + + envoyerAutomatiques: publicProcedure + .mutation(async () => { + const { processRappelsAutomatiques } = await import('./rappelScheduler'); + const result = await processRappelsAutomatiques(); + return result; + }), + + testerRappel: adminProcedure + .input(z.object({ + sequenceId: z.number(), + typeRappel: z.enum(['rappel', 'rappelJ1']), + })) + .mutation(async ({ input }) => { + const sequence = await db.getSequenceById(input.sequenceId); + if (!sequence) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' }); + } + + const formation = await db.getFormationById(sequence.formationId); + if (!formation) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' }); + } + + // Récupérer le formateur si présent + let formateurNom: string | undefined; + if (sequence.formateurId) { + const formateur = await db.getFormateurById(sequence.formateurId); + formateurNom = formateur?.nom; + } + + // Récupérer les dates de la séquence + const dates = await db.getDatesBySequence(input.sequenceId); + + // Récupérer les inscrits + const inscrits = await db.getInscriptionsBySequence(input.sequenceId); + const inscritsConfirmes = inscrits.filter(i => i.inscription.statut === 'confirmee'); + + if (inscritsConfirmes.length === 0) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Aucun apprenant inscrit confirmé pour cette séquence' }); + } + + // Envoyer le rappel de test au premier inscrit uniquement + const premierInscrit = inscritsConfirmes[0]; + const apprenant = premierInscrit.apprenant; + + if (!apprenant || !apprenant.email) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Apprenant introuvable ou sans email' }); + } + + const { sendRappelJ7Email, sendRappelJ1Email } = await import('./emailService'); + + try { + if (input.typeRappel === 'rappel') { + await sendRappelJ7Email({ + apprenantEmail: apprenant.email, + apprenantPrenom: apprenant.prenom, + apprenantNom: apprenant.nom, + apprenantFonction: apprenant.fonction || '', + formationNom: formation.nom, + sequenceNom: sequence.nom, + dates: dates.map((d: any) => ({ + dateDebut: d.dateDebut, + dateFin: d.dateFin, + ordre: d.ordre + })), + lieu: sequence.lieu || '', + formateur: formateurNom, + }); + } else { + await sendRappelJ1Email({ + apprenantEmail: apprenant.email, + apprenantPrenom: apprenant.prenom, + apprenantNom: apprenant.nom, + apprenantFonction: apprenant.fonction || '', + formationNom: formation.nom, + sequenceNom: sequence.nom, + dates: dates.map((d: any) => ({ + dateDebut: d.dateDebut, + dateFin: d.dateFin, + ordre: d.ordre + })), + lieu: sequence.lieu || '', + formateur: formateurNom, + }); + } + + return { + success: true, + message: `Rappel de test envoyé à ${apprenant.email}`, + destinataire: apprenant.email, + }; + } catch (error: any) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: `Erreur lors de l'envoi: ${error.message}`, + }); + } + }), + + historique: adminProcedure + .input(z.object({ + dateDebut: z.string().optional(), + dateFin: z.string().optional(), + sequenceId: z.number().optional(), + statut: z.enum(['success', 'failed']).optional(), + email: z.string().optional(), + limit: z.number().default(100), + offset: z.number().default(0), + })) + .query(async ({ input }) => { + const { getLogsRappelsWithFilters } = await import('./rappelDb'); + return getLogsRappelsWithFilters({ + dateDebut: input.dateDebut ? new Date(input.dateDebut) : undefined, + dateFin: input.dateFin ? new Date(input.dateFin) : undefined, + sequenceId: input.sequenceId, + statut: input.statut, + email: input.email, + limit: input.limit, + offset: input.offset, + }); + }), + + statistiques: adminProcedure.query(async () => { + const { getStatsRappels } = await import('./rappelDb'); + return getStatsRappels(); + }), + + evolution: adminProcedure.query(async () => { + const { getEvolutionEnvois } = await import('./rappelDb'); + return getEvolutionEnvois(); + }), + }), + + emailConfig: router({ + get: adminProcedure.query(async () => { + return db.getActiveEmailConfig(); + }), + + upsert: adminProcedure + .input(z.object({ + provider: z.enum(["resend", "smtp", "simulation"]), + apiKey: z.string().nullable(), + fromEmail: z.string().email(), + fromName: z.string(), + mode: z.enum(["simulation", "production"]), + domainVerified: z.boolean(), + // Champs SMTP + smtpHost: z.string().nullable().optional(), + smtpPort: z.number().nullable().optional(), + smtpSecure: z.enum(["none", "tls", "ssl"]).nullable().optional(), + smtpUser: z.string().nullable().optional(), + smtpPassword: z.string().nullable().optional(), + })) + .mutation(async ({ input }) => { + await db.upsertEmailConfig(input); + return { success: true }; + }), + + testEmail: adminProcedure + .input(z.object({ + toEmail: z.string().email(), + })) + .mutation(async ({ input }) => { + // Importer le service d'envoi + const { sendEmail } = await import("./_core/emailSender"); + + try { + const success = await sendEmail({ + to: input.toEmail, + subject: "Test d'envoi d'email - Formation Manager Itinova", + html: ` +

    Test réussi !

    +

    Cet email de test a été envoyé avec succès depuis votre configuration SMTP.

    +

    Votre configuration d'envoi d'emails fonctionne correctement.

    + `, + }); + + return { success, message: success ? "Email envoyé avec succès" : "Échec de l'envoi" }; + } catch (error: any) { + return { success: false, message: error.message }; + } + }), + }), + + // ===== PARAMÈTRES DE L'APPLICATION ===== + parametres: router({ + get: adminProcedure.query(async () => { + const { getParametres } = await import("./parametresDb"); + return getParametres(); + }), + + update: adminProcedure + .input(z.object({ + urlPublique: z.string().url().optional(), + delaiExpirationQR: z.number().int().positive().optional(), + dureeValiditeToken: z.number().int().positive().optional(), + notificationsActives: z.boolean().optional(), + })) + .mutation(async ({ input, ctx }) => { + const { updateParametres } = await import("./parametresDb"); + await updateParametres(input, ctx.user.id, ctx.user.name || "Administrateur"); + return { success: true }; + }), + + updateUrlPublique: adminProcedure + .input(z.object({ + urlPublique: z.string().url(), + })) + .mutation(async ({ input, ctx }) => { + const { updateUrlPublique } = await import("./parametresDb"); + await updateUrlPublique(input.urlPublique, ctx.user.id, ctx.user.name || "Administrateur"); + return { success: true }; + }), + + testerUrl: adminProcedure + .input(z.object({ + url: z.string(), + })) + .mutation(async ({ input }) => { + const { testerUrl } = await import("./parametresDb"); + return testerUrl(input.url); + }), + + getHistorique: adminProcedure + .input(z.object({ + limit: z.number().int().positive().default(50), + })) + .query(async ({ input }) => { + const { getHistoriqueParametres } = await import("./parametresDb"); + return getHistoriqueParametres(input.limit); + }), + }), + + // ===== ANALYTICS ===== + analytics: router({ + globalStats: adminProcedure.query(async () => { + return analyticsDb.getGlobalStats(); + }), + + inscriptionsByMonth: adminProcedure + .input(z.object({ + startDate: z.string().optional(), + endDate: z.string().optional(), + })) + .query(async ({ input }) => { + const startDate = input.startDate ? new Date(input.startDate) : undefined; + const endDate = input.endDate ? new Date(input.endDate) : undefined; + return analyticsDb.getInscriptionsByMonth(startDate, endDate); + }), + + tauxRemplissageByMonth: adminProcedure.query(async () => { + return analyticsDb.getTauxRemplissageByMonth(); + }), + + participationByEtablissement: adminProcedure.query(async () => { + return analyticsDb.getParticipationByEtablissement(); + }), + + participationByFonction: adminProcedure.query(async () => { + return analyticsDb.getParticipationByFonction(); + }), + + exportExcel: adminProcedure + .input(z.object({ + startDate: z.string().optional(), + endDate: z.string().optional(), + })) + .mutation(async ({ input }) => { + const startDate = input.startDate ? new Date(input.startDate) : undefined; + const endDate = input.endDate ? new Date(input.endDate) : undefined; + const buffer = await generateAnalyticsExcel(startDate, endDate); + return { + data: buffer.toString('base64'), + filename: `rapport-analytique-${new Date().toISOString().split('T')[0]}.xlsx`, + }; + }), + + exportPDF: adminProcedure + .input(z.object({ + startDate: z.string().optional(), + endDate: z.string().optional(), + })) + .mutation(async ({ input }) => { + const startDate = input.startDate ? new Date(input.startDate) : undefined; + const endDate = input.endDate ? new Date(input.endDate) : undefined; + const buffer = await generateAnalyticsPDF(startDate, endDate); + return { + data: buffer.toString('base64'), + filename: `rapport-analytique-${new Date().toISOString().split('T')[0]}.pdf`, + }; + }), + }), + + // ===== QUESTIONNAIRES ===== + questionnaires: router({ + list: adminProcedure.query(async () => { + const questionnaireDb = await import("./questionnaireDb"); + return questionnaireDb.getAllQuestionnaires(); + }), + + getById: adminProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + return questionnaireDb.getQuestionnaireById(input.id); + }), + + create: adminProcedure + .input(z.object({ + titre: z.string(), + type: z.enum(["satisfaction", "evaluation_pre", "evaluation_post"]), + description: z.string().optional(), + formationId: z.number().nullable().optional(), + actif: z.boolean().default(true), + envoiAutomatique: z.boolean().default(false), + delaiEnvoiJours: z.number().default(1), + })) + .mutation(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + const id = await questionnaireDb.createQuestionnaire(input); + return { id, success: true }; + }), + + update: adminProcedure + .input(z.object({ + id: z.number(), + titre: z.string().optional(), + type: z.enum(["satisfaction", "evaluation_pre", "evaluation_post"]).optional(), + description: z.string().optional(), + formationId: z.number().nullable().optional(), + actif: z.boolean().optional(), + envoiAutomatique: z.boolean().optional(), + delaiEnvoiJours: z.number().optional(), + })) + .mutation(async ({ input }) => { + const { id, ...data } = input; + const questionnaireDb = await import("./questionnaireDb"); + await questionnaireDb.updateQuestionnaire(id, data); + return { success: true }; + }), + + delete: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + await questionnaireDb.deleteQuestionnaire(input.id); + return { success: true }; + }), + + getStats: adminProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + return questionnaireDb.getQuestionnaireStats(input.id); + }), + + getReponsesDetaillees: adminProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + return questionnaireDb.getReponsesDetailleesQuestionnaire(input.id); + }), + + exportExcel: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + const { exportQuestionnaireToExcel } = await import("./questionnaireExport"); + const buffer = await exportQuestionnaireToExcel(input.id); + return { + data: buffer.toString('base64'), + filename: `questionnaire-${input.id}-${new Date().toISOString().split('T')[0]}.xlsx`, + }; + }), + + exportPDF: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + const { exportQuestionnaireToPDF } = await import("./questionnaireExport"); + const buffer = await exportQuestionnaireToPDF(input.id); + return { + data: buffer.toString('base64'), + filename: `questionnaire-${input.id}-${new Date().toISOString().split('T')[0]}.pdf`, + }; + }), + }), + + // ===== QUESTIONS ===== + questions: router({ + list: adminProcedure + .input(z.object({ questionnaireId: z.number() })) + .query(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + return questionnaireDb.getQuestionsByQuestionnaireId(input.questionnaireId); + }), + + create: adminProcedure + .input(z.object({ + questionnaireId: z.number(), + ordre: z.number(), + texte: z.string(), + typeQuestion: z.enum(["choix_multiple", "echelle", "texte_libre", "oui_non"]), + options: z.string().optional(), + echelleMin: z.number().optional(), + echelleMax: z.number().optional(), + echelleLabelMin: z.string().optional(), + echelleLabelMax: z.string().optional(), + obligatoire: z.boolean().default(false), + })) + .mutation(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + const id = await questionnaireDb.createQuestion(input); + return { id, success: true }; + }), + + update: adminProcedure + .input(z.object({ + id: z.number(), + ordre: z.number().optional(), + texte: z.string().optional(), + typeQuestion: z.enum(["choix_multiple", "echelle", "texte_libre", "oui_non"]).optional(), + options: z.string().optional(), + echelleMin: z.number().optional(), + echelleMax: z.number().optional(), + echelleLabelMin: z.string().optional(), + echelleLabelMax: z.string().optional(), + obligatoire: z.boolean().optional(), + })) + .mutation(async ({ input }) => { + const { id, ...data } = input; + const questionnaireDb = await import("./questionnaireDb"); + await questionnaireDb.updateQuestion(id, data); + return { success: true }; + }), + + delete: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + await questionnaireDb.deleteQuestion(input.id); + return { success: true }; + }), + }), + + // ===== RÉPONSES QUESTIONNAIRES (PUBLIC) ===== + reponses: router({ + getByToken: publicProcedure + .input(z.object({ token: z.string() })) + .query(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + const envoi = await questionnaireDb.getEnvoiByToken(input.token); + if (!envoi) return null; + + const questionnaire = await questionnaireDb.getQuestionnaireById(envoi.questionnaireId); + const questions = await questionnaireDb.getQuestionsByQuestionnaireId(envoi.questionnaireId); + + return { + envoi, + questionnaire, + questions, + }; + }), + + submit: publicProcedure + .input(z.object({ + token: z.string(), + reponses: z.array(z.object({ + questionId: z.number(), + reponseNumerique: z.number().optional(), + reponseTexte: z.string().optional(), + })), + })) + .mutation(async ({ input }) => { + const questionnaireDb = await import("./questionnaireDb"); + + // Récupérer l'envoi + const envoi = await questionnaireDb.getEnvoiByToken(input.token); + if (!envoi) throw new Error("Token invalide"); + if (envoi.dateReponse) throw new Error("Questionnaire déjà répondu"); + + // Créer la réponse au questionnaire + const reponseQuestionnaireId = await questionnaireDb.createReponseQuestionnaire({ + questionnaireId: envoi.questionnaireId, + apprenantId: envoi.apprenantId, + sequenceId: envoi.sequenceId, + }); + + // Enregistrer chaque réponse + for (const reponse of input.reponses) { + await questionnaireDb.createReponseQuestion({ + reponseQuestionnaireId, + questionId: reponse.questionId, + reponseNumerique: reponse.reponseNumerique, + reponseTexte: reponse.reponseTexte, + }); + } + + // Mettre à jour l'envoi + await questionnaireDb.updateEnvoiQuestionnaire(envoi.id, { + dateReponse: new Date(), + }); + + // Envoyer une notification à l'administrateur + try { + const { notifyOwner } = await import('./_core/notification'); + const questionnaire = await questionnaireDb.getQuestionnaireById(envoi.questionnaireId); + + await notifyOwner({ + title: 'Nouvelle réponse au questionnaire', + content: `Un apprenant a répondu au questionnaire "${questionnaire?.titre || 'Sans titre'}"`, + }); + } catch (error) { + console.error('[Notification] Erreur lors de l\'envoi de la notification:', error); + // Ne pas bloquer la réponse si la notification échoue + } + + return { success: true }; + }), + + envoyerAutomatiques: adminProcedure + .mutation(async () => { + const { processEnvoisAutomatiques } = await import('./questionnaireScheduler'); + const result = await processEnvoisAutomatiques(); + return result; + }), + }), + + // ===== SUIVI QUESTIONNAIRES ===== + questionnaireSuivi: router({ + statsGlobales: adminProcedure.query(async () => { + const suiviDb = await import("./questionnaireSuiviDb"); + return suiviDb.getStatsGlobales(); + }), + + statsByFormation: adminProcedure.query(async () => { + const suiviDb = await import("./questionnaireSuiviDb"); + return suiviDb.getStatsByFormation(); + }), + + evolutionTemporelle: adminProcedure + .input(z.object({ + startDate: z.date().optional(), + endDate: z.date().optional(), + })) + .query(async ({ input }) => { + const suiviDb = await import("./questionnaireSuiviDb"); + return suiviDb.getEvolutionTemporelle(input.startDate, input.endDate); + }), + + statsByFormateur: adminProcedure.query(async () => { + const suiviDb = await import("./questionnaireSuiviDb"); + return suiviDb.getStatsByFormateur(); + }), + }), + + // ===== ESPACE FORMATEUR ===== + formateur: router({ + calendrier: protectedProcedure + .input(z.object({ + formateurId: z.number(), + dateDebut: z.date().optional(), + dateFin: z.date().optional(), + })) + .query(async ({ input }) => { + const formateurDb = await import("./formateurDb"); + return formateurDb.getCalendrierFormateur(input.formateurId, input.dateDebut, input.dateFin); + }), + + apprenants: protectedProcedure + .input(z.object({ sequenceId: z.number() })) + .query(async ({ input }) => { + const formateurDb = await import("./formateurDb"); + return formateurDb.getApprenantsSequence(input.sequenceId); + }), + + supports: protectedProcedure + .input(z.object({ sequenceId: z.number() })) + .query(async ({ input }) => { + const formateurDb = await import("./formateurDb"); + return formateurDb.getSupportsSequence(input.sequenceId); + }), + + ajouterSupport: protectedProcedure + .input(z.object({ + sequenceId: z.number(), + formateurId: z.number(), + nomFichier: z.string(), + typeFichier: z.string(), + tailleFichier: z.number(), + urlFichier: z.string(), + s3Key: z.string(), + description: z.string().optional(), + })) + .mutation(async ({ input }) => { + const formateurDb = await import("./formateurDb"); + return formateurDb.ajouterSupport(input); + }), + + supprimerSupport: protectedProcedure + .input(z.object({ + supportId: z.number(), + formateurId: z.number(), + })) + .mutation(async ({ input }) => { + const formateurDb = await import("./formateurDb"); + const support = await formateurDb.supprimerSupport(input.supportId, input.formateurId); + + // Note: La suppression du fichier S3 devrait être gérée par un processus de nettoyage séparé + // car il n'y a pas de fonction storageDelete dans l'API actuelle + + return { success: true, support }; + }), + + validerPresence: protectedProcedure + .input(z.object({ + inscriptionId: z.number(), + present: z.boolean(), + })) + .mutation(async ({ input }) => { + const formateurDb = await import("./formateurDb"); + await formateurDb.validerPresence(input.inscriptionId, input.present); + return { success: true }; + }), + + historique: protectedProcedure + .input(z.object({ + formateurId: z.number(), + limit: z.number().optional(), + })) + .query(async ({ input }) => { + const formateurDb = await import("./formateurDb"); + return formateurDb.getHistoriqueFormateur(input.formateurId, input.limit); + }), + + detailSequence: protectedProcedure + .input(z.object({ + sequenceId: z.number(), + formateurId: z.number(), + })) + .query(async ({ input }) => { + const formateurDb = await import("./formateurDb"); + return formateurDb.getDetailSequence(input.sequenceId, input.formateurId); + }), + }), + + // ===== ÉTABLISSEMENTS ===== + etablissements: router({ + list: adminProcedure.query(async () => { + const { getEtablissementsStats } = await import("./etablissementsDb"); + return getEtablissementsStats(); + }), + + getDetail: adminProcedure + .input(z.object({ codeEtablissement: z.string() })) + .query(async ({ input }) => { + const { getEtablissementDetail } = await import("./etablissementsDb"); + return getEtablissementDetail(input.codeEtablissement); + }), + }), + + // ===== NOTIFICATIONS ===== + notifications: router({ + envoyerRemerciements: adminProcedure + .input(z.object({ sequenceId: z.number() })) + .mutation(async ({ input }) => { + const { envoyerRemerciementsSequence } = await import("./remerciementScheduler"); + return envoyerRemerciementsSequence(input.sequenceId); + }), + + envoyerRemerciementsAutomatiques: adminProcedure + .mutation(async () => { + const { processRemerciementsAutomatiques } = await import("./remerciementScheduler"); + await processRemerciementsAutomatiques(); + return { success: true }; + }), + + // Historique des notifications + historique: adminProcedure + .input(z.object({ + type: z.enum(["remerciement", "notification_formateur_inscription", "notification_formateur_annulation", "alerte_capacite", "notification_liste_attente"]).optional(), + dateDebut: z.string().optional(), + dateFin: z.string().optional(), + statut: z.enum(["success", "failed"]).optional(), + email: z.string().optional(), + sequenceId: z.number().optional(), + limit: z.number().default(50), + offset: z.number().default(0), + })) + .query(async ({ input }) => { + const { getNotificationLogs } = await import("./notificationLogsDb"); + return getNotificationLogs({ + type: input.type, + dateDebut: input.dateDebut ? new Date(input.dateDebut) : undefined, + dateFin: input.dateFin ? new Date(input.dateFin) : undefined, + statut: input.statut, + email: input.email, + sequenceId: input.sequenceId, + limit: input.limit, + offset: input.offset, + }); + }), + + // Statistiques des notifications + statistiques: adminProcedure + .input(z.object({ + dateDebut: z.string().optional(), + dateFin: z.string().optional(), + })) + .query(async ({ input }) => { + const { getNotificationStats } = await import("./notificationLogsDb"); + return getNotificationStats({ + dateDebut: input.dateDebut ? new Date(input.dateDebut) : undefined, + dateFin: input.dateFin ? new Date(input.dateFin) : undefined, + }); + }), + }), + + // Attestations de formation + attestations: router({ + // Générer une attestation pour une inscription + generer: adminProcedure + .input(z.object({ + inscriptionId: z.number(), + })) + .mutation(async ({ input }) => { + const { genererAttestationPDF, enregistrerAttestation, getAttestation } = await import("./attestationService"); + const db = await import("./db").then(m => m.getDb()); + if (!db) throw new Error("Base de données non disponible"); + + // Vérifier si l'attestation existe déjà + const { inscriptions } = await import("../drizzle/schema"); + const { eq } = await import("drizzle-orm"); + const [inscription] = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1); + if (!inscription) { + throw new Error("Inscription introuvable"); + } + + const attestationExistante = await getAttestation(inscription.apprenantId, inscription.sequenceId); + if (attestationExistante) { + return { + success: true, + attestation: attestationExistante, + message: "Attestation déjà générée", + }; + } + + // Générer le PDF + const { s3Key, pdfUrl } = await genererAttestationPDF(input.inscriptionId); + + // Enregistrer dans la base + const attestationId = await enregistrerAttestation( + input.inscriptionId, + inscription.apprenantId, + inscription.sequenceId, + s3Key, + pdfUrl + ); + + const attestation = await getAttestation(inscription.apprenantId, inscription.sequenceId); + + return { + success: true, + attestation, + message: "Attestation générée avec succès", + }; + }), + + // 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 { getAttestation } = await import("./attestationService"); + return getAttestation(input.apprenantId, input.sequenceId); + }), + + // Récupérer toutes les attestations d'un apprenant + listApprenant: publicProcedure + .input(z.object({ + apprenantId: z.number(), + })) + .query(async ({ input }) => { + const { getAttestationsApprenant } = await import("./attestationService"); + return getAttestationsApprenant(input.apprenantId); + }), + + // Récupérer la configuration des attestations + getConfig: adminProcedure + .query(async () => { + const { getOrCreateConfigAttestation } = await import("./attestationService"); + return getOrCreateConfigAttestation(); + }), + + // Mettre à jour la configuration des attestations + updateConfig: adminProcedure + .input(z.object({ + nomSignataire: z.string().optional(), + fonctionSignataire: z.string().optional(), + texteAttestation: z.string().optional(), + logoUrl: z.string().optional(), + logoS3Key: z.string().optional(), + signatureUrl: z.string().optional(), + signatureS3Key: z.string().optional(), + })) + .mutation(async ({ input }) => { + const { updateConfigAttestation } = await import("./attestationService"); + await updateConfigAttestation(input); + return { success: true }; + }), + + // Récupérer l'historique des envois d'attestations + historique: adminProcedure + .query(async () => { + const { getHistoriqueAttestations } = await import("./attestationsDb"); + return getHistoriqueAttestations(); + }), + + // Récupérer l'historique pour une séquence spécifique + historiqueBySequence: adminProcedure + .input(z.object({ + sequenceId: z.number(), + })) + .query(async ({ input }) => { + const { getHistoriqueAttestationsBySequence } = await import("./attestationsDb"); + return getHistoriqueAttestationsBySequence(input.sequenceId); + }), + + // Générer une prévisualisation du modèle d'attestation + previsualiser: adminProcedure + .mutation(async () => { + const { genererPreviewAttestation } = await import("./attestationService"); + return genererPreviewAttestation(); + }), + }), + + // ===== PRESENCES (ÉMARGEMENT NUMÉRIQUE) ===== + presences: router({ + // Générer un QR code pour une séquence + generateQRCode: protectedProcedure + .input(z.object({ + sequenceId: z.number(), + })) + .mutation(async ({ input, ctx }) => { + const { generateQRToken, generateQRCodeDataURL } = await import("./qrCodeGenerator"); + const { getDb } = await import("./db"); + const { sequences } = await import("../drizzle/schema"); + const { eq } = await import("drizzle-orm"); + + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Vérifier que la séquence existe + const sequence = await db.select().from(sequences).where(eq(sequences.id, input.sequenceId)).limit(1); + if (sequence.length === 0) { + throw new Error("Séquence introuvable"); + } + + // Générer un nouveau token si nécessaire + let token = sequence[0].qrCodeToken; + if (!token) { + token = generateQRToken(); + await db.update(sequences).set({ qrCodeToken: token }).where(eq(sequences.id, input.sequenceId)); + } + + // Générer le QR code + const qrCodeDataURL = await generateQRCodeDataURL(token); + + return { + token, + qrCodeDataURL, + }; + }), + + // Valider une présence (scan QR code ou manuel) + valider: publicProcedure + .input(z.object({ + token: z.string().optional(), + inscriptionId: z.number(), + dateFormationId: z.number(), + modeValidation: z.enum(["qrcode", "manuel"]), + validateurId: z.number().optional(), + commentaire: z.string().optional(), + })) + .mutation(async ({ input }) => { + const { validerPresence } = await import("./presenceDb"); + const { getDb } = await import("./db"); + const { sequences, inscriptions } = await import("../drizzle/schema"); + const { eq } = await import("drizzle-orm"); + + // Si mode QR code, vérifier le token + if (input.modeValidation === "qrcode" && input.token) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Récupérer l'inscription pour vérifier la séquence + const inscription = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1); + if (inscription.length === 0) { + throw new Error("Inscription introuvable"); + } + + // Vérifier que le token correspond à la séquence + const sequence = await db.select().from(sequences).where(eq(sequences.id, inscription[0].sequenceId)).limit(1); + if (sequence.length === 0 || sequence[0].qrCodeToken !== input.token) { + throw new Error("QR code invalide"); + } + } + + const result = await validerPresence({ + inscriptionId: input.inscriptionId, + dateFormationId: input.dateFormationId, + modeValidation: input.modeValidation, + validateurId: input.validateurId, + commentaire: input.commentaire, + }); + + // Vérifier si toutes les présences sont validées + const { checkAllPresencesValidated } = await import("./presenceDb"); + const allValidated = await checkAllPresencesValidated(input.inscriptionId); + + // Si toutes les présences sont validées, générer l'attestation automatiquement + if (allValidated) { + try { + const { genererAttestationPDF, enregistrerAttestation } = await import("./attestationService"); + + // Générer le PDF + const { s3Key, pdfUrl } = await genererAttestationPDF(input.inscriptionId); + + // Récupérer les données de l'inscription + const db = await getDb(); + if (db) { + const inscriptionData = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1); + if (inscriptionData.length > 0) { + // Enregistrer l'attestation en base + await enregistrerAttestation( + input.inscriptionId, + inscriptionData[0].apprenantId, + inscriptionData[0].sequenceId, + s3Key, + pdfUrl + ); + } + } + + return { + ...result, + attestationGenerated: true, + attestationUrl: pdfUrl, + }; + } catch (error) { + console.error("Erreur lors de la génération de l'attestation:", error); + // Ne pas bloquer la validation de présence si l'attestation échoue + } + } + + return result; + }), + + // Lister les présences pour une séquence + listBySequence: protectedProcedure + .input(z.object({ + sequenceId: z.number(), + })) + .query(async ({ input }) => { + const { getPresencesBySequence } = await import("./presenceDb"); + return getPresencesBySequence(input.sequenceId); + }), + + // Lister les présences pour une inscription + listByInscription: publicProcedure + .input(z.object({ + inscriptionId: z.number(), + })) + .query(async ({ input }) => { + const { getPresencesByInscription } = await import("./presenceDb"); + return getPresencesByInscription(input.inscriptionId); + }), + + // Supprimer une présence + delete: protectedProcedure + .input(z.object({ + presenceId: z.number(), + })) + .mutation(async ({ input }) => { + const { supprimerPresence } = await import("./presenceDb"); + return supprimerPresence(input.presenceId); + }), + + // Vérifier si toutes les présences sont validées pour une inscription + checkAllValidated: publicProcedure + .input(z.object({ + inscriptionId: z.number(), + })) + .query(async ({ input }) => { + const { checkAllPresencesValidated } = await import("./presenceDb"); + return checkAllPresencesValidated(input.inscriptionId); + }), + }), + + // ===== GESTION DES ATTESTATIONS ===== + gestionAttestations: router({ + // Récupérer la configuration d'une formation + getFormationConfig: adminProcedure + .input(z.object({ formationId: z.number() })) + .query(async ({ input }) => { + const { getFormationConfig } = await import("./gestionAttestationsDb"); + return getFormationConfig(input.formationId); + }), + + // Mettre à jour la configuration d'une formation + updateFormationConfig: adminProcedure + .input(z.object({ + formationId: z.number(), + modeAttestation: z.enum(["auto", "manuel"]), + modeEnvoi: z.enum(["auto", "manuel"]), + })) + .mutation(async ({ input }) => { + const { updateFormationConfig } = await import("./gestionAttestationsDb"); + return updateFormationConfig( + input.formationId, + input.modeAttestation, + input.modeEnvoi + ); + }), + + // Récupérer les apprenants avec leur statut d'attestation + getApprenantsWithStatus: adminProcedure + .input(z.object({ formationId: z.number() })) + .query(async ({ input }) => { + try { + const { getApprenantsWithAttestationStatus } = await import("./gestionAttestationsDb"); + const results = await getApprenantsWithAttestationStatus(input.formationId); + console.log("[gestionAttestations] Found", results.length, "apprenants for formation", input.formationId); + return results; + } catch (error) { + console.error("[gestionAttestations] Error fetching apprenants:", error); + throw error; + } + }), + + // Uploader un document d'attestation + uploadDocument: adminProcedure + .input(z.object({ + inscriptionId: z.number(), + documentUrl: z.string(), + documentS3Key: z.string(), + })) + .mutation(async ({ input, ctx }) => { + const { uploadAttestationDocument } = await import("./gestionAttestationsDb"); + return uploadAttestationDocument( + input.inscriptionId, + input.documentUrl, + input.documentS3Key, + ctx.user.id + ); + }), + + // Supprimer un document d'attestation + deleteDocument: adminProcedure + .input(z.object({ attestationId: z.number() })) + .mutation(async ({ input }) => { + const { deleteAttestationDocument } = await import("./gestionAttestationsDb"); + return deleteAttestationDocument(input.attestationId); + }), + + // Envoyer une attestation par email + sendAttestation: adminProcedure + .input(z.object({ + attestationId: z.number(), + apprenantEmail: z.string(), + apprenantNom: z.string(), + apprenantPrenom: z.string(), + formationNom: z.string(), + pdfUrl: z.string(), + })) + .mutation(async ({ input }) => { + const { sendAttestationEmail } = await import("./emailService"); + const { markAttestationAsSent } = await import("./gestionAttestationsDb"); + + // Envoyer l'email + await sendAttestationEmail({ + to: input.apprenantEmail, + apprenantNom: input.apprenantNom, + apprenantPrenom: input.apprenantPrenom, + formationNom: input.formationNom, + pdfUrl: input.pdfUrl, + }); + + // Marquer comme envoyé + await markAttestationAsSent(input.attestationId); + + return { success: true }; + }), + }), +}); + +export type AppRouter = typeof appRouter; diff --git a/deployment-package/source_server/storage.ts b/deployment-package/source_server/storage.ts new file mode 100644 index 0000000..6cdf862 --- /dev/null +++ b/deployment-package/source_server/storage.ts @@ -0,0 +1,102 @@ +// Preconfigured storage helpers for Manus WebDev templates +// Uses the Biz-provided storage proxy (Authorization: Bearer ) + +import { ENV } from './_core/env'; + +type StorageConfig = { baseUrl: string; apiKey: string }; + +function getStorageConfig(): StorageConfig { + const baseUrl = ENV.forgeApiUrl; + const apiKey = ENV.forgeApiKey; + + if (!baseUrl || !apiKey) { + throw new Error( + "Storage proxy credentials missing: set BUILT_IN_FORGE_API_URL and BUILT_IN_FORGE_API_KEY" + ); + } + + return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey }; +} + +function buildUploadUrl(baseUrl: string, relKey: string): URL { + const url = new URL("v1/storage/upload", ensureTrailingSlash(baseUrl)); + url.searchParams.set("path", normalizeKey(relKey)); + return url; +} + +async function buildDownloadUrl( + baseUrl: string, + relKey: string, + apiKey: string +): Promise { + const downloadApiUrl = new URL( + "v1/storage/downloadUrl", + ensureTrailingSlash(baseUrl) + ); + downloadApiUrl.searchParams.set("path", normalizeKey(relKey)); + const response = await fetch(downloadApiUrl, { + method: "GET", + headers: buildAuthHeaders(apiKey), + }); + return (await response.json()).url; +} + +function ensureTrailingSlash(value: string): string { + return value.endsWith("/") ? value : `${value}/`; +} + +function normalizeKey(relKey: string): string { + return relKey.replace(/^\/+/, ""); +} + +function toFormData( + data: Buffer | Uint8Array | string, + contentType: string, + fileName: string +): FormData { + const blob = + typeof data === "string" + ? new Blob([data], { type: contentType }) + : new Blob([data as any], { type: contentType }); + const form = new FormData(); + form.append("file", blob, fileName || "file"); + return form; +} + +function buildAuthHeaders(apiKey: string): HeadersInit { + return { Authorization: `Bearer ${apiKey}` }; +} + +export async function storagePut( + relKey: string, + data: Buffer | Uint8Array | string, + contentType = "application/octet-stream" +): Promise<{ key: string; url: string }> { + const { baseUrl, apiKey } = getStorageConfig(); + const key = normalizeKey(relKey); + const uploadUrl = buildUploadUrl(baseUrl, key); + const formData = toFormData(data, contentType, key.split("/").pop() ?? key); + const response = await fetch(uploadUrl, { + method: "POST", + headers: buildAuthHeaders(apiKey), + body: formData, + }); + + if (!response.ok) { + const message = await response.text().catch(() => response.statusText); + throw new Error( + `Storage upload failed (${response.status} ${response.statusText}): ${message}` + ); + } + const url = (await response.json()).url; + return { key, url }; +} + +export async function storageGet(relKey: string): Promise<{ key: string; url: string; }> { + const { baseUrl, apiKey } = getStorageConfig(); + const key = normalizeKey(relKey); + return { + key, + url: await buildDownloadUrl(baseUrl, key, apiKey), + }; +} diff --git a/deployment-package/update.sh b/deployment-package/update.sh deleted file mode 100755 index cdefd10..0000000 --- a/deployment-package/update.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash - -############################################################################### -# Script de mise à jour rapide - Formation Manager Itinova -# Version: 7a737c05 -# Auteur: Manus AI -# Description: Script simplifié pour mettre à jour l'application en production -############################################################################### - -set -e - -# Couleurs -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Configuration -APP_DIR="/var/www/formation-manager-itinova" -SERVICE_NAME="formation-manager" - -print_info() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -print_success() { - echo -e "${GREEN}[OK]${NC} $1" -} - -print_error() { - echo -e "${RED}[ERREUR]${NC} $1" -} - -# Vérifier les privilèges sudo -if [[ $EUID -ne 0 ]]; then - print_error "Ce script doit être exécuté avec sudo" - exit 1 -fi - -echo "========================================" -echo " Mise à jour Formation Manager Itinova" -echo " Version: 7a737c05" -echo "========================================" -echo "" - -cd "$APP_DIR" - -# 1. Arrêter le service -print_info "Arrêt du service..." -systemctl stop "$SERVICE_NAME" -print_success "Service arrêté" - -# 2. Sauvegarder .env -print_info "Sauvegarde de la configuration..." -cp .env /tmp/.env.backup -print_success "Configuration sauvegardée" - -# 3. Mettre à jour le code -print_info "Mise à jour du code..." -git pull origin main -print_success "Code mis à jour" - -# 4. Installer les dépendances -print_info "Installation des dépendances..." -pnpm install -print_success "Dépendances installées" - -# 5. Appliquer les migrations -print_info "Application des migrations..." -pnpm db:push -print_success "Migrations appliquées" - -# 6. Build -print_info "Compilation de l'application..." -pnpm run build -print_success "Application compilée" - -# 7. Restaurer .env -print_info "Restauration de la configuration..." -cp /tmp/.env.backup .env -print_success "Configuration restaurée" - -# 8. Redémarrer le service -print_info "Redémarrage du service..." -systemctl start "$SERVICE_NAME" -sleep 3 - -if systemctl is-active --quiet "$SERVICE_NAME"; then - print_success "Service redémarré" -else - print_error "Échec du redémarrage" - systemctl status "$SERVICE_NAME" - exit 1 -fi - -echo "" -echo "========================================" -print_success "Mise à jour terminée avec succès !" -echo "========================================" -echo "" -print_info "Vérifiez les logs: sudo journalctl -u $SERVICE_NAME -f" diff --git a/fix-all-database-schema-FINAL.sh b/fix-all-database-schema-FINAL.sh new file mode 100755 index 0000000..b06ba72 --- /dev/null +++ b/fix-all-database-schema-FINAL.sh @@ -0,0 +1,281 @@ +#!/bin/bash + +# Script de correction COMPLÈTE et DÉFINITIVE du schéma de base de données +# Formation Manager Itinova - Janvier 2026 +# Ce script corrige TOUTES les colonnes manquantes identifiées + +DB_USER="app_user" +DB_PASS="Itinova69!" +DB_NAME="formation_manager" + +echo "==========================================" +echo "CORRECTION DÉFINITIVE DU SCHÉMA" +echo "Formation Manager Itinova" +echo "==========================================" +echo "" +echo "⚠️ Ce script va MODIFIER la base de données" +echo "Appuyez sur Entrée pour continuer ou Ctrl+C pour annuler..." +read + +echo "" +echo "=== DÉBUT DES CORRECTIONS ===" +echo "" + +# Fonction pour exécuter une requête SQL avec gestion d'erreur +execute_sql() { + local description="$1" + local query="$2" + echo "→ $description" + result=$(mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "$query" 2>&1 | grep -v "Warning: Using a password") + if [ -z "$result" ]; then + echo " ✓ OK" + return 0 + else + if echo "$result" | grep -q "Duplicate column name"; then + echo " ⚠ Colonne existe déjà" + return 0 + elif echo "$result" | grep -q "Can't DROP"; then + echo " ⚠ Colonne n'existe pas" + return 0 + else + echo " ✗ Erreur: $result" + return 1 + fi + fi +} + +echo "==========================================" +echo "1. CORRECTION DE LA TABLE formateurs" +echo "==========================================" + +# CRITIQUE: La colonne email manque dans formateurs +execute_sql "Ajout de la colonne email" \ +"ALTER TABLE formateurs ADD COLUMN email VARCHAR(320) AFTER nom;" + +echo "" +echo "==========================================" +echo "2. CORRECTION DE LA TABLE rappels" +echo "==========================================" + +# CRITIQUE: La colonne timing manque dans rappels +execute_sql "Ajout de la colonne timing" \ +"ALTER TABLE rappels ADD COLUMN timing ENUM('pre_formation', 'post_formation') NOT NULL DEFAULT 'pre_formation' AFTER heureEnvoi;" + +execute_sql "Ajout de la colonne nomFichier" \ +"ALTER TABLE rappels ADD COLUMN nomFichier VARCHAR(255) AFTER timing;" + +execute_sql "Ajout de la colonne urlFichier" \ +"ALTER TABLE rappels ADD COLUMN urlFichier VARCHAR(500) AFTER nomFichier;" + +execute_sql "Ajout de la colonne s3Key" \ +"ALTER TABLE rappels ADD COLUMN s3Key VARCHAR(500) AFTER urlFichier;" + +execute_sql "Ajout de la colonne typeFichier" \ +"ALTER TABLE rappels ADD COLUMN typeFichier VARCHAR(100) AFTER s3Key;" + +execute_sql "Ajout de la colonne tailleFichier" \ +"ALTER TABLE rappels ADD COLUMN tailleFichier INT AFTER typeFichier;" + +execute_sql "Ajout de la colonne derniereExecution" \ +"ALTER TABLE rappels ADD COLUMN derniereExecution DATETIME AFTER actif;" + +execute_sql "Modification de l'ENUM templateType" \ +"ALTER TABLE rappels MODIFY COLUMN templateType ENUM('rappel', 'rappelJ1', 'rappel3', 'rappel4', 'rappel5', 'rappel6') NOT NULL;" + +echo "" +echo "==========================================" +echo "3. CORRECTION DE LA TABLE sequences" +echo "==========================================" + +execute_sql "Modification de l'ENUM statut" \ +"ALTER TABLE sequences MODIFY COLUMN statut ENUM('ouverte','bloquee','terminee','fermee','annulee') NOT NULL DEFAULT 'ouverte';" + +execute_sql "Modification de l'ENUM publicCible" \ +"ALTER TABLE sequences MODIFY COLUMN publicCible ENUM('directeur','chef_service','autre','tous') NOT NULL DEFAULT 'tous';" + +execute_sql "Modification de la taille de lieu" \ +"ALTER TABLE sequences MODIFY COLUMN lieu VARCHAR(255) NOT NULL;" + +execute_sql "Modification de la taille de qrCodeToken" \ +"ALTER TABLE sequences MODIFY COLUMN qrCodeToken VARCHAR(100) UNIQUE;" + +echo "" +echo "==========================================" +echo "4. CORRECTION DE LA TABLE formations" +echo "==========================================" + +execute_sql "Ajout de la colonne modeAttestation" \ +"ALTER TABLE formations ADD COLUMN modeAttestation ENUM('auto', 'manuel') NOT NULL DEFAULT 'auto' AFTER actif;" + +execute_sql "Ajout de la colonne modeEnvoi" \ +"ALTER TABLE formations ADD COLUMN modeEnvoi ENUM('auto', 'manuel') NOT NULL DEFAULT 'manuel' AFTER modeAttestation;" + +echo "" +echo "==========================================" +echo "5. CORRECTION DE LA TABLE users" +echo "==========================================" + +execute_sql "Ajout de la colonne username" \ +"ALTER TABLE users ADD COLUMN username VARCHAR(100) UNIQUE AFTER email;" + +execute_sql "Ajout de la colonne password" \ +"ALTER TABLE users ADD COLUMN password VARCHAR(255) AFTER username;" + +execute_sql "Ajout de la colonne isActive" \ +"ALTER TABLE users ADD COLUMN isActive BOOLEAN NOT NULL DEFAULT TRUE AFTER role;" + +execute_sql "Ajout de la colonne formateurId" \ +"ALTER TABLE users ADD COLUMN formateurId INT AFTER isActive;" + +execute_sql "Modification de l'ENUM role" \ +"ALTER TABLE users MODIFY COLUMN role ENUM('user', 'admin', 'formateur') NOT NULL DEFAULT 'user';" + +echo "" +echo "==========================================" +echo "6. CORRECTION DE LA TABLE apprenants" +echo "==========================================" + +execute_sql "Modification de codeEtablissement" \ +"ALTER TABLE apprenants MODIFY COLUMN codeEtablissement VARCHAR(50) NULL;" + +execute_sql "Modification de fonction" \ +"ALTER TABLE apprenants MODIFY COLUMN fonction VARCHAR(100) NULL;" + +echo "" +echo "==========================================" +echo "7. CORRECTION DE LA TABLE emailConfig" +echo "==========================================" + +execute_sql "Modification de l'ENUM provider" \ +"ALTER TABLE emailConfig MODIFY COLUMN provider ENUM('resend', 'smtp', 'simulation') NOT NULL DEFAULT 'simulation';" + +echo "" +echo "==========================================" +echo "8. CORRECTION DE LA TABLE emailTemplates" +echo "==========================================" + +execute_sql "Modification de l'ENUM type" \ +"ALTER TABLE emailTemplates MODIFY COLUMN type ENUM('inscription', 'teaser', 'rappel', 'rappelJ1', 'rappel3', 'rappel4', 'rappel5', 'rappel6') NOT NULL UNIQUE;" + +echo "" +echo "==========================================" +echo "9. CORRECTION DE LA TABLE logsRappels" +echo "==========================================" + +execute_sql "Ajout de la colonne email" \ +"ALTER TABLE logsRappels ADD COLUMN email VARCHAR(320) NOT NULL AFTER apprenantId;" + +execute_sql "Modification de l'ENUM type" \ +"ALTER TABLE logsRappels MODIFY COLUMN type ENUM('rappel', 'rappelJ1', 'rappel3', 'rappel4', 'rappel5', 'rappel6') NOT NULL;" + +echo "" +echo "==========================================" +echo "10. CORRECTION DE LA TABLE questionnaires" +echo "==========================================" + +# Vérifier si la colonne nom existe et la renommer en titre +result=$(mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='questionnaires' AND COLUMN_NAME='nom';" 2>/dev/null) + +if [ "$result" = "1" ]; then + execute_sql "Renommage de nom en titre" \ + "ALTER TABLE questionnaires CHANGE COLUMN nom titre VARCHAR(255) NOT NULL;" +else + echo "→ La colonne titre existe déjà" +fi + +echo "" +echo "==========================================" +echo "11. CORRECTION DE LA TABLE reponsesQuestionnaires" +echo "==========================================" + +# Vérifier si la colonne complete existe et la supprimer +result=$(mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='reponsesQuestionnaires' AND COLUMN_NAME='complete';" 2>/dev/null) + +if [ "$result" = "1" ]; then + execute_sql "Suppression de la colonne complete" \ + "ALTER TABLE reponsesQuestionnaires DROP COLUMN complete;" +else + echo "→ La colonne complete n'existe pas (OK)" +fi + +echo "" +echo "==========================================" +echo "12. CORRECTION DE LA TABLE attestations" +echo "==========================================" + +execute_sql "Ajout de la colonne documentUrl" \ +"ALTER TABLE attestations ADD COLUMN documentUrl VARCHAR(500) AFTER s3Key;" + +execute_sql "Ajout de la colonne documentS3Key" \ +"ALTER TABLE attestations ADD COLUMN documentS3Key VARCHAR(500) AFTER documentUrl;" + +execute_sql "Ajout de la colonne uploadedBy" \ +"ALTER TABLE attestations ADD COLUMN uploadedBy INT AFTER documentS3Key;" + +execute_sql "Ajout de la colonne uploadedAt" \ +"ALTER TABLE attestations ADD COLUMN uploadedAt TIMESTAMP AFTER uploadedBy;" + +execute_sql "Ajout de la colonne emailEnvoye" \ +"ALTER TABLE attestations ADD COLUMN emailEnvoye BOOLEAN NOT NULL DEFAULT FALSE AFTER uploadedAt;" + +execute_sql "Ajout de la colonne dateEnvoiEmail" \ +"ALTER TABLE attestations ADD COLUMN dateEnvoiEmail TIMESTAMP AFTER emailEnvoye;" + +echo "" +echo "==========================================" +echo "13. CORRECTION DE LA TABLE parametres" +echo "==========================================" + +# Supprimer envoiAutomatiqueAttestations si elle existe +result=$(mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='parametres' AND COLUMN_NAME='envoiAutomatiqueAttestations';" 2>/dev/null) + +if [ "$result" = "1" ]; then + execute_sql "Suppression de la colonne envoiAutomatiqueAttestations" \ + "ALTER TABLE parametres DROP COLUMN envoiAutomatiqueAttestations;" +else + echo "→ La colonne envoiAutomatiqueAttestations n'existe pas (OK)" +fi + +execute_sql "Ajout de la colonne urlPublique" \ +"ALTER TABLE parametres ADD COLUMN urlPublique VARCHAR(500) NOT NULL DEFAULT 'https://formations.itinova.org' AFTER id;" + +execute_sql "Ajout de la colonne delaiExpirationQR" \ +"ALTER TABLE parametres ADD COLUMN delaiExpirationQR INT NOT NULL DEFAULT 30 AFTER urlPublique;" + +execute_sql "Ajout de la colonne dureeValiditeToken" \ +"ALTER TABLE parametres ADD COLUMN dureeValiditeToken INT NOT NULL DEFAULT 24 AFTER delaiExpirationQR;" + +execute_sql "Ajout de la colonne notificationsActives" \ +"ALTER TABLE parametres ADD COLUMN notificationsActives BOOLEAN NOT NULL DEFAULT TRUE AFTER dureeValiditeToken;" + +echo "" +echo "==========================================" +echo "14. CORRECTION DES AUTRES TABLES" +echo "==========================================" + +execute_sql "Ajout de userId dans passwordResetTokens" \ +"ALTER TABLE passwordResetTokens ADD COLUMN userId INT NOT NULL AFTER id;" + +execute_sql "Ajout de ordre dans datesFormation" \ +"ALTER TABLE datesFormation ADD COLUMN ordre INT NOT NULL AFTER sequenceId;" + +execute_sql "Ajout de validateurId dans presences" \ +"ALTER TABLE presences ADD COLUMN validateurId INT AFTER modeValidation;" + +echo "" +echo "==========================================" +echo "✅ TOUTES LES CORRECTIONS APPLIQUÉES" +echo "==========================================" +echo "" + +# Compter les tables +total_tables=$(mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='$DB_NAME';" 2>/dev/null) + +echo "✓ Nombre total de tables: $total_tables" +echo "" +echo "=== PROCHAINES ÉTAPES ===" +echo "1. Redémarrer l'application: pm2 restart formation-manager" +echo "2. Vérifier les logs: pm2 logs formation-manager --lines 30" +echo "3. Tester l'application dans le navigateur" +echo "" +echo "==========================================" diff --git a/formation-manager-itinova-20260113_043521.tar.gz b/formation-manager-itinova-20260113_043521.tar.gz new file mode 100644 index 0000000..2f236aa Binary files /dev/null and b/formation-manager-itinova-20260113_043521.tar.gz differ diff --git a/formation-manager-itinova-20260113_043521.tar.gz.sha256 b/formation-manager-itinova-20260113_043521.tar.gz.sha256 new file mode 100644 index 0000000..be783da --- /dev/null +++ b/formation-manager-itinova-20260113_043521.tar.gz.sha256 @@ -0,0 +1 @@ +e406b674c739c15d5f7eb50042fb115a19c7ce4961c612d14ace016a9575fdee formation-manager-itinova-20260113_043521.tar.gz diff --git a/formation-manager-itinova-deployment-7a737c05.tar.gz b/formation-manager-itinova-deployment-7a737c05.tar.gz deleted file mode 100644 index 5090bf4..0000000 Binary files a/formation-manager-itinova-deployment-7a737c05.tar.gz and /dev/null differ diff --git a/formation-manager-itinova-update-20260113_082603.tar.gz b/formation-manager-itinova-update-20260113_082603.tar.gz new file mode 100644 index 0000000..4a7dfb0 Binary files /dev/null and b/formation-manager-itinova-update-20260113_082603.tar.gz differ diff --git a/formation-manager-update.tar.gz b/formation-manager-update.tar.gz new file mode 100644 index 0000000..eb89580 Binary files /dev/null and b/formation-manager-update.tar.gz differ diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..f775a6a --- /dev/null +++ b/migrations/README.md @@ -0,0 +1,79 @@ +# Migrations SQL + +Ce répertoire contient les migrations SQL versionnées pour la base de données Formation Manager Itinova. + +## Format des fichiers + +Les fichiers de migration doivent suivre ce format de nommage : +``` +YYYYMMDD-description.sql +``` + +Exemples : +- `20260113-fix-schema.sql` +- `20260120-add-notifications.sql` +- `20260201-update-rappels.sql` + +## Ordre d'application + +Les migrations sont appliquées dans l'ordre alphabétique (donc chronologique grâce au format de date). + +## Syntaxe + +Utilisez la syntaxe MySQL compatible avec les versions 5.7+. + +Pour les opérations ALTER TABLE, utilisez `IF NOT EXISTS` ou `IF EXISTS` quand c'est supporté, ou gérez les erreurs dans le script de mise à jour. + +## Exemple de migration + +```sql +-- Migration YYYYMMDD: Description courte +-- Description détaillée de ce que fait cette migration + +-- Ajouter une nouvelle table +CREATE TABLE IF NOT EXISTS nouvelle_table ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(255) NOT NULL, + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Modifier une table existante +ALTER TABLE users +ADD COLUMN IF NOT EXISTS nouveau_champ VARCHAR(100); + +-- Ajouter un index +ALTER TABLE users +ADD INDEX IF NOT EXISTS idx_users_nouveau_champ (nouveau_champ); + +-- Insérer des données par défaut +INSERT IGNORE INTO nouvelle_table (nom) VALUES ('Valeur par défaut'); +``` + +## Suivi des migrations + +Le script de mise à jour crée automatiquement une table `schema_migrations` qui enregistre : +- La version (nom du fichier sans .sql) +- La date d'application +- Une description + +## Créer une nouvelle migration + +1. Créez un fichier avec le format de nommage correct +2. Écrivez votre migration SQL +3. Testez-la sur une base de données de développement +4. Incluez-la dans le package de déploiement + +## Rollback + +Pour annuler une migration, créez une nouvelle migration qui fait l'opération inverse. + +Exemple : Si `20260113-add-column.sql` ajoute une colonne, créez `20260114-remove-column.sql` qui la supprime. + +## Bonnes pratiques + +1. **Toujours tester** sur une base de développement avant de déployer +2. **Sauvegarder** la base de données avant d'appliquer des migrations (fait automatiquement par le script) +3. **Être idempotent** : la migration doit pouvoir être exécutée plusieurs fois sans erreur +4. **Documenter** : ajouter des commentaires expliquant le but de chaque modification +5. **Atomique** : chaque migration doit être une unité logique de changement +6. **Rétrocompatible** : éviter de supprimer des colonnes utilisées par l'ancienne version diff --git a/scripts/create-package.sh b/scripts/create-package.sh new file mode 100755 index 0000000..e18489f --- /dev/null +++ b/scripts/create-package.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +############################################################################# +# Script de création de package - Formation Manager Itinova +# Version: 2.0 +# +# Ce script crée un package tar.gz prêt pour le déploiement incluant: +# - Le code compilé +# - Les dépendances +# - Les migrations SQL +# - Les scripts de déploiement +# - La documentation +############################################################################# + +set -e # Arrêter en cas d'erreur + +# Couleurs +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Configuration +PROJECT_DIR="/home/ubuntu/formation-manager-itinova" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +PACKAGE_NAME="formation-manager-itinova-${TIMESTAMP}.tar.gz" +TEMP_DIR=$(mktemp -d) + +echo -e "${BLUE}=========================================${NC}" +echo -e "${BLUE} Création du package de déploiement${NC}" +echo -e "${BLUE}=========================================${NC}" +echo "" + +# 1. Vérifier que nous sommes dans le bon répertoire +echo -e "${BLUE}[1/8]${NC} Vérification du répertoire..." +cd "$PROJECT_DIR" +if [ ! -f "package.json" ]; then + echo -e "${YELLOW}Erreur: package.json non trouvé${NC}" + exit 1 +fi +echo -e "${GREEN}✓${NC} Répertoire vérifié" + +# 2. Compiler le projet +echo -e "${BLUE}[2/8]${NC} Compilation du projet..." +if command -v pnpm &> /dev/null; then + pnpm build +else + npm run build +fi +echo -e "${GREEN}✓${NC} Projet compilé" + +# 3. Créer la structure dans le répertoire temporaire +echo -e "${BLUE}[3/8]${NC} Préparation de la structure..." +mkdir -p "$TEMP_DIR"/{dist,migrations,scripts,node_modules} +echo -e "${GREEN}✓${NC} Structure créée" + +# 4. Copier les fichiers nécessaires +echo -e "${BLUE}[4/8]${NC} Copie des fichiers..." + +# Code compilé +cp -r dist/* "$TEMP_DIR/dist/" + +# Migrations SQL +if [ -d "migrations" ]; then + cp -r migrations/* "$TEMP_DIR/migrations/" 2>/dev/null || true +fi + +# Scripts de déploiement +cp scripts/update-production.sh "$TEMP_DIR/scripts/" +chmod +x "$TEMP_DIR/scripts/update-production.sh" + +# Fichiers de configuration +cp package.json "$TEMP_DIR/" +cp pnpm-lock.yaml "$TEMP_DIR/" 2>/dev/null || cp package-lock.json "$TEMP_DIR/" 2>/dev/null || true + +# Fichier .env.example (pas le .env réel pour la sécurité) +if [ -f ".env.example" ]; then + cp .env.example "$TEMP_DIR/" +elif [ -f ".env" ]; then + # Créer un .env.example depuis .env en masquant les valeurs sensibles + sed 's/=.*/=/' .env > "$TEMP_DIR/.env.example" +fi + +# Documentation +if [ -f "README.md" ]; then + cp README.md "$TEMP_DIR/" +fi + +echo -e "${GREEN}✓${NC} Fichiers copiés" + +# 5. Installer les dépendances de production +echo -e "${BLUE}[5/8]${NC} Installation des dépendances de production..." +cd "$TEMP_DIR" +if command -v pnpm &> /dev/null; then + pnpm install --prod --frozen-lockfile 2>/dev/null || pnpm install --prod +else + npm install --production +fi +echo -e "${GREEN}✓${NC} Dépendances installées" + +# 6. Créer un fichier VERSION +echo -e "${BLUE}[6/8]${NC} Création du fichier VERSION..." +cat > "$TEMP_DIR/VERSION" << EOF +Version: ${TIMESTAMP} +Date: $(date '+%Y-%m-%d %H:%M:%S') +Git commit: $(cd "$PROJECT_DIR" && git rev-parse --short HEAD 2>/dev/null || echo "N/A") +EOF +echo -e "${GREEN}✓${NC} Fichier VERSION créé" + +# 7. Créer le package tar.gz +echo -e "${BLUE}[7/8]${NC} Création de l'archive..." +cd "$TEMP_DIR" +tar -czf "$PROJECT_DIR/$PACKAGE_NAME" . +echo -e "${GREEN}✓${NC} Archive créée" + +# 8. Nettoyer +echo -e "${BLUE}[8/8]${NC} Nettoyage..." +rm -rf "$TEMP_DIR" +echo -e "${GREEN}✓${NC} Nettoyage terminé" + +# Afficher les informations du package +echo "" +echo -e "${GREEN}=========================================${NC}" +echo -e "${GREEN} Package créé avec succès !${NC}" +echo -e "${GREEN}=========================================${NC}" +echo "" +echo -e "Nom du fichier: ${BLUE}$PACKAGE_NAME${NC}" +echo -e "Emplacement: ${BLUE}$PROJECT_DIR/$PACKAGE_NAME${NC}" +echo -e "Taille: ${BLUE}$(du -h "$PROJECT_DIR/$PACKAGE_NAME" | cut -f1)${NC}" +echo "" +echo -e "${YELLOW}Contenu du package:${NC}" +tar -tzf "$PROJECT_DIR/$PACKAGE_NAME" | head -20 +echo "" +echo -e "${YELLOW}Instructions de déploiement:${NC}" +echo -e "1. Transférer le package sur le serveur:" +echo -e " ${BLUE}scp $PACKAGE_NAME user@serveur:/tmp/${NC}" +echo "" +echo -e "2. Se connecter au serveur et exécuter:" +echo -e " ${BLUE}cd /var/www/formation-manager-itinova${NC}" +echo -e " ${BLUE}./scripts/update-production.sh /tmp/$PACKAGE_NAME${NC}" +echo "" diff --git a/scripts/update-from-package.sh b/scripts/update-from-package.sh new file mode 100755 index 0000000..f71b210 --- /dev/null +++ b/scripts/update-from-package.sh @@ -0,0 +1,242 @@ +#!/bin/bash +# Script de mise à jour depuis package tar.gz +# Formation Manager Itinova +# Ce script met à jour l'application depuis un package tar.gz (sans Git) + +set -e # Arrêter en cas d'erreur + +# Couleurs pour les messages +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo "=========================================" +echo "Mise à jour Formation Manager Itinova" +echo "Depuis package tar.gz" +echo "=========================================" +echo "" + +# Vérifier que nous sommes dans le bon répertoire +if [ ! -f "package.json" ]; then + echo -e "${RED}Erreur: package.json non trouvé.${NC}" + echo -e "${RED}Ce script doit être exécuté depuis /var/www/formation-manager-itinova${NC}" + exit 1 +fi + +# Vérifier que le fichier .env existe +if [ ! -f ".env" ]; then + echo -e "${RED}Erreur: fichier .env non trouvé.${NC}" + echo -e "${RED}La configuration de l'application est manquante.${NC}" + exit 1 +fi + +# Demander le chemin du package +echo -e "${BLUE}Veuillez spécifier le chemin complet du package tar.gz :${NC}" +read -p "Chemin du package (ex: /tmp/formation-manager-itinova-*.tar.gz): " PACKAGE_PATH + +if [ ! -f "$PACKAGE_PATH" ]; then + echo -e "${RED}Erreur: Le fichier $PACKAGE_PATH n'existe pas.${NC}" + exit 1 +fi + +echo "" +echo -e "${YELLOW}Package trouvé: $PACKAGE_PATH${NC}" +echo "" + +# Confirmation +read -p "Voulez-vous continuer avec la mise à jour ? (oui/non): " CONFIRM +if [ "$CONFIRM" != "oui" ]; then + echo "Mise à jour annulée." + exit 0 +fi + +echo "" +echo -e "${YELLOW}1. Sauvegarde de la base de données...${NC}" + +# Créer un répertoire de backup s'il n'existe pas +mkdir -p backups +BACKUP_FILE="backups/db-backup-$(date +%Y%m%d-%H%M%S).sql" + +# Charger les variables d'environnement +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +fi + +# Extraire les informations de connexion depuis DATABASE_URL +if [ -z "$DATABASE_URL" ]; then + echo -e "${RED}Erreur: DATABASE_URL n'est pas définie dans .env${NC}" + exit 1 +fi + +# Parser DATABASE_URL (format: mysql://user:password@host:port/database) +DB_USER=$(echo $DATABASE_URL | sed -n 's/.*:\/\/\([^:]*\):.*/\1/p') +DB_PASS=$(echo $DATABASE_URL | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p') +DB_HOST=$(echo $DATABASE_URL | sed -n 's/.*@\([^:]*\):.*/\1/p') +DB_PORT=$(echo $DATABASE_URL | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') +DB_NAME=$(echo $DATABASE_URL | sed -n 's/.*\/\([^?]*\).*/\1/p') + +echo "Sauvegarde de la base de données $DB_NAME..." +mysqldump -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" --single-transaction --routines --triggers > "$BACKUP_FILE" 2>/dev/null || { + echo -e "${YELLOW}Avertissement: Sauvegarde avec privilèges limités${NC}" + mysqldump -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$BACKUP_FILE" 2>/dev/null +} + +if [ -f "$BACKUP_FILE" ] && [ -s "$BACKUP_FILE" ]; then + echo -e "${GREEN}✓ Sauvegarde créée: $BACKUP_FILE${NC}" +else + echo -e "${RED}Erreur: La sauvegarde a échoué${NC}" + exit 1 +fi + +echo "" +echo -e "${YELLOW}2. Sauvegarde des fichiers actuels...${NC}" + +BACKUP_DIR="backups/files-backup-$(date +%Y%m%d-%H%M%S)" +mkdir -p "$BACKUP_DIR" + +# Sauvegarder les fichiers importants +cp -r client "$BACKUP_DIR/" 2>/dev/null || true +cp -r server "$BACKUP_DIR/" 2>/dev/null || true +cp -r drizzle "$BACKUP_DIR/" 2>/dev/null || true +cp -r shared "$BACKUP_DIR/" 2>/dev/null || true +cp package.json "$BACKUP_DIR/" 2>/dev/null || true +cp tsconfig.json "$BACKUP_DIR/" 2>/dev/null || true +cp vite.config.ts "$BACKUP_DIR/" 2>/dev/null || true + +echo -e "${GREEN}✓ Fichiers sauvegardés dans: $BACKUP_DIR${NC}" + +echo "" +echo -e "${YELLOW}3. Arrêt du service...${NC}" + +# Détecter le gestionnaire de processus (systemd ou PM2) +if systemctl is-active --quiet formation-manager 2>/dev/null; then + echo "Arrêt du service systemd..." + sudo systemctl stop formation-manager + SERVICE_MANAGER="systemd" + echo -e "${GREEN}✓ Service systemd arrêté${NC}" +elif pm2 list | grep -q "formation-manager" 2>/dev/null; then + echo "Arrêt du processus PM2..." + pm2 stop formation-manager + SERVICE_MANAGER="pm2" + echo -e "${GREEN}✓ Processus PM2 arrêté${NC}" +else + echo -e "${YELLOW}Aucun service en cours d'exécution détecté${NC}" + SERVICE_MANAGER="none" +fi + +echo "" +echo -e "${YELLOW}4. Extraction du nouveau code...${NC}" + +# Créer un répertoire temporaire +TEMP_DIR=$(mktemp -d) +echo "Extraction dans $TEMP_DIR..." + +# Extraire le package +tar -xzf "$PACKAGE_PATH" -C "$TEMP_DIR" + +if [ ! -d "$TEMP_DIR/source" ]; then + echo -e "${RED}Erreur: Le répertoire 'source' n'a pas été trouvé dans le package${NC}" + rm -rf "$TEMP_DIR" + exit 1 +fi + +echo -e "${GREEN}✓ Package extrait${NC}" + +echo "" +echo -e "${YELLOW}5. Mise à jour des fichiers...${NC}" + +# Sauvegarder le fichier .env actuel +cp .env .env.backup + +# Copier les nouveaux fichiers (en excluant certains répertoires) +rsync -av --delete \ + --exclude='node_modules' \ + --exclude='dist' \ + --exclude='.env' \ + --exclude='.env.backup' \ + --exclude='backups' \ + --exclude='.manus' \ + "$TEMP_DIR/source/" ./ + +# Restaurer le .env +mv .env.backup .env + +# Nettoyer le répertoire temporaire +rm -rf "$TEMP_DIR" + +echo -e "${GREEN}✓ Fichiers mis à jour${NC}" + +echo "" +echo -e "${YELLOW}6. Installation des dépendances...${NC}" + +# Vérifier si pnpm est installé +if ! command -v pnpm &> /dev/null; then + echo -e "${RED}Erreur: pnpm n'est pas installé${NC}" + echo "Installation de pnpm..." + npm install -g pnpm +fi + +pnpm install + +echo -e "${GREEN}✓ Dépendances installées${NC}" + +echo "" +echo -e "${YELLOW}7. Application des migrations de base de données...${NC}" + +# Utiliser drizzle-kit push pour les mises à jour (ne recrée pas les tables existantes) +echo "Synchronisation du schéma avec la base de données..." +pnpm exec drizzle-kit push || { + echo -e "${YELLOW}Avertissement: La synchronisation a échoué, tentative avec generate...${NC}" + pnpm exec drizzle-kit generate +} + +echo -e "${GREEN}✓ Schéma de base de données synchronisé${NC}" + +echo "" +echo -e "${YELLOW}8. Compilation du projet...${NC}" + +pnpm build + +echo -e "${GREEN}✓ Projet compilé${NC}" + +echo "" +echo -e "${YELLOW}9. Redémarrage du service...${NC}" + +if [ "$SERVICE_MANAGER" = "systemd" ]; then + sudo systemctl start formation-manager + sleep 3 + if systemctl is-active --quiet formation-manager; then + echo -e "${GREEN}✓ Service systemd redémarré avec succès${NC}" + else + echo -e "${RED}Erreur: Le service n'a pas pu démarrer${NC}" + echo "Consultez les logs avec: sudo journalctl -u formation-manager -n 50" + exit 1 + fi +elif [ "$SERVICE_MANAGER" = "pm2" ]; then + pm2 start formation-manager + echo -e "${GREEN}✓ Processus PM2 redémarré${NC}" +else + echo -e "${YELLOW}Aucun gestionnaire de service détecté.${NC}" + echo -e "${YELLOW}Démarrez manuellement l'application avec: pnpm start${NC}" +fi + +echo "" +echo -e "${GREEN}=========================================${NC}" +echo -e "${GREEN}Mise à jour terminée avec succès !${NC}" +echo -e "${GREEN}=========================================${NC}" +echo "" +echo "Sauvegardes créées :" +echo " - Base de données: $BACKUP_FILE" +echo " - Fichiers: $BACKUP_DIR" +echo "" +echo "Pour vérifier le statut :" +if [ "$SERVICE_MANAGER" = "systemd" ]; then + echo " sudo systemctl status formation-manager" + echo " sudo journalctl -u formation-manager -f" +elif [ "$SERVICE_MANAGER" = "pm2" ]; then + echo " pm2 status" + echo " pm2 logs formation-manager" +fi +echo "" diff --git a/scripts/update-production.sh b/scripts/update-production.sh new file mode 100755 index 0000000..6379836 --- /dev/null +++ b/scripts/update-production.sh @@ -0,0 +1,445 @@ +#!/bin/bash + +############################################################################# +# Script de mise à jour production - Formation Manager Itinova +# Version: 2.0 +# +# Ce script gère: +# - Sauvegarde automatique de la base de données +# - Sauvegarde des fichiers +# - Extraction du package +# - Application des migrations SQL versionnées +# - Redémarrage du service +# - Vérification post-déploiement +############################################################################# + +set -e # Arrêter en cas d'erreur +set -u # Arrêter si variable non définie + +# Couleurs pour les messages +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +APP_DIR="/var/www/formation-manager-itinova" +BACKUP_DIR="$APP_DIR/backups" +MIGRATIONS_DIR="$APP_DIR/migrations" +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +LOG_FILE="$APP_DIR/logs/update-$TIMESTAMP.log" + +# Fonction pour afficher les messages +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" | tee -a "$LOG_FILE" +} + +log_success() { + echo -e "${GREEN}[✓]${NC} $1" | tee -a "$LOG_FILE" +} + +log_warning() { + echo -e "${YELLOW}[⚠]${NC} $1" | tee -a "$LOG_FILE" +} + +log_error() { + echo -e "${RED}[✗]${NC} $1" | tee -a "$LOG_FILE" +} + +# Fonction pour vérifier les prérequis +check_prerequisites() { + log_info "Vérification des prérequis..." + + # Vérifier que nous sommes dans le bon répertoire + if [ ! -f "$APP_DIR/package.json" ]; then + log_error "Erreur: package.json non trouvé dans $APP_DIR" + exit 1 + fi + + # Vérifier que le fichier .env existe + if [ ! -f "$APP_DIR/.env" ]; then + log_error "Erreur: fichier .env non trouvé" + exit 1 + fi + + # Créer les répertoires nécessaires + mkdir -p "$BACKUP_DIR" "$MIGRATIONS_DIR" "$(dirname "$LOG_FILE")" + + log_success "Prérequis vérifiés" +} + +# Fonction pour charger les variables d'environnement +load_env() { + log_info "Chargement des variables d'environnement..." + + # Charger le .env + set -a + source "$APP_DIR/.env" + set +a + + # Extraire les informations de connexion MySQL depuis DATABASE_URL + # Format: mysql://user:password@host:port/database + if [ -z "${DATABASE_URL:-}" ]; then + log_error "DATABASE_URL non définie dans .env" + exit 1 + fi + + # Parser DATABASE_URL + DB_USER=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/\([^:]*\):.*/\1/p') + DB_PASS=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p') + DB_HOST=$(echo "$DATABASE_URL" | sed -n 's/.*@\([^:]*\):.*/\1/p') + DB_PORT=$(echo "$DATABASE_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') + DB_NAME=$(echo "$DATABASE_URL" | sed -n 's/.*\/\([^?]*\).*/\1/p') + + if [ -z "$DB_USER" ] || [ -z "$DB_NAME" ]; then + log_error "Impossible de parser DATABASE_URL" + exit 1 + fi + + log_success "Variables d'environnement chargées" +} + +# Fonction pour sauvegarder la base de données +backup_database() { + log_info "Sauvegarde de la base de données..." + + BACKUP_FILE="$BACKUP_DIR/db-backup-$TIMESTAMP.sql" + + # Créer un fichier de configuration MySQL temporaire pour éviter le warning + MYSQL_CONFIG=$(mktemp) + cat > "$MYSQL_CONFIG" << EOF +[client] +user=$DB_USER +password=$DB_PASS +host=$DB_HOST +port=$DB_PORT +EOF + + # Sauvegarder + if mysqldump --defaults-extra-file="$MYSQL_CONFIG" \ + --single-transaction \ + --routines \ + --triggers \ + "$DB_NAME" > "$BACKUP_FILE" 2>&1; then + log_success "Sauvegarde créée: $BACKUP_FILE" + else + log_warning "Sauvegarde partielle (certains privilèges manquants)" + # Ne pas arrêter le script, la sauvegarde partielle est acceptable + fi + + # Nettoyer + rm -f "$MYSQL_CONFIG" + + # Compresser la sauvegarde + gzip "$BACKUP_FILE" + log_success "Sauvegarde compressée: $BACKUP_FILE.gz" + + # Garder seulement les 10 dernières sauvegardes + ls -t "$BACKUP_DIR"/db-backup-*.sql.gz | tail -n +11 | xargs -r rm +} + +# Fonction pour sauvegarder les fichiers +backup_files() { + log_info "Sauvegarde des fichiers..." + + BACKUP_FILE="$BACKUP_DIR/files-backup-$TIMESTAMP.tar.gz" + + # Sauvegarder les fichiers importants (exclure node_modules et dist) + tar -czf "$BACKUP_FILE" \ + --exclude='node_modules' \ + --exclude='dist' \ + --exclude='backups' \ + --exclude='logs' \ + -C "$APP_DIR" \ + . 2>/dev/null || true + + log_success "Fichiers sauvegardés: $BACKUP_FILE" + + # Garder seulement les 5 dernières sauvegardes de fichiers + ls -t "$BACKUP_DIR"/files-backup-*.tar.gz | tail -n +6 | xargs -r rm +} + +# Fonction pour détecter le gestionnaire de processus +detect_process_manager() { + if systemctl is-active --quiet formation-manager 2>/dev/null; then + echo "systemd" + elif pm2 list | grep -q "formation-manager"; then + echo "pm2" + else + echo "unknown" + fi +} + +# Fonction pour arrêter le service +stop_service() { + log_info "Arrêt du service..." + + PROCESS_MANAGER=$(detect_process_manager) + + case "$PROCESS_MANAGER" in + systemd) + sudo systemctl stop formation-manager + log_success "Service arrêté (systemd)" + ;; + pm2) + pm2 stop formation-manager 2>/dev/null || true + log_success "Service arrêté (PM2)" + ;; + *) + log_warning "Gestionnaire de processus non détecté, le service n'a pas été arrêté" + ;; + esac +} + +# Fonction pour extraire le package +extract_package() { + local PACKAGE_PATH="$1" + + log_info "Extraction du package..." + + if [ ! -f "$PACKAGE_PATH" ]; then + log_error "Package non trouvé: $PACKAGE_PATH" + exit 1 + fi + + # Créer un répertoire temporaire + TEMP_DIR=$(mktemp -d) + + # Extraire dans le répertoire temporaire + tar -xzf "$PACKAGE_PATH" -C "$TEMP_DIR" + + # Copier les nouveaux fichiers (préserver .env) + rsync -av --exclude='.env' --exclude='backups' --exclude='logs' "$TEMP_DIR/" "$APP_DIR/" + + # Nettoyer + rm -rf "$TEMP_DIR" + + log_success "Package extrait" +} + +# Fonction pour obtenir la version actuelle de la base de données +get_current_db_version() { + MYSQL_CONFIG=$(mktemp) + cat > "$MYSQL_CONFIG" << EOF +[client] +user=$DB_USER +password=$DB_PASS +host=$DB_HOST +port=$DB_PORT +EOF + + # Créer la table de versions si elle n'existe pas + mysql --defaults-extra-file="$MYSQL_CONFIG" "$DB_NAME" << 'EOF' 2>/dev/null +CREATE TABLE IF NOT EXISTS schema_migrations ( + version VARCHAR(50) PRIMARY KEY, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + description TEXT +); +EOF + + # Obtenir la dernière version + VERSION=$(mysql --defaults-extra-file="$MYSQL_CONFIG" "$DB_NAME" -sN << 'EOF' 2>/dev/null +SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1; +EOF +) + + rm -f "$MYSQL_CONFIG" + + echo "${VERSION:-0}" +} + +# Fonction pour appliquer les migrations +apply_migrations() { + log_info "Application des migrations..." + + CURRENT_VERSION=$(get_current_db_version) + log_info "Version actuelle de la base de données: $CURRENT_VERSION" + + # Créer un fichier de configuration MySQL + MYSQL_CONFIG=$(mktemp) + cat > "$MYSQL_CONFIG" << EOF +[client] +user=$DB_USER +password=$DB_PASS +host=$DB_HOST +port=$DB_PORT +EOF + + # Appliquer chaque migration non appliquée + MIGRATIONS_APPLIED=0 + + for MIGRATION_FILE in "$MIGRATIONS_DIR"/*.sql; do + if [ ! -f "$MIGRATION_FILE" ]; then + continue + fi + + MIGRATION_VERSION=$(basename "$MIGRATION_FILE" .sql) + + # Vérifier si la migration a déjà été appliquée + ALREADY_APPLIED=$(mysql --defaults-extra-file="$MYSQL_CONFIG" "$DB_NAME" -sN << EOF +SELECT COUNT(*) FROM schema_migrations WHERE version = '$MIGRATION_VERSION'; +EOF +) + + if [ "$ALREADY_APPLIED" -eq 0 ]; then + log_info "Application de la migration: $MIGRATION_VERSION" + + # Appliquer la migration + if mysql --defaults-extra-file="$MYSQL_CONFIG" "$DB_NAME" < "$MIGRATION_FILE" 2>&1 | tee -a "$LOG_FILE"; then + # Enregistrer la migration + mysql --defaults-extra-file="$MYSQL_CONFIG" "$DB_NAME" << EOF +INSERT INTO schema_migrations (version, description) +VALUES ('$MIGRATION_VERSION', 'Migration appliquée automatiquement'); +EOF + log_success "Migration $MIGRATION_VERSION appliquée avec succès" + MIGRATIONS_APPLIED=$((MIGRATIONS_APPLIED + 1)) + else + log_error "Échec de la migration $MIGRATION_VERSION" + rm -f "$MYSQL_CONFIG" + exit 1 + fi + else + log_info "Migration $MIGRATION_VERSION déjà appliquée (ignorée)" + fi + done + + rm -f "$MYSQL_CONFIG" + + if [ $MIGRATIONS_APPLIED -eq 0 ]; then + log_info "Aucune nouvelle migration à appliquer" + else + log_success "$MIGRATIONS_APPLIED migration(s) appliquée(s)" + fi +} + +# Fonction pour installer les dépendances +install_dependencies() { + log_info "Installation des dépendances..." + + cd "$APP_DIR" + + if command -v pnpm &> /dev/null; then + pnpm install --prod 2>&1 | tee -a "$LOG_FILE" + else + npm install --production 2>&1 | tee -a "$LOG_FILE" + fi + + log_success "Dépendances installées" +} + +# Fonction pour démarrer le service +start_service() { + log_info "Démarrage du service..." + + PROCESS_MANAGER=$(detect_process_manager) + + case "$PROCESS_MANAGER" in + systemd) + sudo systemctl start formation-manager + sleep 3 + if systemctl is-active --quiet formation-manager; then + log_success "Service démarré (systemd)" + else + log_error "Échec du démarrage du service" + exit 1 + fi + ;; + pm2) + pm2 restart formation-manager 2>&1 | tee -a "$LOG_FILE" + sleep 3 + if pm2 list | grep -q "formation-manager.*online"; then + log_success "Service démarré (PM2)" + else + log_error "Échec du démarrage du service" + exit 1 + fi + ;; + *) + log_warning "Gestionnaire de processus non détecté, démarrage manuel requis" + ;; + esac +} + +# Fonction pour vérifier le service +verify_service() { + log_info "Vérification du service..." + + # Attendre que le service démarre + sleep 5 + + # Vérifier que le service répond + if curl -f http://localhost:3000/ &>/dev/null; then + log_success "Service opérationnel" + else + log_warning "Le service ne répond pas sur le port 3000" + fi + + # Afficher les derniers logs + PROCESS_MANAGER=$(detect_process_manager) + + case "$PROCESS_MANAGER" in + systemd) + log_info "Derniers logs:" + sudo journalctl -u formation-manager -n 20 --no-pager | tee -a "$LOG_FILE" + ;; + pm2) + log_info "Derniers logs:" + pm2 logs formation-manager --lines 20 --nostream | tee -a "$LOG_FILE" + ;; + esac +} + +# Fonction principale +main() { + echo "" + echo "=========================================" + echo " Mise à jour Formation Manager Itinova" + echo "=========================================" + echo "" + + # Demander le chemin du package + if [ $# -eq 0 ]; then + read -p "Chemin du package tar.gz: " PACKAGE_PATH + else + PACKAGE_PATH="$1" + fi + + # Vérifier les prérequis + check_prerequisites + + # Charger les variables d'environnement + load_env + + # Sauvegardes + backup_database + backup_files + + # Arrêter le service + stop_service + + # Extraire le package + extract_package "$PACKAGE_PATH" + + # Appliquer les migrations + apply_migrations + + # Installer les dépendances + install_dependencies + + # Démarrer le service + start_service + + # Vérifier le service + verify_service + + echo "" + log_success "Mise à jour terminée avec succès!" + echo "" + log_info "Logs complets: $LOG_FILE" + log_info "Sauvegarde DB: $BACKUP_DIR/db-backup-$TIMESTAMP.sql.gz" + echo "" +} + +# Exécuter +main "$@" diff --git a/server/questionnaireDb.ts b/server/questionnaireDb.ts index 068a3d6..264d7cc 100644 --- a/server/questionnaireDb.ts +++ b/server/questionnaireDb.ts @@ -199,12 +199,7 @@ export async function getQuestionnaireStats(questionnaireId: number) { const reponsesResult = await db .select({ count: sql`count(*)` }) .from(reponsesQuestionnaires) - .where( - and( - eq(reponsesQuestionnaires.questionnaireId, questionnaireId), - eq(reponsesQuestionnaires.complete, true) - ) - ); + .where(eq(reponsesQuestionnaires.questionnaireId, questionnaireId)); const totalReponses = reponsesResult[0]?.count || 0; // Taux de réponse @@ -239,12 +234,7 @@ export async function getReponsesDetailleesQuestionnaire(questionnaireId: number reponsesQuestions, eq(reponsesQuestions.reponseQuestionnaireId, reponsesQuestionnaires.id) ) - .where( - and( - eq(reponsesQuestionnaires.questionnaireId, questionnaireId), - eq(reponsesQuestionnaires.complete, true) - ) - ); + .where(eq(reponsesQuestionnaires.questionnaireId, questionnaireId)); return result; } diff --git a/server/questionnaireExport.ts b/server/questionnaireExport.ts index f412095..5257898 100644 --- a/server/questionnaireExport.ts +++ b/server/questionnaireExport.ts @@ -22,7 +22,7 @@ export async function exportQuestionnaireToExcel(questionnaireId: number): Promi // Feuille 1: Statistiques globales const statsSheet = workbook.addWorksheet('Statistiques'); - statsSheet.addRow(['Questionnaire', questionnaire.nom]); + statsSheet.addRow(['Questionnaire', questionnaire.titre]); statsSheet.addRow(['Type', questionnaire.type]); statsSheet.addRow(['']); statsSheet.addRow(['Nombre d\'envois', stats.totalEnvois]); @@ -110,7 +110,7 @@ export async function exportQuestionnaireToPDF(questionnaireId: number): Promise // Titre doc.setFontSize(18); doc.setFont('helvetica', 'bold'); - doc.text(questionnaire.nom, 20, yPos); + doc.text(questionnaire.titre, 20, yPos); yPos += 10; // Type diff --git a/server/questionnaireScheduler.ts b/server/questionnaireScheduler.ts index ddb8f46..f476d6f 100644 --- a/server/questionnaireScheduler.ts +++ b/server/questionnaireScheduler.ts @@ -130,7 +130,7 @@ export async function processEnvoisAutomatiques() { for (const questionnaire of questionnairesAEnvoyer) { console.log( - `[Questionnaires] Traitement du questionnaire "${questionnaire.nom}" (délai: J+${questionnaire.delaiEnvoiJours})` + `[Questionnaires] Traitement du questionnaire "${questionnaire.titre}" (délai: J+${questionnaire.delaiEnvoiJours})` ); // Calculer la date cible (aujourd'hui - délai) @@ -198,7 +198,7 @@ export async function processEnvoisAutomatiques() { apprenant.apprenantEmail, apprenant.apprenantNom, apprenant.apprenantPrenom, - questionnaire.nom, + questionnaire.titre, seq.sequenceNom );