From 159243419240716a3481feac55a5c134d6d35237 Mon Sep 17 00:00:00 2001 From: mgonzalez Date: Tue, 3 Mar 2026 11:50:59 -0500 Subject: [PATCH] Initial commit --- .gitignore | 36 + ADMIN.md | 58 + README.md | 93 + SUPERADMIN.md | 45 + USER.md | 25 + backend/package-lock.json | 2999 +++++++++++++++++ backend/package.json | 22 + backend/src/app.js | 50 + backend/src/config/env.js | 11 + backend/src/db/central.js | 22 + backend/src/db/tenant.js | 24 + backend/src/middleware/auth.js | 15 + backend/src/middleware/rbac.js | 10 + backend/src/middleware/tenant.js | 24 + backend/src/routes/admin.js | 217 ++ backend/src/routes/auth.js | 47 + backend/src/routes/reports.js | 31 + backend/src/routes/superadmin.js | 180 + backend/src/routes/surveys.js | 58 + backend/src/scripts/generate_templates.js | 21 + backend/src/scripts/seed_superadmin.js | 27 + backend/src/server.js | 6 + backend/src/services/importService.js | 41 + backend/src/services/participationService.js | 69 + backend/src/services/questionService.js | 176 + backend/src/services/questionSetService.js | 68 + backend/src/services/reportService.js | 69 + backend/src/services/resultService.js | 61 + backend/src/services/surveyService.js | 37 + backend/src/services/tenantProvision.js | 57 + backend/src/sql/central_schema.sql | 36 + backend/src/sql/seed_questions.sql | 41 + backend/src/sql/tenant_init.sql | 4 + .../src/sql/tenant_migration_questions.sql | 77 + backend/src/sql/tenant_schema.sql | 159 + backend/src/utils/jwt.js | 12 + backend/src/utils/password.js | 12 + desarrollo/PARA_NUEVO_CHAT.md | 66 + frontend/index.html | 16 + frontend/package-lock.json | 2009 +++++++++++ frontend/package.json | 21 + frontend/src/App.jsx | 166 + frontend/src/assets/creator-logo.png | Bin 0 -> 2105 bytes frontend/src/main.jsx | 6 + frontend/src/pages/AdminDashboard.jsx | 38 + frontend/src/pages/ImportUsers.jsx | 33 + frontend/src/pages/LoginPage.jsx | 62 + frontend/src/pages/ParticipantSurvey.jsx | 81 + frontend/src/pages/QuestionBank.jsx | 350 ++ frontend/src/pages/SuperadminCompanies.jsx | 377 +++ frontend/src/pages/SurveyPeriods.jsx | 97 + frontend/src/styles/app.css | 257 ++ frontend/src/utils/api.js | 49 + frontend/vite.config.js | 10 + templates/plantilla_usuarios.csv | 1 + templates/plantilla_usuarios.xlsx | Bin 0 -> 6437 bytes 56 files changed, 8579 insertions(+) create mode 100644 .gitignore create mode 100644 ADMIN.md create mode 100644 README.md create mode 100644 SUPERADMIN.md create mode 100644 USER.md create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/src/app.js create mode 100644 backend/src/config/env.js create mode 100644 backend/src/db/central.js create mode 100644 backend/src/db/tenant.js create mode 100644 backend/src/middleware/auth.js create mode 100644 backend/src/middleware/rbac.js create mode 100644 backend/src/middleware/tenant.js create mode 100644 backend/src/routes/admin.js create mode 100644 backend/src/routes/auth.js create mode 100644 backend/src/routes/reports.js create mode 100644 backend/src/routes/superadmin.js create mode 100644 backend/src/routes/surveys.js create mode 100644 backend/src/scripts/generate_templates.js create mode 100644 backend/src/scripts/seed_superadmin.js create mode 100644 backend/src/server.js create mode 100644 backend/src/services/importService.js create mode 100644 backend/src/services/participationService.js create mode 100644 backend/src/services/questionService.js create mode 100644 backend/src/services/questionSetService.js create mode 100644 backend/src/services/reportService.js create mode 100644 backend/src/services/resultService.js create mode 100644 backend/src/services/surveyService.js create mode 100644 backend/src/services/tenantProvision.js create mode 100644 backend/src/sql/central_schema.sql create mode 100644 backend/src/sql/seed_questions.sql create mode 100644 backend/src/sql/tenant_init.sql create mode 100644 backend/src/sql/tenant_migration_questions.sql create mode 100644 backend/src/sql/tenant_schema.sql create mode 100644 backend/src/utils/jwt.js create mode 100644 backend/src/utils/password.js create mode 100644 desarrollo/PARA_NUEVO_CHAT.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/assets/creator-logo.png create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/pages/AdminDashboard.jsx create mode 100644 frontend/src/pages/ImportUsers.jsx create mode 100644 frontend/src/pages/LoginPage.jsx create mode 100644 frontend/src/pages/ParticipantSurvey.jsx create mode 100644 frontend/src/pages/QuestionBank.jsx create mode 100644 frontend/src/pages/SuperadminCompanies.jsx create mode 100644 frontend/src/pages/SurveyPeriods.jsx create mode 100644 frontend/src/styles/app.css create mode 100644 frontend/src/utils/api.js create mode 100644 frontend/vite.config.js create mode 100644 templates/plantilla_usuarios.csv create mode 100644 templates/plantilla_usuarios.xlsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c0b9ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Node +node_modules/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Env +.env +.env.* + +# OS +.DS_Store +Thumbs.db + +# Vite +vite.config.*.timestamp* + +# Build +/dist +/build + +# IDE +.vscode/ +.idea/ + +# Tokens/secrets +*token* +*_token* +*.secret +*.secrets + +# Local DB dumps +*.sql +*.dump diff --git a/ADMIN.md b/ADMIN.md new file mode 100644 index 0000000..22f2939 --- /dev/null +++ b/ADMIN.md @@ -0,0 +1,58 @@ +# Manual Admin Empresa + +## Objetivo +Gestionar mediciones de clima laboral, participantes y reportes de su empresa. + +## Acceso +1) Ingresar con rol **admin** en la pantalla de Login. +2) Indicar `company_code` asignado por superadmin. + +## Funciones principales +- Crear periodos de medición. +- Activar y cerrar mediciones. +- Ver dashboard de resultados. +- Importar participantes desde Excel. +- Descargar reportes PDF y Excel. + +## Crear una medición +1) Ir a **Mediciones**. +2) Completar año, mes y fechas. +3) Al crear la medición se genera el snapshot de preguntas. + +## Activar una medición +- En el listado de mediciones, presionar **Activar**. + +## Cerrar una medición (cálculo automático) +- En el listado de mediciones, presionar **Cerrar**. +- El sistema calcula automáticamente: + - Promedio por pregunta. + - Promedio por dimensión. + - Índice global. + - Tasa de participación. + - Clasificación Bajo/Medio/Alto. + +## Importación de usuarios +- Ir a **Importar**. +- Usar plantilla con columnas: + 1) Nombre + 2) Email + 3) Departamento + 4) Cargo +- El sistema valida y reporta errores por fila. + +## Importación de admins +No aplica. En Fase 1 solo existe **un admin principal por empresa** y se crea automáticamente desde el Superadmin al provisionar la compañía, para evitar inconsistencias y garantizar el aislamiento por tenant. + +## Reportes +- PDF: `GET /reports/pdf/:surveyPeriodId` +- Excel: `GET /reports/excel/:surveyPeriodId` + +## Dashboard +- Índice global actual +- Resultado por dimensión +- Histórico por periodo +- Avance de encuestas (registrados, 100% completadas, <100%) + +## Consideraciones +- Las preguntas no se editan en Fase 1. +- Cada medición queda vinculada al snapshot de preguntas del periodo. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bead4a --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Clima Laboral SaaS - Fase 1 + +## Resumen +Sistema SaaS multi-tenant con MariaDB (una base por empresa). Incluye superadmin, admin por empresa, participantes, encuestas fijas, cálculos automáticos, histórico, reportes PDF/Excel e importación de usuarios. + +## Conexión MariaDB +Actualmente el acceso correcto es con `user_ia` en `192.168.222.20` (puerto 3306). + +## Estructura +- `backend/` API Node.js + Express +- `frontend/` UI React + Vite +- `backend/src/sql/` scripts de base de datos + +## Base de datos central +Archivo: `backend/src/sql/central_schema.sql` + +Tablas: +- `superadmins` +- `companies` (datos de conexión de cada empresa) + - `start_date` / `end_date` controlan vigencia de acceso por empresa + +## Base de datos por empresa +Archivos: +- `backend/src/sql/tenant_schema.sql` +- `backend/src/sql/seed_questions.sql` + +Tablas principales: +- `admins`, `users`, `survey_periods`, `dimensions`, `questions`, `survey_questions`, `responses`, `survey_results` + +Preparación Fase 2: +- `scales`, `question_versions`, `question_weights`, `metric_definitions`, `survey_rules` + +## Flujo del sistema +1. Superadmin crea empresa. +2. Se genera DB de empresa, se ejecuta schema + seed, se crea admin principal. +3. Admin crea medición (survey_period) y se genera snapshot en `survey_questions`. +4. Admin activa la medición. +5. Participantes responden. +6. Admin cierra la medición y se calculan resultados automáticamente. +7. Dashboard, histórico, PDF y Excel disponibles. + +## Cálculos automáticos +- Promedio por pregunta, por dimensión y global. +- Conversión a escala 0-100: `(avg - 1) / 4 * 100`. +- Clasificación: 0-59 Bajo, 60-79 Medio, 80-100 Alto. +- Participación: participantes únicos / total usuarios activos. + +## Endpoints principales +- `POST /auth/login` +- `GET /superadmin/companies` +- `POST /superadmin/companies` +- `GET /admin/survey-periods` +- `POST /admin/survey-periods` +- `PATCH /admin/survey-periods/:id/activate` +- `PATCH /admin/survey-periods/:id/close` +- `GET /admin/dashboard` +- `POST /admin/users/import` +- `GET /reports/pdf/:surveyPeriodId` +- `GET /reports/excel/:surveyPeriodId` +- `GET /surveys/active` +- `POST /surveys/responses` + +## Importación de usuarios +Se espera un Excel con columnas: +1) Nombre +2) Email +3) Departamento +4) Cargo + +## Banco de preguntas (Excel) +Exportación e importación desde Admin: +- Archivo Excel con columnas: `question_code`, `dimension_code`, `dimension_name`, `text`, `is_active`. +- Si cambia el texto de una pregunta con mismo `question_code`, se crea una nueva versión con fecha. +- Para cambios, exporta, edita y vuelve a importar. + +## Versiones de encuesta +Cada set de preguntas genera un `question_set_code` con versión. Las mediciones se crean usando el set activo. +## Importación de admins +No aplica en Fase 1. Solo se permite un admin principal por empresa, creado por Superadmin al provisionar la compañía. + +## Puesta en marcha (local) +1) Instalar dependencias en `backend/` y `frontend/`. +2) Crear base central con `central_schema.sql`. +3) Crear superadmin con `npm run seed:superadmin -- "Nombre" correo@dominio.com clave`. +4) Iniciar backend y frontend. + +## Puertos y acceso +- Backend: `4020` +- Frontend: `5373` +- La UI consume la API usando la IP del host (no usa `localhost`). + +## Nota +No se ejecutaron instalaciones por restricciones del entorno. diff --git a/SUPERADMIN.md b/SUPERADMIN.md new file mode 100644 index 0000000..f458235 --- /dev/null +++ b/SUPERADMIN.md @@ -0,0 +1,45 @@ +# Manual Superadmin + +## Objetivo +Administrar la plataforma SaaS y crear empresas con su base de datos independiente. + +## Acceso +1) Ingresar con rol **superadmin** en la pantalla de Login. +2) No requiere `company_code`. + +## Funciones principales +- Crear empresas (provisionamiento automático de base de datos y admin principal). +- Crear múltiples administradores por empresa. +- Activar o suspender empresas. +- Definir fecha de inicio y fecha de fin de vigencia por empresa. +- Las fechas son obligatorias. Si no están en rango, la empresa queda sin acceso. +- Ver avance de encuestas por empresa (registrados, 100% completadas, <100%). +- Ver listado de empresas. + +## Flujo de creación de empresa +1) Completar el formulario **Nueva Empresa**. +2) Al guardar, el sistema: + - Crea la base de datos de la empresa. + - Ejecuta el schema + seed de preguntas. + - Crea el admin principal. + +## Campos requeridos al crear empresa +- Nombre +- Código (ej: EMPRESA-001) +- Fecha inicio (opcional) +- Fecha fin (opcional) +- DB Name (ej: clima_empresa_001) +- Admin Nombre +- Admin Email +- Admin Password + +> Nota: Los datos de conexión por defecto están precargados en UI, se pueden ajustar si aplica. + +## Endpoints relevantes +- `POST /auth/login` +- `GET /superadmin/companies` +- `POST /superadmin/companies` +- `PATCH /superadmin/companies/:id/status` + +## Consideraciones +- Si el acceso a MariaDB falla (Error 1045), revisar permisos del usuario en el host cliente. diff --git a/USER.md b/USER.md new file mode 100644 index 0000000..563f5e2 --- /dev/null +++ b/USER.md @@ -0,0 +1,25 @@ +# Manual Usuario Participante + +## Objetivo +Responder la encuesta de clima laboral activa de su empresa. + +## Acceso +1) Ingresar con rol **user** en la pantalla de Login. +2) Indicar `company_code`. +3) Usar correo y contraseña entregados por el admin. + +## Responder encuesta +1) Ir a **Encuesta**. +2) Completar todas las preguntas (escala 1 a 5). +3) Presionar **Enviar**. + +## Escala +- 1: Totalmente en desacuerdo +- 2: En desacuerdo +- 3: Ni de acuerdo ni en desacuerdo +- 4: De acuerdo +- 5: Totalmente de acuerdo + +## Consideraciones +- Sólo se puede responder durante el periodo activo. +- Las respuestas quedan asociadas al periodo de medición vigente. diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..9cf609e --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,2999 @@ +{ + "name": "clima-laboral-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clima-laboral-backend", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dayjs": "^1.11.11", + "exceljs": "^4.4.0", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.10.2", + "pdfkit": "^0.15.0" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "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" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "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" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz", + "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "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.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "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.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "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.14.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" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "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.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fontkit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", + "integrity": "sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.3.13", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "deep-equal": "^2.0.5", + "dfa": "^1.2.0", + "restructure": "^2.0.1", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.3.1", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "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.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "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.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mysql2": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.18.2.tgz", + "integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pdfkit": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.15.2.tgz", + "integrity": "sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^1.8.1", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.0.2", + "png-js": "^1.0.0" + } + }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/restructure": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", + "integrity": "sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT", + "peer": true + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "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.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..8508689 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,22 @@ +{ + "name": "clima-laboral-backend", + "version": "1.0.0", + "private": true, + "main": "src/server.js", + "scripts": { + "dev": "node src/server.js", + "start": "node src/server.js", + "seed:superadmin": "node src/scripts/seed_superadmin.js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dayjs": "^1.11.11", + "exceljs": "^4.4.0", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.10.2", + "pdfkit": "^0.15.0" + } +} diff --git a/backend/src/app.js b/backend/src/app.js new file mode 100644 index 0000000..6168105 --- /dev/null +++ b/backend/src/app.js @@ -0,0 +1,50 @@ +const express = require('express'); +const cors = require('cors'); + +const authRoutes = require('./routes/auth'); +const superadminRoutes = require('./routes/superadmin'); +const adminRoutes = require('./routes/admin'); +const surveyRoutes = require('./routes/surveys'); +const reportRoutes = require('./routes/reports'); + +const app = express(); +app.use(cors()); +app.use(express.json({ limit: '5mb' })); + +app.get('/health', (req, res) => res.json({ ok: true })); + +app.use('/auth', authRoutes); +app.use('/superadmin', superadminRoutes); +app.use('/admin', adminRoutes); +app.use('/surveys', surveyRoutes); +app.use('/reports', reportRoutes); + +const { getCentralPool } = require('./db/central'); + +app.use(async (err, req, res, next) => { + console.error(err); + const role = req.user?.role || null; + const userId = req.user?.id || null; + const companyCode = req.user?.company_code || req.headers['x-company-code'] || null; + const menu = req.headers['x-menu'] || req.headers['x-route'] || req.path; + const errorMessage = err?.message || 'Error interno'; + + if (role !== 'superadmin') { + try { + const central = getCentralPool(); + await central.query( + 'INSERT INTO error_logs (user_role, user_id, company_code, menu, error_message) VALUES (?, ?, ?, ?, ?)', + [role, userId, companyCode, menu, errorMessage] + ); + } catch (logErr) { + console.error('Error log fail:', logErr.message); + } + } + + if (role === 'superadmin') { + return res.status(500).json({ error: 'Error interno', detail: errorMessage }); + } + return res.status(500).json({ error: 'Error interno' }); +}); + +module.exports = app; diff --git a/backend/src/config/env.js b/backend/src/config/env.js new file mode 100644 index 0000000..95fc408 --- /dev/null +++ b/backend/src/config/env.js @@ -0,0 +1,11 @@ +module.exports = { + port: process.env.PORT || 4020, + jwtSecret: process.env.JWT_SECRET || 'CHANGE_ME', + centralDb: { + host: process.env.CENTRAL_DB_HOST || '192.168.222.20', + port: process.env.CENTRAL_DB_PORT || 3306, + user: process.env.CENTRAL_DB_USER || 'user_ia', + password: process.env.CENTRAL_DB_PASS || 'UserIA2026', + database: process.env.CENTRAL_DB_NAME || 'clima_platform' + } +}; diff --git a/backend/src/db/central.js b/backend/src/db/central.js new file mode 100644 index 0000000..c9278b7 --- /dev/null +++ b/backend/src/db/central.js @@ -0,0 +1,22 @@ +const mysql = require('mysql2/promise'); +const { centralDb } = require('../config/env'); + +let pool; + +function getCentralPool() { + if (!pool) { + pool = mysql.createPool({ + host: centralDb.host, + port: centralDb.port, + user: centralDb.user, + password: centralDb.password, + database: centralDb.database, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 + }); + } + return pool; +} + +module.exports = { getCentralPool }; diff --git a/backend/src/db/tenant.js b/backend/src/db/tenant.js new file mode 100644 index 0000000..b27ffbc --- /dev/null +++ b/backend/src/db/tenant.js @@ -0,0 +1,24 @@ +const mysql = require('mysql2/promise'); +const pools = new Map(); + +function getTenantPool(company) { + if (!company || !company.db_name) { + throw new Error('Empresa sin datos de conexión'); + } + if (!pools.has(company.db_name)) { + const pool = mysql.createPool({ + host: company.db_host, + port: company.db_port || 3306, + user: company.db_user, + password: company.db_pass, + database: company.db_name, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 + }); + pools.set(company.db_name, pool); + } + return pools.get(company.db_name); +} + +module.exports = { getTenantPool }; diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js new file mode 100644 index 0000000..4451882 --- /dev/null +++ b/backend/src/middleware/auth.js @@ -0,0 +1,15 @@ +const { verifyToken } = require('../utils/jwt'); + +function authRequired(req, res, next) { + const header = req.headers.authorization || ''; + const token = header.replace('Bearer ', ''); + if (!token) return res.status(401).json({ error: 'Token requerido' }); + try { + req.user = verifyToken(token); + next(); + } catch (err) { + return res.status(401).json({ error: 'Token inválido' }); + } +} + +module.exports = { authRequired }; diff --git a/backend/src/middleware/rbac.js b/backend/src/middleware/rbac.js new file mode 100644 index 0000000..95c085f --- /dev/null +++ b/backend/src/middleware/rbac.js @@ -0,0 +1,10 @@ +function requireRole(...roles) { + return (req, res, next) => { + if (!req.user || !roles.includes(req.user.role)) { + return res.status(403).json({ error: 'No autorizado' }); + } + next(); + }; +} + +module.exports = { requireRole }; diff --git a/backend/src/middleware/tenant.js b/backend/src/middleware/tenant.js new file mode 100644 index 0000000..ce3358e --- /dev/null +++ b/backend/src/middleware/tenant.js @@ -0,0 +1,24 @@ +const { getCentralPool } = require('../db/central'); +const { getTenantPool } = require('../db/tenant'); + +async function resolveTenant(req, res, next) { + try { + const code = req.headers['x-company-code'] || req.query.company_code || req.body.company_code; + if (!code) return res.status(400).json({ error: 'company_code requerido' }); + + const central = getCentralPool(); + const [rows] = await central.query( + 'SELECT * FROM companies WHERE code = ? AND status = "active" AND start_date <= CURDATE() AND end_date >= CURDATE()', + [code] + ); + if (!rows.length) return res.status(404).json({ error: 'Empresa no encontrada o inactiva' }); + + req.company = rows[0]; + req.tenantPool = getTenantPool(rows[0]); + next(); + } catch (err) { + next(err); + } +} + +module.exports = { resolveTenant }; diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js new file mode 100644 index 0000000..f5accb5 --- /dev/null +++ b/backend/src/routes/admin.js @@ -0,0 +1,217 @@ +const express = require('express'); +const multer = require('multer'); +const { authRequired } = require('../middleware/auth'); +const { requireRole } = require('../middleware/rbac'); +const { resolveTenant } = require('../middleware/tenant'); +const { createSurveyPeriod, activateSurvey, closeSurvey } = require('../services/surveyService'); +const { importUsersFromExcel } = require('../services/importService'); +const { getParticipationSummary } = require('../services/participationService'); +const { + listQuestions, + exportQuestionsExcel, + exportQuestionsTemplate, + importQuestionsExcel, + listQuestionSets, + listDimensions, + createDimension, + createQuestionSetFromCurrent, + createQuestionSetEmpty, + addQuestionToSet +} = require('../services/questionService'); + +const upload = multer(); +const router = express.Router(); + +router.use(authRequired, requireRole('admin', 'superadmin'), resolveTenant); + +router.get('/survey-periods', async (req, res, next) => { + try { + const [rows] = await req.tenantPool.query('SELECT * FROM survey_periods ORDER BY id DESC'); + res.json(rows); + } catch (err) { + next(err); + } +}); + +router.post('/survey-periods', async (req, res, next) => { + try { + const { year, month, fecha_inicio, fecha_fin } = req.body; + if (!year || !month || !fecha_inicio || !fecha_fin) return res.status(400).json({ error: 'Campos requeridos' }); + const period = await createSurveyPeriod(req.tenantPool, { year, month, fecha_inicio, fecha_fin }); + res.json(period); + } catch (err) { + next(err); + } +}); + +router.patch('/survey-periods/:id/activate', async (req, res, next) => { + try { + await activateSurvey(req.tenantPool, req.params.id); + res.json({ ok: true }); + } catch (err) { + next(err); + } +}); + +router.patch('/survey-periods/:id/close', async (req, res, next) => { + try { + const result = await closeSurvey(req.tenantPool, req.params.id); + res.json({ ok: true, result }); + } catch (err) { + next(err); + } +}); + +router.get('/dashboard', async (req, res, next) => { + try { + const [[current]] = await req.tenantPool.query( + 'SELECT score, classification, participation_rate FROM survey_results WHERE scope = "global" ORDER BY survey_period_id DESC LIMIT 1' + ); + const [dimensions] = await req.tenantPool.query( + 'SELECT d.name, r.score, r.classification FROM survey_results r JOIN dimensions d ON d.id = r.dimension_id WHERE r.scope = "dimension" AND r.survey_period_id = (SELECT MAX(id) FROM survey_periods WHERE estado = "cerrada")' + ); + const [history] = await req.tenantPool.query( + 'SELECT sp.codigo_medicion, r.score FROM survey_results r JOIN survey_periods sp ON sp.id = r.survey_period_id WHERE r.scope = "global" ORDER BY sp.id DESC' + ); + const participation = await getParticipationSummary(req.tenantPool); + res.json({ current, dimensions, history, participation }); + } catch (err) { + next(err); + } +}); + +router.post('/users/import', upload.single('file'), async (req, res, next) => { + try { + if (!req.file) return res.status(400).json({ error: 'Archivo requerido' }); + const result = await importUsersFromExcel(req.file.buffer, req.tenantPool); + res.json(result); + } catch (err) { + next(err); + } +}); + +router.get('/questions', async (req, res, next) => { + try { + const rows = await listQuestions(req.tenantPool); + res.json(rows); + } catch (err) { + next(err); + } +}); + +router.get('/questions/export', async (req, res, next) => { + try { + const buffer = await exportQuestionsExcel(req.tenantPool); + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', 'attachment; filename=\"preguntas.xlsx\"'); + res.send(buffer); + } catch (err) { + next(err); + } +}); + +router.get('/questions/template', async (req, res, next) => { + try { + const buffer = await exportQuestionsTemplate(); + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', 'attachment; filename=\"preguntas_template.xlsx\"'); + res.send(buffer); + } catch (err) { + next(err); + } +}); + +router.post('/questions/import', upload.single('file'), async (req, res, next) => { + try { + if (!req.file) return res.status(400).json({ error: 'Archivo requerido' }); + const setId = req.body?.set_id ? Number(req.body.set_id) : null; + const result = await importQuestionsExcel(req.file.buffer, req.tenantPool, setId); + res.json(result); + } catch (err) { + next(err); + } +}); + +router.get('/question-sets', async (req, res, next) => { + try { + const rows = await listQuestionSets(req.tenantPool); + res.json(rows); + } catch (err) { + next(err); + } +}); + +router.post('/question-sets', async (req, res, next) => { + try { + const { name, empty } = req.body; + const result = empty ? await createQuestionSetEmpty(req.tenantPool, name) : await createQuestionSetFromCurrent(req.tenantPool, name); + res.json(result); + } catch (err) { + next(err); + } +}); + +router.delete('/question-sets/:id', async (req, res, next) => { + try { + const [rows] = await req.tenantPool.query('SELECT id, code, is_active FROM question_sets WHERE id = ?', [req.params.id]); + if (!rows.length) return res.status(404).json({ error: 'Set no encontrado' }); + const setRow = rows[0]; + if (setRow.is_active) { + return res.status(400).json({ error: 'No se puede eliminar un set activo' }); + } + const [[{ used }]] = await req.tenantPool.query('SELECT COUNT(*) AS used FROM survey_periods WHERE question_set_code = ?', [setRow.code]); + if (used > 0) { + return res.status(400).json({ error: 'No se puede eliminar: ya tiene mediciones' }); + } + await req.tenantPool.query('DELETE FROM question_set_questions WHERE question_set_id = ?', [setRow.id]); + await req.tenantPool.query('DELETE FROM question_sets WHERE id = ?', [setRow.id]); + res.json({ ok: true }); + } catch (err) { + next(err); + } +}); + +router.get('/question-sets/:id/questions', async (req, res, next) => { + try { + const [rows] = await req.tenantPool.query( + 'SELECT qs.question_code, qs.text_snapshot, d.name AS dimension FROM question_set_questions qs JOIN dimensions d ON d.id = qs.dimension_id WHERE qs.question_set_id = ? ORDER BY qs.sort_order', + [req.params.id] + ); + res.json(rows); + } catch (err) { + next(err); + } +}); + +router.get('/dimensions', async (req, res, next) => { + try { + const rows = await listDimensions(req.tenantPool); + res.json(rows); + } catch (err) { + next(err); + } +}); + +router.post('/dimensions', async (req, res, next) => { + try { + const { name, code, description } = req.body; + if (!name) return res.status(400).json({ error: 'Nombre requerido' }); + const result = await createDimension(req.tenantPool, { name, code, description }); + res.json(result); + } catch (err) { + next(err); + } +}); + +router.post('/question-sets/:id/questions', async (req, res, next) => { + try { + const { dimension_id, text } = req.body; + if (!dimension_id || !text) return res.status(400).json({ error: 'dimension_id y text requeridos' }); + const result = await addQuestionToSet(req.tenantPool, { questionSetId: req.params.id, dimensionId: dimension_id, text }); + res.json(result); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js new file mode 100644 index 0000000..fa9e874 --- /dev/null +++ b/backend/src/routes/auth.js @@ -0,0 +1,47 @@ +const express = require('express'); +const { getCentralPool } = require('../db/central'); +const { resolveTenant } = require('../middleware/tenant'); +const { comparePassword } = require('../utils/password'); +const { signToken } = require('../utils/jwt'); + +const router = express.Router(); + +router.post('/login', async (req, res, next) => { + try { + const { email, password, role, company_code } = req.body; + if (!email || !password || !role) { + return res.status(400).json({ error: 'email, password y role requeridos' }); + } + + if (role === 'superadmin') { + const central = getCentralPool(); + const [rows] = await central.query('SELECT * FROM superadmins WHERE email = ? AND status = "active"', [email]); + if (!rows.length) return res.status(401).json({ error: 'Credenciales inválidas' }); + const ok = await comparePassword(password, rows[0].password_hash); + if (!ok) return res.status(401).json({ error: 'Credenciales inválidas' }); + + const token = signToken({ id: rows[0].id, role: 'superadmin' }); + return res.json({ token, role: 'superadmin' }); + } + + if (!company_code) return res.status(400).json({ error: 'company_code requerido' }); + + req.body.company_code = company_code; + await resolveTenant(req, res, async (err) => { + if (err) return next(err); + const pool = req.tenantPool; + const table = role === 'admin' ? 'admins' : 'users'; + const [rows] = await pool.query(`SELECT * FROM ${table} WHERE email = ? AND status = 'active'`, [email]); + if (!rows.length) return res.status(401).json({ error: 'Credenciales inválidas' }); + const ok = await comparePassword(password, rows[0].password_hash); + if (!ok) return res.status(401).json({ error: 'Credenciales inválidas' }); + + const token = signToken({ id: rows[0].id, role, company_code }); + return res.json({ token, role, company_code }); + }); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/backend/src/routes/reports.js b/backend/src/routes/reports.js new file mode 100644 index 0000000..94a0653 --- /dev/null +++ b/backend/src/routes/reports.js @@ -0,0 +1,31 @@ +const express = require('express'); +const { authRequired } = require('../middleware/auth'); +const { requireRole } = require('../middleware/rbac'); +const { resolveTenant } = require('../middleware/tenant'); +const { generatePdfReport, generateExcelReport } = require('../services/reportService'); + +const router = express.Router(); + +router.get('/pdf/:surveyPeriodId', authRequired, requireRole('admin'), resolveTenant, async (req, res, next) => { + try { + const buffer = await generatePdfReport(req.tenantPool, req.params.surveyPeriodId, req.company.name); + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', 'attachment; filename="reporte.pdf"'); + res.send(buffer); + } catch (err) { + next(err); + } +}); + +router.get('/excel/:surveyPeriodId', authRequired, requireRole('admin'), resolveTenant, async (req, res, next) => { + try { + const buffer = await generateExcelReport(req.tenantPool, req.params.surveyPeriodId); + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', 'attachment; filename="reporte.xlsx"'); + res.send(buffer); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/backend/src/routes/superadmin.js b/backend/src/routes/superadmin.js new file mode 100644 index 0000000..c692502 --- /dev/null +++ b/backend/src/routes/superadmin.js @@ -0,0 +1,180 @@ +const express = require('express'); +const { authRequired } = require('../middleware/auth'); +const { requireRole } = require('../middleware/rbac'); +const { getCentralPool } = require('../db/central'); +const { provisionTenant } = require('../services/tenantProvision'); +const { getTenantPool } = require('../db/tenant'); +const { hashPassword } = require('../utils/password'); +const { getParticipationSummary } = require('../services/participationService'); + +const router = express.Router(); + +router.get('/companies', authRequired, requireRole('superadmin'), async (req, res, next) => { + try { + const central = getCentralPool(); + const [rows] = await central.query('SELECT id, name, code, status, start_date, end_date, db_host, db_port, db_name, created_at FROM companies ORDER BY id DESC'); + res.json(rows); + } catch (err) { + next(err); + } +}); + +router.post('/companies', authRequired, requireRole('superadmin'), async (req, res, next) => { + try { + const { name, code, start_date, end_date, db_name, db_host, db_port, db_user, db_pass, admin_name, admin_email, admin_password } = req.body; + if (!name || !code || !db_name || !db_host || !db_user || !db_pass || !admin_name || !admin_email || !admin_password) { + return res.status(400).json({ error: 'Faltan campos requeridos' }); + } + if (!start_date || !end_date) { + return res.status(400).json({ error: 'Fecha inicio y fin son obligatorias' }); + } + if (start_date > end_date) { + return res.status(400).json({ error: 'Fecha inicio no puede ser mayor que fecha fin' }); + } + + await provisionTenant({ + name, + code, + dbName: db_name, + dbHost: db_host, + dbPort: db_port || 3306, + dbUser: db_user, + dbPass: db_pass, + adminName: admin_name, + adminEmail: admin_email, + adminPassword: admin_password, + startDate: start_date, + endDate: end_date + }); + + res.json({ ok: true }); + } catch (err) { + next(err); + } +}); + +router.patch('/companies/:id/status', authRequired, requireRole('superadmin'), async (req, res, next) => { + try { + const { status } = req.body; + if (!['active', 'suspended'].includes(status)) { + return res.status(400).json({ error: 'Estado inválido' }); + } + const central = getCentralPool(); + await central.query('UPDATE companies SET status = ? WHERE id = ?', [status, req.params.id]); + res.json({ ok: true }); + } catch (err) { + next(err); + } +}); + +router.patch('/companies/:id/dates', authRequired, requireRole('superadmin'), async (req, res, next) => { + try { + const { start_date, end_date } = req.body; + if (!start_date || !end_date) { + return res.status(400).json({ error: 'Fecha inicio y fin son obligatorias' }); + } + if (start_date > end_date) { + return res.status(400).json({ error: 'Fecha inicio no puede ser mayor que fecha fin' }); + } + const central = getCentralPool(); + await central.query('UPDATE companies SET start_date = ?, end_date = ? WHERE id = ?', [ + start_date || null, + end_date || null, + req.params.id + ]); + res.json({ ok: true }); + } catch (err) { + next(err); + } +}); + +router.get('/companies/:id/participation', authRequired, requireRole('superadmin'), async (req, res, next) => { + try { + const central = getCentralPool(); + const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]); + if (!company) return res.status(404).json({ error: 'Empresa no encontrada' }); + const tenantPool = getTenantPool(company); + const participation = await getParticipationSummary(tenantPool); + res.json({ company_id: company.id, participation }); + } catch (err) { + next(err); + } +}); + +router.delete('/companies/:id', authRequired, requireRole('superadmin'), async (req, res, next) => { + try { + const central = getCentralPool(); + const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]); + if (!company) return res.status(404).json({ error: 'Empresa no encontrada' }); + + const tenantPool = getTenantPool(company); + const [[{ periods }]] = await tenantPool.query('SELECT COUNT(*) AS periods FROM survey_periods'); + const [[{ responses }]] = await tenantPool.query('SELECT COUNT(*) AS responses FROM responses'); + + if (periods > 0 || responses > 0) { + return res.status(400).json({ error: 'No se puede eliminar: existen encuestas registradas' }); + } + + await central.query('DROP DATABASE IF EXISTS `' + company.db_name + '`'); + await central.query('DELETE FROM companies WHERE id = ?', [company.id]); + res.json({ ok: true }); + } catch (err) { + next(err); + } +}); + +router.post('/companies/:id/admins', authRequired, requireRole('superadmin'), async (req, res, next) => { + try { + const { name, email, password } = req.body; + if (!name || !email || !password) { + return res.status(400).json({ error: 'Nombre, email y password requeridos' }); + } + const central = getCentralPool(); + const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]); + if (!company) return res.status(404).json({ error: 'Empresa no encontrada' }); + + const tenantPool = getTenantPool(company); + const passwordHash = await hashPassword(password); + await tenantPool.query( + 'INSERT INTO admins (name, email, password_hash, role, status) VALUES (?, ?, ?, "admin", "active")', + [name, email, passwordHash] + ); + res.json({ ok: true }); + } catch (err) { + next(err); + } +}); + +router.get('/companies/:id/admins', authRequired, requireRole('superadmin'), async (req, res, next) => { + try { + const central = getCentralPool(); + const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]); + if (!company) return res.status(404).json({ error: 'Empresa no encontrada' }); + + const tenantPool = getTenantPool(company); + const [rows] = await tenantPool.query('SELECT id, name, email, status, created_at FROM admins ORDER BY id DESC'); + res.json(rows); + } catch (err) { + next(err); + } +}); + +router.patch('/companies/:id/admins/:adminId/status', authRequired, requireRole('superadmin'), async (req, res, next) => { + try { + const { status } = req.body; + if (!['active', 'inactive'].includes(status)) { + return res.status(400).json({ error: 'Estado inválido' }); + } + const central = getCentralPool(); + const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]); + if (!company) return res.status(404).json({ error: 'Empresa no encontrada' }); + + const tenantPool = getTenantPool(company); + await tenantPool.query('UPDATE admins SET status = ? WHERE id = ?', [status, req.params.adminId]); + res.json({ ok: true }); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/backend/src/routes/surveys.js b/backend/src/routes/surveys.js new file mode 100644 index 0000000..bfba86f --- /dev/null +++ b/backend/src/routes/surveys.js @@ -0,0 +1,58 @@ +const express = require('express'); +const { authRequired } = require('../middleware/auth'); +const { resolveTenant } = require('../middleware/tenant'); + +const router = express.Router(); + +router.get('/active', resolveTenant, async (req, res, next) => { + try { + const [[period]] = await req.tenantPool.query('SELECT * FROM survey_periods WHERE estado = "activa" ORDER BY id DESC LIMIT 1'); + if (!period) return res.json({ period: null, questions: [] }); + + const [questions] = await req.tenantPool.query( + 'SELECT sq.question_id, sq.question_code, sq.text_snapshot AS text, d.name AS dimension FROM survey_questions sq JOIN dimensions d ON d.id = sq.dimension_id WHERE sq.survey_period_id = ? ORDER BY sq.sort_order ASC', + [period.id] + ); + + res.json({ period, questions }); + } catch (err) { + next(err); + } +}); + +router.post('/responses', authRequired, resolveTenant, async (req, res, next) => { + try { + const { survey_period_id, answers } = req.body; + if (!survey_period_id || !Array.isArray(answers)) { + return res.status(400).json({ error: 'survey_period_id y answers requeridos' }); + } + const [[{ total_questions }]] = await req.tenantPool.query( + 'SELECT COUNT(*) AS total_questions FROM survey_questions WHERE survey_period_id = ?', + [survey_period_id] + ); + const uniqueIds = new Set(answers.map((a) => a.question_id)); + if (uniqueIds.size !== total_questions) { + return res.status(400).json({ error: 'Resultados incompletos' }); + } + + for (const ans of answers) { + if (![1,2,3,4,5].includes(Number(ans.value))) { + return res.status(400).json({ error: 'Valor inválido' }); + } + const [[qrow]] = await req.tenantPool.query( + 'SELECT question_code FROM survey_questions WHERE survey_period_id = ? AND question_id = ? LIMIT 1', + [survey_period_id, ans.question_id] + ); + await req.tenantPool.query( + 'INSERT INTO responses (survey_period_id, user_id, question_id, question_code, value) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value)', + [survey_period_id, req.user.id, ans.question_id, qrow?.question_code || '', ans.value] + ); + } + + res.json({ ok: true }); + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/backend/src/scripts/generate_templates.js b/backend/src/scripts/generate_templates.js new file mode 100644 index 0000000..462138d --- /dev/null +++ b/backend/src/scripts/generate_templates.js @@ -0,0 +1,21 @@ +const path = require('path'); +const fs = require('fs'); +const ExcelJS = require('exceljs'); + +async function run() { + const outDir = path.resolve(__dirname, '../../../templates'); + fs.mkdirSync(outDir, { recursive: true }); + + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet('Usuarios'); + sheet.addRow(['Nombre', 'Email', 'Departamento', 'Cargo']); + + const outPath = path.join(outDir, 'plantilla_usuarios.xlsx'); + await workbook.xlsx.writeFile(outPath); + console.log('Plantilla XLSX creada:', outPath); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/backend/src/scripts/seed_superadmin.js b/backend/src/scripts/seed_superadmin.js new file mode 100644 index 0000000..5cbd057 --- /dev/null +++ b/backend/src/scripts/seed_superadmin.js @@ -0,0 +1,27 @@ +const { getCentralPool } = require('../db/central'); +const { hashPassword } = require('../utils/password'); + +async function run() { + const name = process.argv[2]; + const email = process.argv[3]; + const password = process.argv[4]; + + if (!name || !email || !password) { + console.error('Uso: node seed_superadmin.js "Nombre" correo@example.com clave'); + process.exit(1); + } + + const passwordHash = await hashPassword(password); + const pool = getCentralPool(); + await pool.query( + 'INSERT INTO superadmins (name, email, password_hash, status) VALUES (?, ?, ?, "active")', + [name, email, passwordHash] + ); + console.log('Superadmin creado'); + process.exit(0); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/backend/src/server.js b/backend/src/server.js new file mode 100644 index 0000000..d6d4f7d --- /dev/null +++ b/backend/src/server.js @@ -0,0 +1,6 @@ +const app = require('./app'); +const { port } = require('./config/env'); + +app.listen(port, '0.0.0.0', () => { + console.log(`API escuchando en puerto ${port}`); +}); diff --git a/backend/src/services/importService.js b/backend/src/services/importService.js new file mode 100644 index 0000000..1c45935 --- /dev/null +++ b/backend/src/services/importService.js @@ -0,0 +1,41 @@ +const ExcelJS = require('exceljs'); + +async function importUsersFromExcel(buffer, pool) { + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(buffer); + const sheet = workbook.worksheets[0]; + const errors = []; + const inserted = []; + + if (!sheet) return { errors: ['No hay hoja en el archivo'], inserted: 0 }; + + sheet.eachRow((row, rowNumber) => { + if (rowNumber === 1) return; + const name = String(row.getCell(1).value || '').trim(); + const email = String(row.getCell(2).value || '').trim(); + const department = String(row.getCell(3).value || '').trim(); + const position = String(row.getCell(4).value || '').trim(); + + if (!name || !email) { + errors.push(`Fila ${rowNumber}: nombre y email son obligatorios`); + return; + } + + inserted.push({ name, email, department, position }); + }); + + for (const user of inserted) { + try { + await pool.query( + 'INSERT INTO users (name, email, department, position, status) VALUES (?, ?, ?, ?, "active")', + [user.name, user.email, user.department, user.position] + ); + } catch (err) { + errors.push(`Usuario ${user.email}: ${err.message}`); + } + } + + return { inserted: inserted.length - errors.length, errors }; +} + +module.exports = { importUsersFromExcel }; diff --git a/backend/src/services/participationService.js b/backend/src/services/participationService.js new file mode 100644 index 0000000..a67b4ea --- /dev/null +++ b/backend/src/services/participationService.js @@ -0,0 +1,69 @@ +async function getLatestSurveyPeriodId(pool) { + const [[active]] = await pool.query( + 'SELECT id FROM survey_periods WHERE estado = "activa" ORDER BY id DESC LIMIT 1' + ); + if (active && active.id) return active.id; + const [[closed]] = await pool.query( + 'SELECT id FROM survey_periods WHERE estado = "cerrada" ORDER BY id DESC LIMIT 1' + ); + return closed ? closed.id : null; +} + +async function getParticipationSummary(pool) { + const [[{ total_users }]] = await pool.query( + 'SELECT COUNT(*) AS total_users FROM users WHERE status = "active"' + ); + const surveyPeriodId = await getLatestSurveyPeriodId(pool); + if (!surveyPeriodId) { + return { + survey_period_id: null, + total_users, + total_questions: 0, + completed_users: 0, + partial_users: 0, + started_users: 0 + }; + } + + const [[{ total_questions }]] = await pool.query( + 'SELECT COUNT(*) AS total_questions FROM survey_questions WHERE survey_period_id = ?', + [surveyPeriodId] + ); + + if (!total_questions) { + return { + survey_period_id: surveyPeriodId, + total_users, + total_questions: 0, + completed_users: 0, + partial_users: 0, + started_users: 0 + }; + } + + const [[{ started_users }]] = await pool.query( + 'SELECT COUNT(DISTINCT user_id) AS started_users FROM responses WHERE survey_period_id = ?', + [surveyPeriodId] + ); + + const [[{ completed_users }]] = await pool.query( + 'SELECT COUNT(*) AS completed_users FROM (SELECT user_id, COUNT(*) c FROM responses WHERE survey_period_id = ? GROUP BY user_id HAVING c = ?) t', + [surveyPeriodId, total_questions] + ); + + const [[{ partial_users }]] = await pool.query( + 'SELECT COUNT(*) AS partial_users FROM (SELECT user_id, COUNT(*) c FROM responses WHERE survey_period_id = ? GROUP BY user_id HAVING c > 0 AND c < ?) t', + [surveyPeriodId, total_questions] + ); + + return { + survey_period_id: surveyPeriodId, + total_users, + total_questions, + completed_users, + partial_users, + started_users + }; +} + +module.exports = { getParticipationSummary }; diff --git a/backend/src/services/questionService.js b/backend/src/services/questionService.js new file mode 100644 index 0000000..bc4d753 --- /dev/null +++ b/backend/src/services/questionService.js @@ -0,0 +1,176 @@ +const ExcelJS = require('exceljs'); +const { createQuestionSet, createEmptyQuestionSet, getActiveQuestionSet } = require('./questionSetService'); + +async function listQuestions(pool) { + const [rows] = await pool.query( + 'SELECT q.id, q.code, q.text, q.is_active, d.name AS dimension, d.code AS dimension_code FROM questions q JOIN dimensions d ON d.id = q.dimension_id ORDER BY d.sort_order, q.id' + ); + return rows; +} + +async function listQuestionSets(pool) { + const [rows] = await pool.query('SELECT id, code, name, version, is_active, created_at FROM question_sets ORDER BY id DESC'); + return rows; +} + +async function listDimensions(pool) { + const [rows] = await pool.query('SELECT id, name, code, description, sort_order FROM dimensions ORDER BY sort_order, id'); + return rows; +} + +async function createDimension(pool, { name, code, description }) { + const finalCode = code || `DIM-${Date.now()}`; + const [res] = await pool.query( + 'INSERT INTO dimensions (name, code, description, sort_order) VALUES (?, ?, ?, ?)', + [name, finalCode, description || null, 0] + ); + return { id: res.insertId, code: finalCode, name }; +} + +async function exportQuestionsExcel(pool) { + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet('Preguntas'); + sheet.addRow(['question_code', 'dimension_code', 'dimension_name', 'text', 'is_active']); + + const rows = await listQuestions(pool); + rows.forEach((r) => { + sheet.addRow([r.code, r.dimension_code, r.dimension, r.text, r.is_active]); + }); + + return workbook.xlsx.writeBuffer(); +} + +async function exportQuestionsTemplate() { + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet('Preguntas'); + sheet.addRow(['question_code', 'dimension_code', 'dimension_name', 'text', 'is_active']); + sheet.addRow(['Q-001', 'DIM-LID', 'Liderazgo', 'Ejemplo de pregunta', 1]); + return workbook.xlsx.writeBuffer(); +} + +async function importQuestionsExcel(buffer, pool, setId) { + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(buffer); + const sheet = workbook.worksheets[0]; + const errors = []; + let processed = 0; + const setSortOrder = new Map(); + + if (!sheet) return { processed, errors: ['No hay hoja en el archivo'] }; + + const dimMap = new Map(); + const [dims] = await pool.query('SELECT id, code FROM dimensions'); + dims.forEach((d) => dimMap.set(String(d.code).trim(), d.id)); + + for (let i = 2; i <= sheet.rowCount; i++) { + const row = sheet.getRow(i); + const code = String(row.getCell(1).value || '').trim(); + const dimCode = String(row.getCell(2).value || '').trim(); + const text = String(row.getCell(4).value || '').trim(); + const isActiveRaw = row.getCell(5).value; + const isActive = String(isActiveRaw || '1').trim() === '0' ? 0 : 1; + + if (!code || !dimCode || !text) { + errors.push(`Fila ${i}: code, dimension_code y text son obligatorios`); + continue; + } + const dimId = dimMap.get(dimCode); + if (!dimId) { + errors.push(`Fila ${i}: dimension_code inválido (${dimCode})`); + continue; + } + + const [[existing]] = await pool.query('SELECT id, text FROM questions WHERE code = ?', [code]); + let questionId = existing?.id; + if (!existing) { + const [res] = await pool.query( + 'INSERT INTO questions (dimension_id, code, text, is_active) VALUES (?, ?, ?, ?)', + [dimId, code, text, isActive] + ); + questionId = res.insertId; + await pool.query( + 'INSERT INTO question_versions (question_id, version, text, is_active) VALUES (?, 1, ?, 1)', + [questionId, text] + ); + } else { + if (existing.text !== text) { + const [[ver]] = await pool.query('SELECT MAX(version) AS v FROM question_versions WHERE question_id = ?', [existing.id]); + const nextVer = (ver?.v || 0) + 1; + await pool.query( + 'INSERT INTO question_versions (question_id, version, text, is_active) VALUES (?, ?, ?, 1)', + [existing.id, nextVer, text] + ); + await pool.query('UPDATE questions SET text = ?, is_active = ? WHERE id = ?', [text, isActive, existing.id]); + } + } + if (setId && questionId) { + const [[{ existsInSet }]] = await pool.query( + 'SELECT COUNT(*) AS existsInSet FROM question_set_questions WHERE question_set_id = ? AND question_id = ?', + [setId, questionId] + ); + if (!existsInSet) { + let currentOrder = setSortOrder.get(setId); + if (!currentOrder) { + const [[{ maxOrder }]] = await pool.query( + 'SELECT COALESCE(MAX(sort_order), 0) AS maxOrder FROM question_set_questions WHERE question_set_id = ?', + [setId] + ); + currentOrder = maxOrder || 0; + } + currentOrder += 1; + setSortOrder.set(setId, currentOrder); + await pool.query( + 'INSERT INTO question_set_questions (question_set_id, question_id, question_code, dimension_id, text_snapshot, version, sort_order) VALUES (?, ?, ?, ?, ?, 1, ?)', + [setId, questionId, code, dimId, text, currentOrder] + ); + } + } + processed += 1; + } + + return { processed, errors }; +} + +async function createQuestionSetFromCurrent(pool, name) { + await getActiveQuestionSet(pool); + return createQuestionSet(pool, name); +} + +async function createQuestionSetEmpty(pool, name) { + return createEmptyQuestionSet(pool, name); +} + +async function addQuestionToSet(pool, { questionSetId, dimensionId, text }) { + const code = `Q-${Date.now()}`; + const [qRes] = await pool.query( + 'INSERT INTO questions (dimension_id, code, text, is_active) VALUES (?, ?, ?, 1)', + [dimensionId, code, text] + ); + await pool.query( + 'INSERT INTO question_versions (question_id, version, text, is_active) VALUES (?, 1, ?, 1)', + [qRes.insertId, text] + ); + const [[{ maxOrder }]] = await pool.query( + 'SELECT COALESCE(MAX(sort_order), 0) AS maxOrder FROM question_set_questions WHERE question_set_id = ?', + [questionSetId] + ); + const sortOrder = (maxOrder || 0) + 1; + await pool.query( + 'INSERT INTO question_set_questions (question_set_id, question_id, question_code, dimension_id, text_snapshot, version, sort_order) VALUES (?, ?, ?, ?, ?, 1, ?)', + [questionSetId, qRes.insertId, code, dimensionId, text, sortOrder] + ); + return { question_id: qRes.insertId, code, sort_order: sortOrder }; +} + +module.exports = { + listQuestions, + listQuestionSets, + listDimensions, + createDimension, + exportQuestionsExcel, + exportQuestionsTemplate, + importQuestionsExcel, + createQuestionSetFromCurrent, + createQuestionSetEmpty, + addQuestionToSet +}; diff --git a/backend/src/services/questionSetService.js b/backend/src/services/questionSetService.js new file mode 100644 index 0000000..53831f7 --- /dev/null +++ b/backend/src/services/questionSetService.js @@ -0,0 +1,68 @@ +const dayjs = require('dayjs'); + +async function ensureDefaultQuestionSet(pool) { + const [[exists]] = await pool.query('SELECT id, code FROM question_sets WHERE is_active = 1 ORDER BY id DESC LIMIT 1'); + if (exists && exists.id) return exists; + + const code = `QSET-${dayjs().format('YYYYMMDD')}-01`; + const name = `Set ${dayjs().format('YYYY-MM-DD')}`; + const [result] = await pool.query( + 'INSERT INTO question_sets (code, name, version, is_active) VALUES (?, ?, 1, 1)', + [code, name] + ); + const questionSetId = result.insertId; + const [questions] = await pool.query('SELECT id, code, dimension_id, text FROM questions WHERE is_active = 1 ORDER BY id ASC'); + for (let i = 0; i < questions.length; i++) { + const q = questions[i]; + await pool.query( + 'INSERT INTO question_set_questions (question_set_id, question_id, question_code, dimension_id, text_snapshot, version, sort_order) VALUES (?, ?, ?, ?, ?, 1, ?)', + [questionSetId, q.id, q.code, q.dimension_id, q.text, i + 1] + ); + } + return { id: questionSetId, code }; +} + +async function createQuestionSet(pool, name) { + const [[latest]] = await pool.query('SELECT version FROM question_sets ORDER BY id DESC LIMIT 1'); + const version = (latest?.version || 0) + 1; + const code = `QSET-${dayjs().format('YYYYMMDD')}-${String(version).padStart(2, '0')}`; + + await pool.query('UPDATE question_sets SET is_active = 0'); + const [result] = await pool.query( + 'INSERT INTO question_sets (code, name, version, is_active) VALUES (?, ?, ?, 1)', + [code, name || `Set ${dayjs().format('YYYY-MM-DD')}`, version] + ); + const questionSetId = result.insertId; + + const [questions] = await pool.query('SELECT id, code, dimension_id, text FROM questions WHERE is_active = 1 ORDER BY id ASC'); + for (let i = 0; i < questions.length; i++) { + const q = questions[i]; + await pool.query( + 'INSERT INTO question_set_questions (question_set_id, question_id, question_code, dimension_id, text_snapshot, version, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)', + [questionSetId, q.id, q.code, q.dimension_id, q.text, version, i + 1] + ); + } + + return { id: questionSetId, code, version }; +} + +async function createEmptyQuestionSet(pool, name) { + const [[latest]] = await pool.query('SELECT version FROM question_sets ORDER BY id DESC LIMIT 1'); + const version = (latest?.version || 0) + 1; + const code = `QSET-${dayjs().format('YYYYMMDD')}-${String(version).padStart(2, '0')}`; + + await pool.query('UPDATE question_sets SET is_active = 0'); + const [result] = await pool.query( + 'INSERT INTO question_sets (code, name, version, is_active) VALUES (?, ?, ?, 1)', + [code, name || `Set ${dayjs().format('YYYY-MM-DD')}`, version] + ); + return { id: result.insertId, code, version }; +} + +async function getActiveQuestionSet(pool) { + const [[active]] = await pool.query('SELECT id, code FROM question_sets WHERE is_active = 1 ORDER BY id DESC LIMIT 1'); + if (active && active.id) return active; + return ensureDefaultQuestionSet(pool); +} + +module.exports = { ensureDefaultQuestionSet, createQuestionSet, createEmptyQuestionSet, getActiveQuestionSet }; diff --git a/backend/src/services/reportService.js b/backend/src/services/reportService.js new file mode 100644 index 0000000..7d0f9a4 --- /dev/null +++ b/backend/src/services/reportService.js @@ -0,0 +1,69 @@ +const PDFDocument = require('pdfkit'); +const ExcelJS = require('exceljs'); + +async function generatePdfReport(pool, surveyPeriodId, companyName, logoPath) { + const [[period]] = await pool.query('SELECT * FROM survey_periods WHERE id = ?', [surveyPeriodId]); + const [dimensions] = await pool.query( + 'SELECT d.name, r.score, r.classification FROM survey_results r JOIN dimensions d ON d.id = r.dimension_id WHERE r.survey_period_id = ? AND r.scope = "dimension"', + [surveyPeriodId] + ); + const [[global]] = await pool.query( + 'SELECT score, classification, participation_rate FROM survey_results WHERE survey_period_id = ? AND scope = "global"', + [surveyPeriodId] + ); + + const doc = new PDFDocument({ margin: 50 }); + const chunks = []; + doc.on('data', (c) => chunks.push(c)); + + if (logoPath) { + try { doc.image(logoPath, 50, 40, { width: 80 }); } catch (_) {} + } + + doc.fontSize(18).text('Reporte de Clima Laboral', 150, 50); + doc.moveDown(); + doc.fontSize(12).text(`Empresa: ${companyName || '-'}`); + doc.text(`Código medición: ${period.codigo_medicion}`); + doc.text(`Periodo: ${period.fecha_inicio} a ${period.fecha_fin}`); + doc.text(`Participación: ${global ? global.participation_rate : 0}%`); + + doc.moveDown(); + doc.fontSize(14).text('Resumen Ejecutivo'); + doc.fontSize(11).text('El índice global refleja el nivel general de clima laboral. Se recomienda revisar dimensiones con puntajes bajos para generar planes de acción.'); + + doc.moveDown(); + doc.fontSize(14).text('Resultados por Dimensión'); + dimensions.forEach((d) => { + doc.fontSize(11).text(`${d.name}: ${d.score} (${d.classification})`); + }); + + doc.end(); + + return new Promise((resolve) => { + doc.on('end', () => resolve(Buffer.concat(chunks))); + }); +} + +async function generateExcelReport(pool, surveyPeriodId) { + const workbook = new ExcelJS.Workbook(); + const rawSheet = workbook.addWorksheet('Respuestas'); + const dimSheet = workbook.addWorksheet('Dimensiones'); + + rawSheet.addRow(['Usuario', 'Pregunta', 'Valor']); + const [rawRows] = await pool.query( + 'SELECT u.email, q.text, r.value FROM responses r JOIN users u ON u.id = r.user_id JOIN questions q ON q.id = r.question_id WHERE r.survey_period_id = ?', + [surveyPeriodId] + ); + rawRows.forEach((r) => rawSheet.addRow([r.email, r.text, r.value])); + + dimSheet.addRow(['Dimensión', 'Puntaje', 'Clasificación']); + const [dimRows] = await pool.query( + 'SELECT d.name, r.score, r.classification FROM survey_results r JOIN dimensions d ON d.id = r.dimension_id WHERE r.survey_period_id = ? AND r.scope = "dimension"', + [surveyPeriodId] + ); + dimRows.forEach((r) => dimSheet.addRow([r.name, r.score, r.classification])); + + return workbook.xlsx.writeBuffer(); +} + +module.exports = { generatePdfReport, generateExcelReport }; diff --git a/backend/src/services/resultService.js b/backend/src/services/resultService.js new file mode 100644 index 0000000..c4b6f38 --- /dev/null +++ b/backend/src/services/resultService.js @@ -0,0 +1,61 @@ +function scoreFromAvg(avg) { + if (avg === null || avg === undefined) return 0; + return ((avg - 1) / 4) * 100; +} + +function classify(score) { + if (score < 60) return 'Bajo'; + if (score < 80) return 'Medio'; + return 'Alto'; +} + +async function calculateResults(pool, surveyPeriodId) { + const [[{ total_users }]] = await pool.query('SELECT COUNT(*) AS total_users FROM users WHERE status = "active"'); + const [[{ participants }]] = await pool.query( + 'SELECT COUNT(DISTINCT user_id) AS participants FROM responses WHERE survey_period_id = ?', + [surveyPeriodId] + ); + const participationRate = total_users ? (participants / total_users) * 100 : 0; + + await pool.query('DELETE FROM survey_results WHERE survey_period_id = ?', [surveyPeriodId]); + + const [questionRows] = await pool.query( + 'SELECT question_id, AVG(value) AS avg_val, COUNT(*) AS responses FROM responses WHERE survey_period_id = ? GROUP BY question_id', + [surveyPeriodId] + ); + + for (const row of questionRows) { + const score = scoreFromAvg(row.avg_val); + await pool.query( + 'INSERT INTO survey_results (survey_period_id, scope, question_id, score, classification, response_count, participation_rate) VALUES (?, "question", ?, ?, ?, ?, ?)', + [surveyPeriodId, row.question_id, score.toFixed(2), classify(score), row.responses, participationRate.toFixed(2)] + ); + } + + const [dimensionRows] = await pool.query( + 'SELECT q.dimension_id, AVG(r.value) AS avg_val, COUNT(*) AS responses FROM responses r JOIN questions q ON q.id = r.question_id WHERE r.survey_period_id = ? GROUP BY q.dimension_id', + [surveyPeriodId] + ); + + for (const row of dimensionRows) { + const score = scoreFromAvg(row.avg_val); + await pool.query( + 'INSERT INTO survey_results (survey_period_id, scope, dimension_id, score, classification, response_count, participation_rate) VALUES (?, "dimension", ?, ?, ?, ?, ?)', + [surveyPeriodId, row.dimension_id, score.toFixed(2), classify(score), row.responses, participationRate.toFixed(2)] + ); + } + + const [[globalRow]] = await pool.query( + 'SELECT AVG(value) AS avg_val, COUNT(*) AS responses FROM responses WHERE survey_period_id = ?', + [surveyPeriodId] + ); + const globalScore = scoreFromAvg(globalRow.avg_val || 0); + await pool.query( + 'INSERT INTO survey_results (survey_period_id, scope, score, classification, response_count, participation_rate) VALUES (?, "global", ?, ?, ?, ?)', + [surveyPeriodId, globalScore.toFixed(2), classify(globalScore), globalRow.responses || 0, participationRate.toFixed(2)] + ); + + return { participationRate, globalScore }; +} + +module.exports = { calculateResults, classify }; diff --git a/backend/src/services/surveyService.js b/backend/src/services/surveyService.js new file mode 100644 index 0000000..8a685df --- /dev/null +++ b/backend/src/services/surveyService.js @@ -0,0 +1,37 @@ +const dayjs = require('dayjs'); +const { calculateResults } = require('./resultService'); +const { getActiveQuestionSet } = require('./questionSetService'); + +async function createSurveyPeriod(pool, { year, month, fecha_inicio, fecha_fin }) { + const code = `CLIMA-${year}-${String(month).padStart(2, '0')}`; + const activeSet = await getActiveQuestionSet(pool); + const [result] = await pool.query( + 'INSERT INTO survey_periods (codigo_medicion, question_set_code, year, month, fecha_inicio, fecha_fin, estado) VALUES (?, ?, ?, ?, ?, ?, "borrador")', + [code, activeSet.code, year, month, fecha_inicio, fecha_fin] + ); + + const surveyPeriodId = result.insertId; + const [setQuestions] = await pool.query( + 'SELECT question_id, question_code, dimension_id, text_snapshot, sort_order FROM question_set_questions WHERE question_set_id = ? ORDER BY sort_order ASC', + [activeSet.id] + ); + for (const sq of setQuestions) { + await pool.query( + 'INSERT INTO survey_questions (survey_period_id, question_id, question_code, dimension_id, text_snapshot, sort_order, question_set_code) VALUES (?, ?, ?, ?, ?, ?, ?)', + [surveyPeriodId, sq.question_id, sq.question_code, sq.dimension_id, sq.text_snapshot, sq.sort_order, activeSet.code] + ); + } + + return { id: surveyPeriodId, code }; +} + +async function activateSurvey(pool, surveyPeriodId) { + await pool.query('UPDATE survey_periods SET estado = "activa" WHERE id = ?', [surveyPeriodId]); +} + +async function closeSurvey(pool, surveyPeriodId) { + await pool.query('UPDATE survey_periods SET estado = "cerrada", fecha_cierre = ? WHERE id = ?', [dayjs().format('YYYY-MM-DD HH:mm:ss'), surveyPeriodId]); + return calculateResults(pool, surveyPeriodId); +} + +module.exports = { createSurveyPeriod, activateSurvey, closeSurvey }; diff --git a/backend/src/services/tenantProvision.js b/backend/src/services/tenantProvision.js new file mode 100644 index 0000000..1d68bc5 --- /dev/null +++ b/backend/src/services/tenantProvision.js @@ -0,0 +1,57 @@ +const fs = require('fs'); +const path = require('path'); +const { getCentralPool } = require('../db/central'); +const { hashPassword } = require('../utils/password'); + +function splitSql(sql) { + return sql + .split(/;\s*\n/) + .map((s) => s.trim()) + .filter((s) => s.length); +} + +async function runSqlStatements(pool, sqlText) { + const statements = splitSql(sqlText); + for (const stmt of statements) { + await pool.query(stmt); + } +} + +async function provisionTenant({ name, code, dbName, dbHost, dbPort, dbUser, dbPass, adminName, adminEmail, adminPassword, startDate, endDate }) { + const central = getCentralPool(); + + await central.query( + 'INSERT INTO companies (name, code, start_date, end_date, db_host, db_port, db_name, db_user, db_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [name, code, startDate || null, endDate || null, dbHost, dbPort, dbName, dbUser, dbPass] + ); + + await central.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`); + + const mysql = require('mysql2/promise'); + const tenantPool = mysql.createPool({ + host: dbHost, + port: dbPort, + user: dbUser, + password: dbPass, + database: dbName, + waitForConnections: true, + connectionLimit: 5 + }); + + const schemaSql = fs.readFileSync(path.join(__dirname, '../sql/tenant_schema.sql'), 'utf8'); + const seedSql = fs.readFileSync(path.join(__dirname, '../sql/seed_questions.sql'), 'utf8'); + + await runSqlStatements(tenantPool, schemaSql); + await runSqlStatements(tenantPool, seedSql); + + const passwordHash = await hashPassword(adminPassword); + await tenantPool.query( + 'INSERT INTO admins (name, email, password_hash, role, status) VALUES (?, ?, ?, "admin", "active")', + [adminName, adminEmail, passwordHash] + ); + + await tenantPool.end(); + return true; +} + +module.exports = { provisionTenant }; diff --git a/backend/src/sql/central_schema.sql b/backend/src/sql/central_schema.sql new file mode 100644 index 0000000..36f1255 --- /dev/null +++ b/backend/src/sql/central_schema.sql @@ -0,0 +1,36 @@ +CREATE DATABASE IF NOT EXISTS clima_platform CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE clima_platform; + +CREATE TABLE IF NOT EXISTS superadmins ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + email VARCHAR(160) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + status ENUM('active','inactive') NOT NULL DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS companies ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(160) NOT NULL, + code VARCHAR(40) NOT NULL UNIQUE, + status ENUM('active','suspended') NOT NULL DEFAULT 'active', + start_date DATE NULL, + end_date DATE NULL, + db_host VARCHAR(120) NOT NULL, + db_port INT NOT NULL DEFAULT 3306, + db_name VARCHAR(120) NOT NULL, + db_user VARCHAR(120) NOT NULL, + db_pass VARCHAR(120) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS error_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_role VARCHAR(40) NULL, + user_id INT NULL, + company_code VARCHAR(40) NULL, + menu VARCHAR(120) NULL, + error_message TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/backend/src/sql/seed_questions.sql b/backend/src/sql/seed_questions.sql new file mode 100644 index 0000000..e99a871 --- /dev/null +++ b/backend/src/sql/seed_questions.sql @@ -0,0 +1,41 @@ +INSERT INTO dimensions (name, code, description, sort_order) VALUES +('Liderazgo', 'LID', 'Estilo de dirección y apoyo del liderazgo', 1), +('Comunicación', 'COM', 'Flujo, claridad y oportunidad de la información', 2), +('Trabajo en equipo', 'TEA', 'Colaboración y apoyo entre áreas', 3), +('Reconocimiento', 'REC', 'Valoración del desempeño y esfuerzos', 4), +('Desarrollo profesional', 'DES', 'Oportunidades de crecimiento y aprendizaje', 5), +('Cultura organizacional', 'CUL', 'Valores compartidos y ambiente laboral', 6); + +INSERT INTO scales (name, min_value, max_value, labels_json) VALUES +('Likert 1-5', 1, 5, '{"1":"Totalmente en desacuerdo","2":"En desacuerdo","3":"Ni de acuerdo ni en desacuerdo","4":"De acuerdo","5":"Totalmente de acuerdo"}'); + +INSERT INTO questions (dimension_id, code, text) VALUES +(1, 'LID-01', 'Mi líder comunica expectativas claras sobre mi trabajo.'), +(1, 'LID-02', 'Recibo apoyo oportuno de mi líder cuando lo necesito.'), +(1, 'LID-03', 'Las decisiones de liderazgo se perciben justas y consistentes.'), +(1, 'LID-04', 'Mi líder promueve un ambiente de confianza y respeto.'), + +(2, 'COM-01', 'La información importante llega a tiempo para hacer bien mi trabajo.'), +(2, 'COM-02', 'La comunicación entre áreas es clara y efectiva.'), +(2, 'COM-03', 'Puedo expresar mis ideas y ser escuchado/a.'), +(2, 'COM-04', 'Los cambios relevantes se comunican de manera adecuada.'), + +(3, 'TEA-01', 'En mi equipo se fomenta la colaboración para lograr objetivos.'), +(3, 'TEA-02', 'Mis compañeros/as están dispuestos a ayudar cuando se requiere.'), +(3, 'TEA-03', 'Existe coordinación efectiva entre roles dentro del equipo.'), +(3, 'TEA-04', 'Se resuelven los conflictos de forma constructiva.'), + +(4, 'REC-01', 'Mi trabajo es reconocido de manera oportuna.'), +(4, 'REC-02', 'El reconocimiento recibido es coherente con mi desempeño.'), +(4, 'REC-03', 'La organización valora los esfuerzos adicionales.'), +(4, 'REC-04', 'Se celebran los logros del equipo.'), + +(5, 'DES-01', 'Tengo oportunidades reales de crecimiento profesional.'), +(5, 'DES-02', 'Recibo retroalimentación que me ayuda a mejorar.'), +(5, 'DES-03', 'La organización promueve el aprendizaje continuo.'), +(5, 'DES-04', 'Dispongo de recursos para desarrollar nuevas habilidades.'), + +(6, 'CUL-01', 'Me siento orgulloso/a de pertenecer a esta organización.'), +(6, 'CUL-02', 'Los valores de la empresa se reflejan en las decisiones diarias.'), +(6, 'CUL-03', 'El ambiente laboral es respetuoso e inclusivo.'), +(6, 'CUL-04', 'Se promueve el bienestar de las personas.'); diff --git a/backend/src/sql/tenant_init.sql b/backend/src/sql/tenant_init.sql new file mode 100644 index 0000000..2b13e3d --- /dev/null +++ b/backend/src/sql/tenant_init.sql @@ -0,0 +1,4 @@ +SET FOREIGN_KEY_CHECKS=0; +SOURCE tenant_schema.sql; +SOURCE seed_questions.sql; +SET FOREIGN_KEY_CHECKS=1; diff --git a/backend/src/sql/tenant_migration_questions.sql b/backend/src/sql/tenant_migration_questions.sql new file mode 100644 index 0000000..022ea97 --- /dev/null +++ b/backend/src/sql/tenant_migration_questions.sql @@ -0,0 +1,77 @@ +-- Add columns +ALTER TABLE dimensions ADD COLUMN IF NOT EXISTS code VARCHAR(40) NULL; +ALTER TABLE questions ADD COLUMN IF NOT EXISTS code VARCHAR(60) NULL; +ALTER TABLE survey_periods ADD COLUMN IF NOT EXISTS question_set_code VARCHAR(60) NULL; +ALTER TABLE survey_questions ADD COLUMN IF NOT EXISTS question_code VARCHAR(60) NULL; +ALTER TABLE survey_questions ADD COLUMN IF NOT EXISTS text_snapshot VARCHAR(500) NULL; +ALTER TABLE survey_questions ADD COLUMN IF NOT EXISTS question_set_code VARCHAR(60) NULL; +ALTER TABLE responses ADD COLUMN IF NOT EXISTS question_code VARCHAR(60) NULL; +ALTER TABLE question_versions ADD COLUMN IF NOT EXISTS changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; + +-- Create new tables +CREATE TABLE IF NOT EXISTS question_sets ( + id INT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(60) NOT NULL UNIQUE, + name VARCHAR(120) NOT NULL, + version INT NOT NULL DEFAULT 1, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS question_set_questions ( + id INT AUTO_INCREMENT PRIMARY KEY, + question_set_id INT NOT NULL, + question_id INT NOT NULL, + question_code VARCHAR(60) NOT NULL, + dimension_id INT NOT NULL, + text_snapshot VARCHAR(500) NOT NULL, + version INT NOT NULL DEFAULT 1, + sort_order INT NOT NULL DEFAULT 0, + changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_set_id) REFERENCES question_sets(id), + FOREIGN KEY (question_id) REFERENCES questions(id), + FOREIGN KEY (dimension_id) REFERENCES dimensions(id) +); + +-- Populate dimension codes if empty +UPDATE dimensions SET code = 'LID' WHERE name='Liderazgo' AND (code IS NULL OR code=''); +UPDATE dimensions SET code = 'COM' WHERE name='Comunicación' AND (code IS NULL OR code=''); +UPDATE dimensions SET code = 'TEA' WHERE name='Trabajo en equipo' AND (code IS NULL OR code=''); +UPDATE dimensions SET code = 'REC' WHERE name='Reconocimiento' AND (code IS NULL OR code=''); +UPDATE dimensions SET code = 'DES' WHERE name='Desarrollo profesional' AND (code IS NULL OR code=''); +UPDATE dimensions SET code = 'CUL' WHERE name='Cultura organizacional' AND (code IS NULL OR code=''); + +-- Populate question codes +SET @n:=0; +UPDATE questions q JOIN dimensions d ON d.id=q.dimension_id SET q.code = CONCAT(d.code, '-', LPAD((@n:=@n+1),2,'0')) WHERE d.code='LID' AND (q.code IS NULL OR q.code='') ORDER BY q.id; +SET @n:=0; +UPDATE questions q JOIN dimensions d ON d.id=q.dimension_id SET q.code = CONCAT(d.code, '-', LPAD((@n:=@n+1),2,'0')) WHERE d.code='COM' AND (q.code IS NULL OR q.code='') ORDER BY q.id; +SET @n:=0; +UPDATE questions q JOIN dimensions d ON d.id=q.dimension_id SET q.code = CONCAT(d.code, '-', LPAD((@n:=@n+1),2,'0')) WHERE d.code='TEA' AND (q.code IS NULL OR q.code='') ORDER BY q.id; +SET @n:=0; +UPDATE questions q JOIN dimensions d ON d.id=q.dimension_id SET q.code = CONCAT(d.code, '-', LPAD((@n:=@n+1),2,'0')) WHERE d.code='REC' AND (q.code IS NULL OR q.code='') ORDER BY q.id; +SET @n:=0; +UPDATE questions q JOIN dimensions d ON d.id=q.dimension_id SET q.code = CONCAT(d.code, '-', LPAD((@n:=@n+1),2,'0')) WHERE d.code='DES' AND (q.code IS NULL OR q.code='') ORDER BY q.id; +SET @n:=0; +UPDATE questions q JOIN dimensions d ON d.id=q.dimension_id SET q.code = CONCAT(d.code, '-', LPAD((@n:=@n+1),2,'0')) WHERE d.code='CUL' AND (q.code IS NULL OR q.code='') ORDER BY q.id; + +-- Default question set +INSERT INTO question_sets (code, name, version, is_active) +SELECT 'QSET-DEFAULT', 'Set inicial', 1, 1 +WHERE NOT EXISTS (SELECT 1 FROM question_sets); + +SET @setId := (SELECT id FROM question_sets WHERE is_active=1 ORDER BY id DESC LIMIT 1); + +INSERT INTO question_set_questions (question_set_id, question_id, question_code, dimension_id, text_snapshot, version, sort_order) +SELECT @setId, q.id, q.code, q.dimension_id, q.text, 1, q.id +FROM questions q +WHERE NOT EXISTS (SELECT 1 FROM question_set_questions WHERE question_set_id=@setId); + +-- Backfill survey_periods and survey_questions +UPDATE survey_periods SET question_set_code = 'QSET-DEFAULT' WHERE question_set_code IS NULL OR question_set_code=''; +UPDATE survey_questions sq JOIN questions q ON q.id = sq.question_id SET sq.question_code = q.code, sq.text_snapshot = q.text, sq.question_set_code = 'QSET-DEFAULT' WHERE sq.question_code IS NULL OR sq.text_snapshot IS NULL OR sq.question_set_code IS NULL; + +-- Backfill responses +UPDATE responses r JOIN questions q ON q.id=r.question_id SET r.question_code = q.code WHERE r.question_code IS NULL OR r.question_code=''; + +-- Add unique constraints if not exists (run separately if needed) diff --git a/backend/src/sql/tenant_schema.sql b/backend/src/sql/tenant_schema.sql new file mode 100644 index 0000000..74d0638 --- /dev/null +++ b/backend/src/sql/tenant_schema.sql @@ -0,0 +1,159 @@ +CREATE TABLE IF NOT EXISTS admins ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + email VARCHAR(160) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + role ENUM('admin') NOT NULL DEFAULT 'admin', + status ENUM('active','inactive') NOT NULL DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + email VARCHAR(160) NOT NULL UNIQUE, + password_hash VARCHAR(255) NULL, + department VARCHAR(120), + position VARCHAR(120), + status ENUM('active','inactive') NOT NULL DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS survey_periods ( + id INT AUTO_INCREMENT PRIMARY KEY, + codigo_medicion VARCHAR(40) NOT NULL UNIQUE, + question_set_code VARCHAR(60) NOT NULL, + year INT NOT NULL, + month INT NOT NULL, + fecha_inicio DATE NOT NULL, + fecha_fin DATE NOT NULL, + estado ENUM('borrador','activa','cerrada') NOT NULL DEFAULT 'borrador', + fecha_cierre DATETIME NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS dimensions ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + code VARCHAR(40) NOT NULL UNIQUE, + description VARCHAR(255), + sort_order INT NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS questions ( + id INT AUTO_INCREMENT PRIMARY KEY, + dimension_id INT NOT NULL, + code VARCHAR(60) NOT NULL UNIQUE, + text VARCHAR(500) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (dimension_id) REFERENCES dimensions(id) +); + +CREATE TABLE IF NOT EXISTS survey_questions ( + id INT AUTO_INCREMENT PRIMARY KEY, + survey_period_id INT NOT NULL, + question_id INT NOT NULL, + question_code VARCHAR(60) NOT NULL, + dimension_id INT NOT NULL, + text_snapshot VARCHAR(500) NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + question_set_code VARCHAR(60) NOT NULL, + FOREIGN KEY (survey_period_id) REFERENCES survey_periods(id), + FOREIGN KEY (question_id) REFERENCES questions(id), + FOREIGN KEY (dimension_id) REFERENCES dimensions(id) +); + +CREATE TABLE IF NOT EXISTS responses ( + id INT AUTO_INCREMENT PRIMARY KEY, + survey_period_id INT NOT NULL, + user_id INT NOT NULL, + question_id INT NOT NULL, + question_code VARCHAR(60) NOT NULL, + value INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uniq_response (survey_period_id, user_id, question_id), + FOREIGN KEY (survey_period_id) REFERENCES survey_periods(id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (question_id) REFERENCES questions(id) +); + +CREATE TABLE IF NOT EXISTS survey_results ( + id INT AUTO_INCREMENT PRIMARY KEY, + survey_period_id INT NOT NULL, + scope ENUM('global','dimension','question') NOT NULL, + dimension_id INT NULL, + question_id INT NULL, + score DECIMAL(5,2) NOT NULL, + classification ENUM('Bajo','Medio','Alto') NOT NULL, + response_count INT NOT NULL DEFAULT 0, + participation_rate DECIMAL(5,2) NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (survey_period_id) REFERENCES survey_periods(id) +); + +CREATE TABLE IF NOT EXISTS scales ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + min_value INT NOT NULL, + max_value INT NOT NULL, + labels_json JSON NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1 +); + +CREATE TABLE IF NOT EXISTS question_versions ( + id INT AUTO_INCREMENT PRIMARY KEY, + question_id INT NOT NULL, + version INT NOT NULL, + text VARCHAR(500) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_id) REFERENCES questions(id) +); + +CREATE TABLE IF NOT EXISTS question_sets ( + id INT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(60) NOT NULL UNIQUE, + name VARCHAR(120) NOT NULL, + version INT NOT NULL DEFAULT 1, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS question_set_questions ( + id INT AUTO_INCREMENT PRIMARY KEY, + question_set_id INT NOT NULL, + question_id INT NOT NULL, + question_code VARCHAR(60) NOT NULL, + dimension_id INT NOT NULL, + text_snapshot VARCHAR(500) NOT NULL, + version INT NOT NULL DEFAULT 1, + sort_order INT NOT NULL DEFAULT 0, + changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_set_id) REFERENCES question_sets(id), + FOREIGN KEY (question_id) REFERENCES questions(id), + FOREIGN KEY (dimension_id) REFERENCES dimensions(id) +); + +CREATE TABLE IF NOT EXISTS question_weights ( + id INT AUTO_INCREMENT PRIMARY KEY, + question_id INT NOT NULL, + weight DECIMAL(5,2) NOT NULL DEFAULT 1, + is_active TINYINT(1) NOT NULL DEFAULT 1, + FOREIGN KEY (question_id) REFERENCES questions(id) +); + +CREATE TABLE IF NOT EXISTS metric_definitions ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + formula TEXT NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1 +); + +CREATE TABLE IF NOT EXISTS survey_rules ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + rule_json JSON NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1 +); diff --git a/backend/src/utils/jwt.js b/backend/src/utils/jwt.js new file mode 100644 index 0000000..89bde5c --- /dev/null +++ b/backend/src/utils/jwt.js @@ -0,0 +1,12 @@ +const jwt = require('jsonwebtoken'); +const { jwtSecret } = require('../config/env'); + +function signToken(payload) { + return jwt.sign(payload, jwtSecret, { expiresIn: '8h' }); +} + +function verifyToken(token) { + return jwt.verify(token, jwtSecret); +} + +module.exports = { signToken, verifyToken }; diff --git a/backend/src/utils/password.js b/backend/src/utils/password.js new file mode 100644 index 0000000..74ac008 --- /dev/null +++ b/backend/src/utils/password.js @@ -0,0 +1,12 @@ +const bcrypt = require('bcryptjs'); + +async function hashPassword(password) { + const salt = await bcrypt.genSalt(10); + return bcrypt.hash(password, salt); +} + +async function comparePassword(password, hash) { + return bcrypt.compare(password, hash); +} + +module.exports = { hashPassword, comparePassword }; diff --git a/desarrollo/PARA_NUEVO_CHAT.md b/desarrollo/PARA_NUEVO_CHAT.md new file mode 100644 index 0000000..ac8826c --- /dev/null +++ b/desarrollo/PARA_NUEVO_CHAT.md @@ -0,0 +1,66 @@ +# Continuación proyecto Clima Laboral SaaS (Resumen actualizado) + +Proyecto en `/var/www/react/clima_laboral` con backend Node/Express y frontend React/Vite (sin TS). MariaDB multi-tenant: DB central `clima_platform` + DB por empresa (ej. `P123456`). Usuario DB correcto: `user_ia` (host `%`), pass `UserIA2026`, host DB `192.168.222.20:3306`. + +## Backend (estado) +- Rutas admin: + - `/admin/questions`, `/admin/questions/export`, `/admin/questions/import` + - `/admin/questions/template` (descarga template Excel) + - `/admin/question-sets`, `/admin/question-sets` POST (`empty` flag) + - `/admin/question-sets/:id` DELETE (solo si inactivo y sin survey_periods) + - `/admin/question-sets/:id/questions` GET + POST (agregar pregunta a set) + - `/admin/dimensions` GET + POST +- Servicios: + - `questionSetService.js` agrega `createEmptyQuestionSet` + - `questionService.js` agrega `listDimensions`, `createDimension`, `addQuestionToSet`, `exportQuestionsTemplate` + - `importQuestionsExcel` ahora acepta `setId` para cargar preguntas dentro del set +- Error logging en `error_logs` para no superadmin. + +## Frontend (UI/UX) +- Estilos globales ya cargados: `frontend/src/main.jsx` importa `./styles/app.css`. +- Menú superior usa `NavLink`; botón activo se resalta (`.topbar nav a.active`). +- Iconos Font Awesome (tamaño 16px en botones). Sin sesión no se ven iconos. +- Login: al iniciar sesión desaparece el formulario y muestra imagen. +- Popups globales de error. +- Anti‑spam de mensajes en `frontend/src/utils/api.js`. +- `/admin/dimensions` 404 ya no genera popup (silenciado). +- `QuestionBank.jsx`: + - Subnav: Versiones + botón Excel template + botón “+”. + - “+” abre vista Nuevo Set con nombre sugerido `QSET-YYYYMMDD-XX` basado en máximo existente. + - Vista Nuevo Set: crear set, importar Excel, crear ámbitos y agregar preguntas. + - Lista de preguntas se muestra en vivo en nuevo/editar. + - Vista Editar Set (botón en cada set) permite importar Excel, crear ámbitos y agregar preguntas manuales. + - Vista Abrir set muestra solo preguntas. + - Si no hay `company_code`, muestra mensaje “Selecciona una empresa...”. +- `QuestionBank` no rompe si `/admin/dimensions` falla. + +## Estándares UI (acordados) +- Todos los botones son **solo iconos** (Font Awesome), sin texto visible. +- Tooltips visibles al hover (título del icono). +- Tamaño de icono estándar: **16px** en todos los botones. +- Iconos activos resaltados con color de acento. +- Menús ocultos cuando no hay sesión (sin token). +- Submenús con botones redondos iguales a los de Empresas. +- Formularios y listados separados por vista (no mezclar en la misma pantalla). +- Fechas visibles en formato `YYYY-MM-DD`. + +## Datos actuales +- Empresa: `EMPRESA DE PRUEBA`, code `123456`, DB `P123456`, status active. +- Superadmin: `superadmin@demo.com` / `SuperAdmin2026` +- Admin: `admin.prueba@demo.com` / `Admin2026!` +- User: `usuario.prueba@demo.com` / `Usuario2026!` +- Question sets en DB `P123456`: `QSET-DEFAULT`, `QSET-20260303-03`, `QSET-20260303-04`, `QSET-20260303-05` (algunos activos/inactivos). + +## Problemas frecuentes +- Si “Versiones de Encuesta” sale vacío, es porque no se entró con `company_code` (usar Superadmin > Empresas > Acceder). +- Si `/admin/dimensions` da 404, reiniciar backend para cargar rutas nuevas. + +## Últimos cambios recientes +- Botón Excel Template movido arriba junto a “+”. +- Sugerencia de nombre de set corregida para usar máximo existente + 1. +- Vista Editar Set agregada con lista de preguntas y edición. +- Popup 404 silenciado para `/admin/dimensions`. + +## Pendientes +- A gusto del usuario, agregar etiqueta “Escala 1–5” visible en encuestas/preguntas. +- Si necesita renombrar set o eliminar preguntas, implementar. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..32dd686 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + Clima Laboral SaaS + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..910d9ec --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2009 @@ +{ + "name": "clima-laboral-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clima-laboral-frontend", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.7", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-icons": "^5.5.0", + "react-router-dom": "^6.26.2" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.2", + "vite": "^5.4.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "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" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..4c6214f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "clima-laboral-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.7", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-icons": "^5.5.0", + "react-router-dom": "^6.26.2" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.2", + "vite": "^5.4.8" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..692fbc2 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,166 @@ +import { BrowserRouter, Routes, Route, NavLink, Navigate } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import LoginPage from './pages/LoginPage.jsx'; +import SuperadminCompanies from './pages/SuperadminCompanies.jsx'; +import AdminDashboard from './pages/AdminDashboard.jsx'; +import SurveyPeriods from './pages/SurveyPeriods.jsx'; +import ImportUsers from './pages/ImportUsers.jsx'; +import ParticipantSurvey from './pages/ParticipantSurvey.jsx'; +import QuestionBank from './pages/QuestionBank.jsx'; +import creatorLogo from './assets/creator-logo.png'; +import './styles/app.css'; + +export default function App() { + const [notice, setNotice] = useState(null); + const [role, setRole] = useState(localStorage.getItem('role')); + const [token, setToken] = useState(localStorage.getItem('token')); + + useEffect(() => { + const handler = (e) => setNotice(e.detail); + window.addEventListener('app:notice', handler); + return () => window.removeEventListener('app:notice', handler); + }, []); + + useEffect(() => { + const onStorage = () => setRole(localStorage.getItem('role')); + const onAuth = () => setRole(localStorage.getItem('role')); + window.addEventListener('storage', onStorage); + window.addEventListener('auth:changed', onAuth); + return () => { + window.removeEventListener('storage', onStorage); + window.removeEventListener('auth:changed', onAuth); + }; + }, []); + + useEffect(() => { + const onAuth = () => setToken(localStorage.getItem('token')); + window.addEventListener('auth:changed', onAuth); + return () => window.removeEventListener('auth:changed', onAuth); + }, []); + + const hasSession = !!token && !!role; + + useEffect(() => { + if (!token) { + setRole(null); + } + }, [token]); + + const Guard = ({ allow, children }) => { + if (!allow) return ; + return children; + }; + + return ( + +
+
+
+ Empresa creadora +

Clima Laboral SaaS

+
+ {hasSession && ( + + )} +
+ {notice && ( +
+
+

Notificación

+

{notice.message}

+ +
+
+ )} +
+ + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + +
+
+
+ ); +} diff --git a/frontend/src/assets/creator-logo.png b/frontend/src/assets/creator-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..aee4086e508e5fa8455793eba459b3d6ec21d21a GIT binary patch literal 2105 zcmd5;`#%#3AD%_gDr=NEl-yOwx{zpcrUDm4|AY7Qem|e*`~32JzCV4R&+}XlM)+>t_{&BB0I=EL z&nt9|zpf!zd;QwB_Fqn0Bb`LQuw(!Lr1uXr0QXCw0KkT6e=ql6iKVk86Q}e8!GCam zx&V@QWpDkp!?~gY3(%TAGwvcWjtlDQtUHi4z2z^iTiLpRNn}FG$;HIBjyH!^X@#^v{q zgL7^9T9sQeX+)ExlK!;YLwI*+<)=gZSBVOS6MLSs;Vz6OR!+ z;VqY*v?Na;7RSVeWQo1K{i)>SiJUOM`3i9{-0y8tJq-zO@^1BVI3rI~&~ zyEkjWsLVBWo+ln07^v>;?X6Cp=aEu9(+IF5j?LL;hoi@Er0J97w*&jP-_LQX`1`M`Q0~^pD@C$ zL6o`nAJGMYK#Qe;ubv{2$k_M|@o6gg@Ww@T;tKp{A-pwD>~>T{cVaT*B#n2Ja&Azm^fKm@>HDF-*v zqH#C|*LPFjDJR7RJpNQ|1gF}0vfp33XeIWkRC*8*2wb9kihkE*e|c*a(V0wW;^I8| zYtcNWfhMG&@>sKp=i~DDMRfidzOD(}I1n-yM^7oyq%E`AkAvvBOxb+uj%=a9WYY(BDR=WKsax}cLPyRCA~lUs`}Y@jyG6I$i4lm%?lsZk^$Gu~wLM*S00nN-A2jyHYb~ zbbIV3+m^|hG-!>2qNa`IxnQTvF>L^XJ0wYt#j^2z*$@5sG#yyQ2+sBs_7MKUZ>O6* zfr|y@xsoMAy_I9w0u;PuAlYIhg%0c2R{De+ zE9#3ND_*s)UsAA4bMq%!Gi@0~k!AO&j8^0?maR8N;*KcKosKMIAcnlsU#RzA2n9wr zm22jQ2B8mVM!nvc=c9*oL{@0kVP#bPB2=4KCAH5MkRpQSP% zilx6@EYo@Q#|v5Uw!~6xLC@m9aW)zvWWkQ>-q&zo!j*K>kE{b-=m9?~XE`3FH6!b> zL?m2QyNCJGVWuHdW)mIhWHKjLaq-)#Hq_P8igC1YhvXSGtOsY7^Pp;0@8~8=tKcd- zOgPdBPPhg2#b(~#?to`1TA~Op!a|g0NA7fd%+QweCU`rL?|bZx!<;RGzLWsGdQj|HA09CJdrarMu$C4?6N#S@9p8bV;PWT$)o1P)I4w5~kCohCEw*Ug z5+!8h!h!g8S9V#sDZLMOUzQ1Pl1j1UHOD4n28?hMRY4H*nU)Y@;M;pei8BYRU|X!C zBT%TC)#G&b1AEMPt(`oDm>xTLm->@`9ufLNT#k*LT2a0t?%U!Hk+^FWgd-pzX8jbLvT^WQ# zEIjx&n`BJrc6J2jt`}rN3}9E{4NrJwxeJSmQ<~SqLS8XZ@Uw6<;azvCxFqtAlo?S|!pIlZip2(VTYa`0lLp7NDH?NPnsS=$ChR)3 v#u~r@GLzj^Z|e2xz>c2(FG={{_?>jD6OZ-f`q1C#L|9;*Z! literal 0 HcmV?d00001 diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..3fe4a20 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.jsx'; +import './styles/app.css'; + +createRoot(document.getElementById('root')).render(); diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx new file mode 100644 index 0000000..3cc9cdd --- /dev/null +++ b/frontend/src/pages/AdminDashboard.jsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; +import api from '../utils/api'; + +export default function AdminDashboard() { + const [data, setData] = useState({ current: null, dimensions: [], history: [], participation: null }); + + useEffect(() => { + api.get('/admin/dashboard').then((res) => setData(res.data)).catch(() => {}); + }, []); + + return ( +
+
+

Índice Global

+

{data.current ? `${data.current.score} (${data.current.classification})` : 'Sin datos'}

+

Participación: {data.current ? `${data.current.participation_rate}%` : '-'}

+
+
+

Avance de Encuestas

+

Registrados: {data.participation ? data.participation.total_users : 0}

+

Completaron 100%: {data.participation ? data.participation.completed_users : 0}

+

Menos del 100%: {data.participation ? data.participation.partial_users : 0}

+
+
+

Resultados por Dimensión

+ {data.dimensions.map((d) => ( +

{d.name}: {d.score} ({d.classification})

+ ))} +
+
+

Histórico

+ {data.history.map((h) => ( +

{h.codigo_medicion}: {h.score}

+ ))} +
+
+ ); +} diff --git a/frontend/src/pages/ImportUsers.jsx b/frontend/src/pages/ImportUsers.jsx new file mode 100644 index 0000000..edfbc34 --- /dev/null +++ b/frontend/src/pages/ImportUsers.jsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import api from '../utils/api'; + +export default function ImportUsers() { + const [file, setFile] = useState(null); + const [result, setResult] = useState(null); + + const upload = async () => { + if (!file) return; + const form = new FormData(); + form.append('file', file); + const res = await api.post('/admin/users/import', form, { headers: { 'Content-Type': 'multipart/form-data' } }); + setResult(res.data); + }; + + return ( +
+

Importar Usuarios

+ setFile(e.target.files[0])} /> + + {result && ( +
+

Insertados: {result.inserted}

+ {result.errors?.length > 0 && ( +
    + {result.errors.map((e, i) =>
  • {e}
  • )} +
+ )} +
+ )} +
+ ); +} diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx new file mode 100644 index 0000000..e46572a --- /dev/null +++ b/frontend/src/pages/LoginPage.jsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import api from '../utils/api'; +import creatorLogo from '../assets/creator-logo.png'; + +export default function LoginPage() { + const [form, setForm] = useState({ role: 'admin', email: '', password: '', company_code: '' }); + const [msg, setMsg] = useState(''); + const navigate = useNavigate(); + const hasSession = !!localStorage.getItem('token'); + + const handleChange = (e) => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + const login = async () => { + try { + const res = await api.post('/auth/login', form); + localStorage.setItem('token', res.data.token); + localStorage.setItem('role', res.data.role); + if (res.data.company_code) localStorage.setItem('company_code', res.data.company_code); + window.dispatchEvent(new Event('auth:changed')); + setMsg('Login correcto'); + navigate('/'); + } catch (err) { + setMsg(err.response?.data?.error || 'Error de login'); + } + }; + + return ( +
+ {!hasSession && ( + <> +

Acceso

+ + + {form.role !== 'superadmin' && ( + <> + + + + )} + + + + + + {msg &&

{msg}

} + + )} + {hasSession && ( +
+ Imagen de bienvenida +
+ )} +
+ ); +} diff --git a/frontend/src/pages/ParticipantSurvey.jsx b/frontend/src/pages/ParticipantSurvey.jsx new file mode 100644 index 0000000..e861270 --- /dev/null +++ b/frontend/src/pages/ParticipantSurvey.jsx @@ -0,0 +1,81 @@ +import { useEffect, useMemo, useState } from 'react'; +import api from '../utils/api'; + +export default function ParticipantSurvey() { + const [survey, setSurvey] = useState({ period: null, questions: [] }); + const [answers, setAnswers] = useState({}); + const [msg, setMsg] = useState(''); + const [step, setStep] = useState(0); + + useEffect(() => { + api.get('/surveys/active').then((res) => setSurvey(res.data)); + }, []); + + const grouped = useMemo(() => { + const map = new Map(); + survey.questions.forEach((q) => { + if (!map.has(q.dimension)) map.set(q.dimension, []); + map.get(q.dimension).push(q); + }); + return Array.from(map.entries()); + }, [survey.questions]); + + const totalSteps = grouped.length; + const current = grouped[step] || []; + const currentTitle = current[0] || ''; + const currentQuestions = current[1] || []; + + const submit = async () => { + const totalQuestions = survey.questions.length; + const answered = Object.keys(answers).length; + if (answered !== totalQuestions) { + window.dispatchEvent(new CustomEvent('app:notice', { detail: { type: 'error', message: 'Resultados incompletos. Responde todas las preguntas.' } })); + return; + } + const payload = { + survey_period_id: survey.period.id, + answers: Object.entries(answers).map(([question_id, value]) => ({ question_id, value: Number(value) })) + }; + await api.post('/surveys/responses', payload); + setMsg('Respuestas guardadas'); + }; + + if (!survey.period) return
No hay encuesta activa.
; + + return ( +
+

Encuesta {survey.period.codigo_medicion}

+

Periodo: {survey.period.fecha_inicio} a {survey.period.fecha_fin}

+ {totalSteps > 0 && ( +

{currentTitle}

+ )} + {currentQuestions.map((q) => ( +
+

{q.text}

+ +
+ ))} +
+ {' '} + + {step === totalSteps - 1 && ( + + )} +
+ {msg &&

{msg}

} +
+ ); +} diff --git a/frontend/src/pages/QuestionBank.jsx b/frontend/src/pages/QuestionBank.jsx new file mode 100644 index 0000000..a4ed97f --- /dev/null +++ b/frontend/src/pages/QuestionBank.jsx @@ -0,0 +1,350 @@ +import { useEffect, useState } from 'react'; +import api from '../utils/api'; + +export default function QuestionBank() { + const [questions, setQuestions] = useState([]); + const [sets, setSets] = useState([]); + const [file, setFile] = useState(null); + const [setName, setSetName] = useState(''); + const [msg, setMsg] = useState(''); + const [view, setView] = useState('sets'); + const [activeSet, setActiveSet] = useState(null); + const [setQuestionsList, setSetQuestionsList] = useState([]); + const [dimensions, setDimensions] = useState([]); + const [newDimName, setNewDimName] = useState(''); + const [selectedDimId, setSelectedDimId] = useState(null); + const [newQuestionText, setNewQuestionText] = useState(''); + const [newSetInfo, setNewSetInfo] = useState(null); + const companyCode = localStorage.getItem('company_code'); + + const load = async () => { + try { + const [qRes, sRes] = await Promise.all([ + api.get('/admin/questions'), + api.get('/admin/question-sets') + ]); + setQuestions(qRes.data); + setSets(sRes.data); + } catch (err) { + return; + } + try { + const dRes = await api.get('/admin/dimensions'); + setDimensions(dRes.data); + } catch (err) { + setDimensions([]); + } + }; + + useEffect(() => { + if (!companyCode) { + window.dispatchEvent(new CustomEvent('app:notice', { detail: { type: 'error', message: 'Selecciona una empresa antes de entrar a Preguntas.' } })); + return; + } + load(); + }, []); + + const exportExcel = async () => { + const res = await api.get('/admin/questions/export', { responseType: 'blob' }); + const url = window.URL.createObjectURL(new Blob([res.data])); + const a = document.createElement('a'); + a.href = url; + a.download = 'preguntas.xlsx'; + a.click(); + window.URL.revokeObjectURL(url); + }; + + const importExcel = async () => { + if (!file) return; + const form = new FormData(); + form.append('file', file); + if (newSetInfo?.id) form.append('set_id', String(newSetInfo.id)); + const res = await api.post('/admin/questions/import', form, { headers: { 'Content-Type': 'multipart/form-data' } }); + setMsg(`Procesadas: ${res.data.processed}. Errores: ${res.data.errors.length}`); + await load(); + }; + + const createSet = async () => { + const res = await api.post('/admin/question-sets', { name: setName, empty: true }); + setMsg(`Set creado: ${res.data.code}`); + setNewSetInfo(res.data); + setSetName(''); + await load(); + }; + + const openSet = async (setRow) => { + const res = await api.get(`/admin/question-sets/${setRow.id}/questions`); + setActiveSet(setRow); + setSetQuestionsList(res.data); + }; + + const editSet = async (setRow) => { + const res = await api.get(`/admin/question-sets/${setRow.id}/questions`); + setNewSetInfo(setRow); + setSetQuestionsList(res.data); + setView('editset'); + }; + + const downloadTemplate = async () => { + const res = await api.get('/admin/questions/template', { responseType: 'blob' }); + const url = window.URL.createObjectURL(new Blob([res.data])); + const a = document.createElement('a'); + a.href = url; + a.download = 'preguntas_template.xlsx'; + a.click(); + window.URL.revokeObjectURL(url); + }; + + const createDimension = async () => { + if (!newDimName) return; + const res = await api.post('/admin/dimensions', { name: newDimName }); + setNewDimName(''); + await load(); + setSelectedDimId(res.data.id); + }; + + const addQuestion = async () => { + if (!newSetInfo || !selectedDimId || !newQuestionText) return; + await api.post(`/admin/question-sets/${newSetInfo.id}/questions`, { dimension_id: selectedDimId, text: newQuestionText }); + setNewQuestionText(''); + const res = await api.get(`/admin/question-sets/${newSetInfo.id}/questions`); + setSetQuestionsList(res.data); + }; + + if (!companyCode) { + return ( +
+

Preguntas

+

Selecciona una empresa para ver las versiones de encuesta.

+
+ ); + } + + return ( +
+
+ + + +
+ + {view === 'sets' && !activeSet && ( +
+

Versiones de Encuesta

+ + + + {sets.map((s) => ( + + + + + + + + + + ))} + +
CódigoNombreVersiónActivoAbrirEditarEliminar
{s.code}{s.name}{s.version}{s.is_active ? 'Sí' : 'No'} + + + + + +
+
+ )} + + {view === 'newset' && ( +
+

Nuevo set de preguntas

+
+ setSetName(e.target.value)} placeholder="Nombre del set" /> +
+
+ setFile(e.target.files[0])} /> + +
+
+ + +
+ {newSetInfo && ( +
+

Ámbitos

+
+ setNewDimName(e.target.value)} placeholder="Nombre del ámbito" /> + +
+
+ {dimensions.map((d) => ( + + ))} +
+
+ setNewQuestionText(e.target.value)} placeholder="Texto de la pregunta" /> + +
+ + + + {setQuestionsList.map((q, idx) => ( + + + + + + ))} + +
CódigoDimensiónTexto
{q.question_code}{q.dimension}{q.text_snapshot}
+
+ )} + {msg &&

{msg}

} +
+ )} + + {view === 'editset' && newSetInfo && ( +
+

Editar set: {newSetInfo.code}

+
+ +
+
+ setFile(e.target.files[0])} /> + +
+
+ setNewDimName(e.target.value)} placeholder="Nombre del ámbito" /> + +
+
+ {dimensions.map((d) => ( + + ))} +
+
+ setNewQuestionText(e.target.value)} placeholder="Texto de la pregunta" /> + +
+ + + + {setQuestionsList.map((q, idx) => ( + + + + + + ))} + +
CódigoDimensiónTexto
{q.question_code}{q.dimension}{q.text_snapshot}
+ +
+ )} + + {view === 'sets' && activeSet && ( +
+

Preguntas del set: {activeSet.code}

+ + + + {setQuestionsList.map((q, idx) => ( + + + + + + ))} + +
CódigoDimensiónTexto
{q.question_code}{q.dimension}{q.text_snapshot}
+ +
+ )} + + {/* Preguntas se muestran al abrir un set */} +
+ ); +} diff --git a/frontend/src/pages/SuperadminCompanies.jsx b/frontend/src/pages/SuperadminCompanies.jsx new file mode 100644 index 0000000..0f0da16 --- /dev/null +++ b/frontend/src/pages/SuperadminCompanies.jsx @@ -0,0 +1,377 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import api from '../utils/api'; + +export default function SuperadminCompanies() { + const today = new Date().toISOString().slice(0, 10); + const nextYear = new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString().slice(0, 10); + const [companies, setCompanies] = useState([]); + const [editDates, setEditDates] = useState({}); + const [participationByCompany, setParticipationByCompany] = useState({}); + const [section, setSection] = useState('companies'); + const [view, setView] = useState('list'); + const [adminForm, setAdminForm] = useState({ name: '', email: '', password: '' }); + const [adminItems, setAdminItems] = useState([]); + const [adminCtx, setAdminCtx] = useState({ companyId: null, companyName: '', mode: 'list' }); + const [form, setForm] = useState({ + name: '', + code: '', + start_date: today, + end_date: nextYear, + db_name: '', + db_host: '192.168.222.20', + db_port: 3306, + db_user: 'user_ia', + db_pass: 'UserIA2026' + }); + const [msg, setMsg] = useState(''); + const navigate = useNavigate(); + + const load = async () => { + const res = await api.get('/superadmin/companies'); + setCompanies(res.data); + const nextDates = {}; + res.data.forEach((c) => { + nextDates[c.id] = { start_date: c.start_date || '', end_date: c.end_date || '' }; + }); + setEditDates(nextDates); + }; + + useEffect(() => { load(); }, []); + + const create = async () => { + try { + if (!form.start_date || !form.end_date) { + setMsg('Fecha inicio y fin son obligatorias'); + return; + } + await api.post('/superadmin/companies', form); + setMsg('Empresa creada'); + load(); + } catch (err) { + setMsg(err.response?.data?.error || 'Error'); + } + }; + + const openAdmins = async (company, mode) => { + const res = await api.get(`/superadmin/companies/${company.id}/admins`); + setAdminItems(res.data); + setAdminCtx({ companyId: company.id, companyName: company.name, mode }); + setAdminForm({ name: '', email: '', password: '' }); + setSection('admins'); + }; + + const createAdmin = async () => { + try { + if (!adminForm.name || !adminForm.email || !adminForm.password) { + setMsg('Nombre, email y password son obligatorios'); + return; + } + await api.post(`/superadmin/companies/${adminCtx.companyId}/admins`, adminForm); + const res = await api.get(`/superadmin/companies/${adminCtx.companyId}/admins`); + setAdminItems(res.data); + setAdminCtx({ ...adminCtx, mode: 'list' }); + setAdminForm({ name: '', email: '', password: '' }); + setMsg('Admin creado'); + } catch (err) { + setMsg(err.response?.data?.error || 'Error al crear admin'); + } + }; + + const selectCompany = (code) => { + localStorage.setItem('company_code', code); + setMsg(`Empresa seleccionada: ${code}`); + navigate('/admin'); + }; + + const formatDate = (value) => { + if (!value) return '-'; + return String(value).split('T')[0]; + }; + + const toggleAdminStatus = async (adminId, currentStatus) => { + const next = currentStatus === 'active' ? 'inactive' : 'active'; + await api.patch(`/superadmin/companies/${adminCtx.companyId}/admins/${adminId}/status`, { status: next }); + const res = await api.get(`/superadmin/companies/${adminCtx.companyId}/admins`); + setAdminItems(res.data); + }; + + return ( +
+
+ + + +
+ + {section === 'companies' && ( +
+
+ + + +
+ + {view === 'new' && ( +
+
+

Nueva Empresa

+ + setForm({...form, name:e.target.value})} /> + + setForm({...form, code:e.target.value})} /> + + setForm({...form, start_date:e.target.value})} /> + + setForm({...form, end_date:e.target.value})} /> + + setForm({...form, db_name:e.target.value})} /> + + {msg &&

{msg}

} +
+
+ )} + + {view === 'list' && ( +
+

Empresas

+ + + + {companies.map((c) => ( + + + + + + + + + + ))} + +
NombreCódigoEstadoInicioFinEditarAcceso
{c.name}{c.code}{c.status}{formatDate(c.start_date)}{formatDate(c.end_date)} + + + +
+
+ )} + + {view === 'manage' && ( +
+

Empresas

+ + + + {companies.map((c) => ( + + + + + + + + + ))} + +
NombreCódigoEstadoInicioFinAcciones
{c.name}{c.code}{c.status} + setEditDates({ + ...editDates, + [c.id]: { ...editDates[c.id], start_date: e.target.value } + })} + /> + + setEditDates({ + ...editDates, + [c.id]: { ...editDates[c.id], end_date: e.target.value } + })} + /> + + {c.status === 'active' ? ( + + ) : ( + + )} + {' '} + + {' '} + + {' '} + + {' '} + +
+
+ )} + + {view === 'print' && ( +
+

Imprimir

+

Imprime el listado actual de empresas y avance.

+ +
+ )} +
+ )} + + {section === 'admins' && ( +
+ {!adminCtx.companyId && ( + + + + {companies.map((c) => ( + + + + + + ))} + +
EmpresaCódigoAcciones
{c.name}{c.code} + + {' '} + +
+ )} + + {adminCtx.companyId && adminCtx.mode === 'list' && ( +
+

Administradores - {adminCtx.companyName}

+ + + + {adminItems.map((a) => ( + + + + + + + ))} + +
NombreEmailEstadoAcciones
{a.name}{a.email}{a.status} + {a.status === 'active' ? ( + + ) : ( + + )} + {' '} + +
+
+ )} + + {adminCtx.companyId && adminCtx.mode === 'new' && ( +
+

Crear administrador - {adminCtx.companyName}

+ + setAdminForm({...adminForm, name:e.target.value})} /> + + setAdminForm({...adminForm, email:e.target.value})} /> + + setAdminForm({...adminForm, password:e.target.value})} /> + + {' '} + +
+ )} +
+ )} + + {section === 'avance' && ( +
+

Avance por empresa

+ + + + {companies.map((c) => ( + + + + + ))} + +
EmpresaAcciones
{c.name} + +
+ {companies.map((c) => { + const p = participationByCompany[c.id]; + if (!p) return null; + return ( +
+

{c.name} - Avance

+

Registrados: {p.total_users}

+

Completaron 100%: {p.completed_users}

+

Menos del 100%: {p.partial_users}

+
+ ); + })} +
+ )} +
+ ); +} diff --git a/frontend/src/pages/SurveyPeriods.jsx b/frontend/src/pages/SurveyPeriods.jsx new file mode 100644 index 0000000..6ad2ddf --- /dev/null +++ b/frontend/src/pages/SurveyPeriods.jsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from 'react'; +import api from '../utils/api'; + +export default function SurveyPeriods() { + const [periods, setPeriods] = useState([]); + const [view, setView] = useState('list'); + const [form, setForm] = useState({ year: 2026, month: 3, fecha_inicio: '2026-03-01', fecha_fin: '2026-03-31' }); + + const load = async () => { + const res = await api.get('/admin/survey-periods'); + setPeriods(res.data); + }; + + useEffect(() => { load(); }, []); + + const create = async () => { + await api.post('/admin/survey-periods', form); + load(); + }; + + const activate = async (id) => { await api.patch(`/admin/survey-periods/${id}/activate`); load(); }; + const close = async (id) => { await api.patch(`/admin/survey-periods/${id}/close`); load(); }; + + return ( +
+
+ + + + +
+ + {view === 'new' && ( +
+
+

Nueva Medición

+ + setForm({...form, year:e.target.value})} /> + + setForm({...form, month:e.target.value})} /> + + setForm({...form, fecha_inicio:e.target.value})} /> + + setForm({...form, fecha_fin:e.target.value})} /> + +
+
+ )} + + {view === 'list' && ( +
+

Mediciones

+ + + + {periods.map((p) => ( + + + + + ))} + +
CódigoEstado
{p.codigo_medicion}{p.estado}
+
+ )} + + {view === 'manage' && ( +
+

Mediciones

+ + + + {periods.map((p) => ( + + + + + + ))} + +
CódigoEstadoAcciones
{p.codigo_medicion}{p.estado} + {' '} + +
+
+ )} + + {view === 'print' && ( +
+

Imprimir

+

Imprime el listado actual de mediciones.

+ +
+ )} +
+ ); +} diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css new file mode 100644 index 0000000..91b2052 --- /dev/null +++ b/frontend/src/styles/app.css @@ -0,0 +1,257 @@ +@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap'); + +:root { + --bg: #f7f4ef; + --ink: #1b1b1f; + --accent: #0c6d5c; + --accent-2: #e1b85b; + --card: #ffffff; + --muted: #6c6c74; + --border: #e5e0d8; +} + +* { box-sizing: border-box; } +body { + margin: 0; + font-family: 'Montserrat', sans-serif; + background: radial-gradient(circle at 20% 0%, #fff7e8 0%, #f7f4ef 55%, #f1ede7 100%); + color: var(--ink); +} + +.layout { + min-height: 100vh; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 28px; + background: var(--card); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; +} + +.topbar h1 { + margin: 0; + font-size: 20px; + letter-spacing: 0.5px; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; +} + +.brand img { + height: 36px; + width: auto; + border-radius: 6px; + border: 1px solid var(--border); + background: #fff; +} + +.topbar nav a { + margin-left: 14px; + text-decoration: none; + color: var(--ink); + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 999px; + background: #f0e9db; + border: 1px solid var(--border); +} + +.topbar nav a.active { + background: var(--accent); + color: #fff; + border: none; +} + +.topbar nav button { + margin-left: 14px; + text-decoration: none; + color: var(--ink); + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 999px; + background: #f0e9db; + border: 1px solid var(--border); + cursor: pointer; +} + +.content { + padding: 28px; +} + +.card { + background: var(--card); + border: 1px solid var(--border); + border-radius: 16px; + padding: 20px; + margin-bottom: 18px; + box-shadow: 0 10px 24px rgba(0,0,0,0.04); +} + +.login-image { + display: flex; + align-items: center; + justify-content: center; + min-height: 240px; +} + +.login-image img { + max-width: 320px; + width: 100%; + height: auto; + border-radius: 12px; + border: 1px solid var(--border); + background: #fff; +} + +.grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); +} + +button { + background: var(--accent); + color: #fff; + border: none; + padding: 10px 16px; + border-radius: 10px; + font-weight: 600; + cursor: pointer; +} + +button.secondary { + background: var(--accent-2); + color: #3b2c14; +} + +input, select { + width: 100%; + padding: 10px; + border-radius: 10px; + border: 1px solid var(--border); + margin-top: 6px; + margin-bottom: 12px; +} + +label { font-size: 12px; color: var(--muted); } + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, .table td { + text-align: left; + padding: 10px; + border-bottom: 1px solid var(--border); +} + +.subnav { + display: flex; + gap: 12px; + padding: 12px 18px; + background: var(--card); + border: 1px solid var(--border); + border-radius: 14px; + margin-bottom: 18px; +} + +.subnav button { + background: #f0e9db; + color: var(--ink); + border: 1px solid var(--border); + width: 40px; + height: 40px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.subnav button.active { + background: var(--accent); + color: #fff; + border: none; +} + +.danger { + background: #b23a3a; + color: #fff; +} + +.action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + width: 40px; + height: 40px; + font-weight: 700; + border: none; + box-shadow: 0 8px 16px rgba(0,0,0,0.08); + margin: 4px 4px 4px 0; +} + +.topbar nav a { font-size: 16px; } +.subnav button { font-size: 16px; } +.action-btn { font-size: 16px; } +button, a { font-size: 16px; } + +.action-suspend { background: #e4b85b; color: #2b1a00; } +.action-activate { background: #7ac7a8; color: #0c2b1f; } +.action-save { background: #0c6d5c; color: #fff; } +.action-report { background: #0a5f7a; color: #fff; } +.action-delete { background: #b23a3a; color: #fff; } + +.badge { + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + background: #e6f2ef; + color: var(--accent); + font-weight: 600; +} + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.modal { + background: #fff; + border-radius: 16px; + padding: 20px 24px; + width: min(420px, 90vw); + box-shadow: 0 20px 60px rgba(0,0,0,0.2); + border: 1px solid var(--border); +} + +.modal h3 { + margin: 0 0 8px 0; +} + +.modal p { + margin: 0 0 16px 0; + color: var(--muted); +} diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js new file mode 100644 index 0000000..e8581d4 --- /dev/null +++ b/frontend/src/utils/api.js @@ -0,0 +1,49 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: + import.meta.env.VITE_API_URL || + `http://${window.location.hostname}:4020` +}); + +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) config.headers.Authorization = `Bearer ${token}`; + const companyCode = localStorage.getItem('company_code'); + if (companyCode) config.headers['x-company-code'] = companyCode; + config.headers['x-menu'] = window.location.pathname; + return config; +}); + +api.interceptors.response.use( + (response) => response, + (error) => { + const status = error?.response?.status; + const url = error?.config?.url || ''; + const isSilent404 = status === 404 && url.includes('/admin/dimensions'); + const notice = { type: 'error', message: '' }; + if (isSilent404) { + return Promise.reject(error); + } + if (status === 401) { + notice.message = 'Sesión expirada o inválida. Inicia sesión nuevamente.'; + } else if (status === 403) { + notice.message = 'No tienes permisos para esta acción.'; + } else if (status === 404) { + notice.message = 'Recurso no encontrado o empresa inactiva.'; + } else if (status >= 500) { + notice.message = 'Error interno del servidor.'; + } + if (notice.message) { + const last = window.__lastNotice || {}; + const now = Date.now(); + if (last.message !== notice.message || now - (last.time || 0) > 1500) { + window.__lastNotice = { message: notice.message, time: now }; + window.dispatchEvent(new CustomEvent('app:notice', { detail: notice })); + } + } + return Promise.reject(error); + } +); + +export default api; diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..9a08dcb --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5373, + host: '0.0.0.0' + } +}); diff --git a/templates/plantilla_usuarios.csv b/templates/plantilla_usuarios.csv new file mode 100644 index 0000000..77eb584 --- /dev/null +++ b/templates/plantilla_usuarios.csv @@ -0,0 +1 @@ +Nombre,Email,Departamento,Cargo diff --git a/templates/plantilla_usuarios.xlsx b/templates/plantilla_usuarios.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b2d4b7418bb8c0558dd6f4aeb699e87c9dbfcba6 GIT binary patch literal 6437 zcmai32Q-{p*XHVd^xlFndh{Ay5QNct9i3scMDIi|L868rQA4z7K}dvAqeP7a5qGv zZe;FNazrU}E)ug33c@nEzU9XcNhvFXVB@8RHxdi}6LYxJWP%NBB2OVESIkV&J ztf-6#G-IFZxA$}|fME#-GJjDKb?=ytzk(uvrZ$mXXf!t3i%h1&r1bWpDye;rj1wlI zY{&5(Ir5Enj_M$G3T4oLbD<-CGJ}Gh+#*MSbh<982ZVOxOlZac11MSn` za+Xq&puzeh``cjW_HG31IOvf~7<{e;xS5MKXR1sp#oa9l2Bb-9-0O)2Zb19Hqg$r*&C&gj&Vwb1=hJPnszctnUM{cV+7@ z8fY@YLzuh`u&hq`KC%iA`XOC2@=w-K5RBY+`bpV84X~~Z{*Bo%<|^GwW*ZR9QvGh` z4S_mZLLiQpC*hiChYntVJp2s1>cG&;Pz9Le4h%LnIzS1lsZ(`Qwi9+fJJ6w4qQT6= z$n7i}f6}G2+IJulbx(B! zxxi+*seN>_P|?_>Bb6}boQPf?s%$Z9v^yr&^ogJ5_3plR|NPGVI9CF{)CiX#M)!+{ z>`6aRYP0#_(IHvyd@`>Y<9avaHsylAu|i*0G3A4;`4#&APBi`%L6_9H+kwFzf0k)i zGVl5rqN$`JGD7hW+aHiymwd*hXhNnK09~^OKmZ)iE2HMFb<>K2PG*hr@R@kTnlz7%2Y`rWaR0ky^azEEe%P8 z->R%123O+JTbR!}qH&efqNU>}A}QW{MSo{~KFMbEj;n6O_u5vFog|~~&M08t*0SB-S&Oj_IJ6$*2x+l{n(d=ioHA5i=&i#VbWI%yzEAFg z90zJwQ*M5s^B*|%5p5%&L|?zbKWOV%pA{|#*%r$i;8+>P){^y&Rm?yQ^Y`d8NO&9{ zEaDm{XXqOtqt$&RJ34KpB(owP3A`Xs<}Lpun5#x!{jP7_L&n%gQn}PSOi8&>94zx} z$~ zzpMk3_;!cCZUG|T{?WYNee9qD=B=meLt6ep%NriTl~2l^7!1`5gRtkeP6BQp&TM2T zy64@vRUJ9{3R!M?ksAvCx_eeT9zv+F;o7T8=fYfx15uY(Q!|jjEg6oi{fPfaBkjSB z3K)bNtJZS4Ts6EhC0J?L?vvV%3$Y^l@Zyuv4)osMEs5dt+|U$?tOzT0sqn1PFKZpA z69a|`FSn(M$==DA9eGY!E$0@L!#ilN|G@v+U_9)=&fuSY=!yrgOx{^WL|><*zZ$fN zMs;QO$lTEtS_`W<7Zr)CmGztca`qebVd-NC9BU(4boxY zQjFm>;f^~;S{MbBjTfE>PKY_t4z^b6jYR@89kOoDXfTM1+}M+Vlg}Qo*qgPw(eQaF zg%wDw=)t^bP%B z&{l-=UGk!;<{m4e_NH6pjJmueZ`QH=P%Y(bgdJ)H#XALFX@too`amR#xMe!z3u+%i zm;KS{lai|`(;xbK7ioPju!BFneZuqnRTrvQXPH$ncLF_LchxIE2dF(JUMKU+K_R?&J^lLlO$G;}Q0EUFQRw%ROg8eJ_<1;#5H z7!~1Gl)psS13AqF#Pn}6mr9z|h}!i|aPdFGi}gxkr0O37@um#{@DW%hK zP%#R|byGA2t$lT6X3Mc}r$%*r`ASredw>Ny$pbA;r|X#cy~b_#-pq8VmIp^Ph-YnK zD<-9@HAmtBbo-E;c!wH))>Oz`IcTP@caZ~ro)gL@3+@i zEoCO*ySGjrcy>4vHcNEBU_MfSnP`UPj$K0Jmt18u%7^@*2;?FbY09>HN<^oT2&=vf<`7Br5vA4B;*LuY*p4_)qX#LUqgqJObIL zL0<|(6b`WFU%&~0aRt7XhI~gt5klbpAX*_jm`2`GFrg2Idxc#6kql3pg;QOoVle)> zKArS?%Nkhf1iop-6KhubiGH4_~Mdu=&OJoLiS?g10GI?bi4ojQ}A-!`ltD|0^k zkfVZKO0a3wGyW!n4|=C4+cYHeGm_2lP^{iG)f^I?dm*@hXVWPNRWBDrx`rWwZnCEx z$K!tVM*dd1E4ZqJv_Q`a`+VSL_8cG1{W7oD>HWfY>!)QwV!L2v6Mn*1jXjICJEdjM z$5TDWvaz%K?ZuoW+cbt+4Nil|hQ5_ZM7tG5sOl2|w%*ehRt@Ysp5c=w zy8$gXmRAIyPaeI>(O0UC11A#M!lQY7ZuBKZ46B4dHFGGM221J$ruHso$b9YFyYH(S z>j)Oc?bQ3_M~xFSGYMD{S4Fmm>Sp-RrB*r%YzGJJLcj(F+&j%dWUcBw95~e?Z3Z$m zdykZ`mY-HK(XJ>?>fCOwzSoDd*WIuSoINrTAr@|#sK&uS3)W12OB;hj>S|}=COs1z zRIN}#xF)9|_Rv0x#jKQ7C`GP(rV#XMV+q3{;aEw-E{PowwpP+RKfEwXU1%aqo?2hj zFv{0Yr?`+D7+RGQKcV+!0vrb#h`Hz)Ls*7nyG*6q&7O=*t} zK)M~8nu=;PzXo(XfT`Ct$91{x$#i3^A!)x>6=x+lqO(S4k0-)}?x~1#j9Efa>*_tF zh%GMGt4d8$vB(2_M(1YBqA!Y728aW@00G}emZl;y>S|*^V#eog@dqkM-?pLExxTL%M$Fv-@FZsmV=t6IR(><`$>pr_pDlT>EIc5x?lc|Ryr z6f&zHZssbq6Jy_=J0lUt_qa$3Y)#1vsofc`TAJ20?oNqj%h>VTW2>kMfs-hB`ZVtO7!SI}6_a~;N$mtKj!s16Ia##d)AceTcjuVD zVYxt>r&qwZlmtWXwxTL&yK)eud2HSNKjom7o8i4G<)Y?nB2IKxu#Uo+1g{`I2c%p6}dXi;voD;)Az1^rD>X4K+gN0CoSU5~O)qClaJ=*wM|+GXPx#)sZq2MDFj7!;AoF|BTx1DV&c-R2 zcy>$Woyd~!rbJIG7ahjzU`i!|aw#S0s8On27-v8$;VCrDhmCAQpNSaM8BkaG*eOlAGUlu%*vcjr)Sdz%gO)0UGb`M04b2TO6SVfRAJ`RKX0(p6pGaj!%Abwrq#&cTEW_lc~J z-&D_D7wEs=jI1G68c>MqpKl8+)8%v_h_?k6#M=S@QO7^cEL>eLZ-?2Sc9$t$+!i!{eI-BnicsQEJS_5H)c&0TqrBn?%%Cq*~9 zS=13HIi(M=%BpwxxJ$tFbWALT9~Sq0&Q3yQi6tHfQn{EUE>Iec1ZlpF3ppscxC%L= zuyXL%x`++ycu!+ei#)3><9J8uuK zR5@o0wk@X|zssOt)6VjjqVP~r&t#`8A6k3%?OA_nR+@uz+gCoyRcxBPWO7+ard@uc z;J5;vt{8m0$j3=%N?J{%6V!lxH|!yqnlA=f*3T{5Dp=x=y`$jTOngAmWRxt2=i>px zJ2HuFwHomSFI%m=x{`xCUoxD&oPERwx0fYTaxfLt3>Nw`U#%^*P(!{Xg>L7 zWrwMFe(qT7aNSH8tGUF$VTSf9)r~ZJu6cY#S;^;>Lwm}Jc0)M`8U-g}89pI@MB46`}-b46%zN{cn@KwTK2q^;4yhEAkQJdp)GjEFQEF?SvC2w!RzmtjC&%I@d&pzC8WQC1R*g?Yp zsnslfU6d^5;VC^EcaK}nXj(Lgi0hLbZSxAqqdasZx$be(c=gj_E>7uxyvbt5OQwJl z(N(o$zE$AWz8U5+UcH3kN=Y4LEs$wFkwsa-*+LY(iPhOwu>50TB*;>}>a!8~p}N(l@23wK=J)ZPA7OGgip3Fn5J+6OMAQPl}*3x+s zfF%g=oXjr?j)#clf*+`3rKtjhWM~^+o4aPv{ z*)J~@z^irH5~C3bzE-XN)9Igz;57;fsp2Q*hivu#IlC0juGXcqHbf=;kkPI^{WH2t zA?<2i0=`}W{yo4yg|%xSLV!lNMfkab{a&;GGu})6>1tiN2O^|v#6JI_MqP(~bw>Ww zqps0NNIL(({!*uYF?O}fpGwBBT`UmULPYQnUE|jd|BFIdj2}B(3kUxh{-4spFUR)3 z!+r?~zx-ZB{^w2c*9J|9i{oYFKktpdp#LjmV+8jD%YB>#$B`@XAOY3d%Q