From 2a1a3d40b75224faa4b3716a4ea2e2cac10d4dba Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Jul 2026 14:50:48 -0600 Subject: [PATCH] Primer commit --- .env | 13 + .env.example | 3 + .gitignore | 24 + HANDOFF.md | 73 + README.md | 5 + index.html | 13 + package-lock.json | 1547 +++++++++++++++++ package.json | 22 + public/favicon.svg | 1 + public/icons.svg | 24 + src/App.vue | 7 + src/assets/hero.png | Bin 0 -> 13057 bytes src/assets/vite.svg | 1 + src/assets/vue.svg | 1 + src/components/AdminFooter.vue | 27 + src/components/AdminMainBody.vue | 23 + src/components/AdminNavbar.vue | 129 ++ src/components/AdminSidebar.vue | 98 ++ src/components/WsMonitorPanel.vue | 227 +++ src/components/report/ReportBuilderSheet.vue | 48 + src/components/results/ChildSearchPanel.vue | 63 + src/components/results/ResultCard.vue | 90 + src/components/results/SearchResultBoard.vue | 36 + src/components/results/SourceGroup.vue | 176 ++ src/composables/useConfigurationTree.js | 179 ++ src/composables/useSearchResultsStore.js | 422 +++++ src/composables/useWsMonitor.js | 169 ++ src/layouts/AdminLayout.vue | 57 + src/main.js | 15 + src/router/index.js | 98 ++ src/services/auth.js | 322 ++++ src/services/config.js | 93 + src/services/resultAdapters/adapterActas.js | 18 + src/services/resultAdapters/adapterGeneric.js | 70 + src/services/resultAdapters/adapterIne.js | 94 + .../resultAdapters/adapterOportunidades.js | 91 + src/services/resultAdapters/adapterPlacas.js | 40 + src/services/resultAdapters/adapterSaechf.js | 103 ++ src/services/resultAdapters/index.js | 57 + src/services/wsService.js | 269 +++ src/style.css | 10 + src/views/BillingView.vue | 34 + src/views/ChangePasswordView.vue | 159 ++ src/views/ConfigurationView.vue | 127 ++ src/views/DashboardView.vue | 262 +++ src/views/IntegrationsView.vue | 46 + src/views/LoginView.vue | 101 ++ src/views/Misbusquedas.vue | 344 ++++ src/views/UsersView.vue | 66 + test.db | 0 vite.config.js | 8 + 51 files changed, 5905 insertions(+) create mode 100644 .env create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 HANDOFF.md create mode 100644 README.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 public/icons.svg create mode 100644 src/App.vue create mode 100644 src/assets/hero.png create mode 100644 src/assets/vite.svg create mode 100644 src/assets/vue.svg create mode 100644 src/components/AdminFooter.vue create mode 100644 src/components/AdminMainBody.vue create mode 100644 src/components/AdminNavbar.vue create mode 100644 src/components/AdminSidebar.vue create mode 100644 src/components/WsMonitorPanel.vue create mode 100644 src/components/report/ReportBuilderSheet.vue create mode 100644 src/components/results/ChildSearchPanel.vue create mode 100644 src/components/results/ResultCard.vue create mode 100644 src/components/results/SearchResultBoard.vue create mode 100644 src/components/results/SourceGroup.vue create mode 100644 src/composables/useConfigurationTree.js create mode 100644 src/composables/useSearchResultsStore.js create mode 100644 src/composables/useWsMonitor.js create mode 100644 src/layouts/AdminLayout.vue create mode 100644 src/main.js create mode 100644 src/router/index.js create mode 100644 src/services/auth.js create mode 100644 src/services/config.js create mode 100644 src/services/resultAdapters/adapterActas.js create mode 100644 src/services/resultAdapters/adapterGeneric.js create mode 100644 src/services/resultAdapters/adapterIne.js create mode 100644 src/services/resultAdapters/adapterOportunidades.js create mode 100644 src/services/resultAdapters/adapterPlacas.js create mode 100644 src/services/resultAdapters/adapterSaechf.js create mode 100644 src/services/resultAdapters/index.js create mode 100644 src/services/wsService.js create mode 100644 src/style.css create mode 100644 src/views/BillingView.vue create mode 100644 src/views/ChangePasswordView.vue create mode 100644 src/views/ConfigurationView.vue create mode 100644 src/views/DashboardView.vue create mode 100644 src/views/IntegrationsView.vue create mode 100644 src/views/LoginView.vue create mode 100644 src/views/Misbusquedas.vue create mode 100644 src/views/UsersView.vue create mode 100644 test.db create mode 100644 vite.config.js diff --git a/.env b/.env new file mode 100644 index 0000000..2ef74cd --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +VITE_API_BASE_URL=https://api.leaks.mx +VITE_AUTH_LOGIN_URL=https://api.leaks.mx/usuario/login + +VITE_AUTH_CLIENT_ID=string +VITE_AUTH_CLIENT_SECRET=tu_secreto_real + + + +# Configuracion WebSocket (por defecto replica la del sistema actual) +VITE_WS_HOST=ws.leaks.mx +VITE_WS_PORT=2140 +VITE_WS_PATH=/websocket +VITE_WS_SECURE=true \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..04dbbb5 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_AUTH_LOGIN_URL=https://fast.roph.one/usuario/login +VITE_AUTH_CLIENT_ID=your-client-id +VITE_AUTH_CLIENT_SECRET=your-client-secret diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..3b450fd --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,73 @@ +================================ +HANDOFF PROYECTO PANEL VUE +Fecha: 2026-07-10 +Ruta local actual: C:/python/test/panel + +1. Resumen ejecutivo +Proyecto migrado a arquitectura de panel admin con Vue 3 + Vite + Tailwind. +Login obligatorio antes de entrar al panel, con guard de rutas. +Integración de WebSocket activa. +Búsqueda principal en Dashboard (3 pestañas: Teléfono, Placa, Nombre). +Monitor WS con logs. +Arquitectura nueva de resultados normalizados en árbol (sesión -> base -> card -> búsquedas hijas). +Exportación de reporte final en JSON y PDF desde selección de cards. +2. Estado funcional implementado +Autenticación: +Login y sesión en localStorage. +Ruta protegida para admin. +Layout: +Navbar, Sidebar, Footer. +Sidebar colapsable y persistente. +Dashboard: +Formulario de búsqueda principal. +Envío por WS con contrato alineado a HomeView. +Resultados: +Store central de resultados. +Adaptadores por fuente/base. +Tablero agrupado por base. +Cards con acciones para búsquedas derivadas. +Reportes: +Selección manual de cards. +Exportación JSON. +Exportación PDF (imprimible). +3.Archivos clave +src/views/DashboardView.vue +src/composables/useSearchResultsStore.js +src/composables/useWsMonitor.js +src/components/WsMonitorPanel.vue +src/components/results/SearchResultBoard.vue +src/components/results/SourceGroup.vue +src/components/results/ResultCard.vue +src/components/results/ChildSearchPanel.vue +src/components/report/ReportBuilderSheet.vue +src/services/wsService.js +src/services/auth.js +src/services/resultAdapters/index.js +src/services/resultAdapters/adapterGeneric.js +src/services/resultAdapters/adapterIne.js +src/services/resultAdapters/adapterActas.js +src/services/resultAdapters/adapterPlacas.js +.env.example + +4. Variables de entorno requeridas +VITE_AUTH_LOGIN_URL +VITE_AUTH_CLIENT_ID +VITE_AUTH_CLIENT_SECRET +VITE_WS_HOST +VITE_WS_PORT +VITE_WS_PATH +VITE_WS_SECURE +5. Punto crítico validado +Se eliminó la inyección de requestId dentro de busqueda.context para respetar contrato de backend WS. +El envío actual de búsqueda usa estructura: +cmd: busqueda +busqueda.query +busqueda.context +Pendientes recomendados +Pendientes recomendados +Cambiar searchLoading para que termine con respuesta real WS y no por timeout. +Agregar validaciones visuales por pestaña de búsqueda. +Paginación o virtualización por grupo cuando haya muchos resultados. +Persistencia opcional de sesiones de resultados. +================================ +FIN HANDOFF \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1511959 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ab2bd56 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1547 @@ +{ + "name": "panel", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "panel", + "version": "0.0.0", + "dependencies": { + "vue": "^3.5.39", + "vue-router": "^4.6.4", + "vue3-toastify": "^0.2.9" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.2", + "@vitejs/plugin-vue": "^6.0.7", + "tailwindcss": "^4.3.2", + "vite": "^8.1.1" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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==", + "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/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue3-toastify": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/vue3-toastify/-/vue3-toastify-0.2.9.tgz", + "integrity": "sha512-LN2GJGKgjt+C6IIBANp9hCM2A4yc5jC2Kj4YGl1J7ptj4rhThOJOGGlkCg+IxDt7hntNw9n3MG9ifv/AymyxKQ==", + "license": "MIT", + "engines": { + "node": ">=20", + "npm": ">=9.0.0" + }, + "peerDependencies": { + "vue": ">=3.2.0" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f3e74fc --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "panel", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.5.39", + "vue-router": "^4.6.4", + "vue3-toastify": "^0.2.9" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.2", + "@vitejs/plugin-vue": "^6.0.7", + "tailwindcss": "^4.3.2", + "vite": "^8.1.1" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..13ee9ea --- /dev/null +++ b/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/assets/vue.svg b/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/AdminFooter.vue b/src/components/AdminFooter.vue new file mode 100644 index 0000000..8cde14a --- /dev/null +++ b/src/components/AdminFooter.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/AdminMainBody.vue b/src/components/AdminMainBody.vue new file mode 100644 index 0000000..44f4157 --- /dev/null +++ b/src/components/AdminMainBody.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/AdminNavbar.vue b/src/components/AdminNavbar.vue new file mode 100644 index 0000000..df9dc3e --- /dev/null +++ b/src/components/AdminNavbar.vue @@ -0,0 +1,129 @@ + + + diff --git a/src/components/AdminSidebar.vue b/src/components/AdminSidebar.vue new file mode 100644 index 0000000..0e28d79 --- /dev/null +++ b/src/components/AdminSidebar.vue @@ -0,0 +1,98 @@ + + + diff --git a/src/components/WsMonitorPanel.vue b/src/components/WsMonitorPanel.vue new file mode 100644 index 0000000..f7f7c66 --- /dev/null +++ b/src/components/WsMonitorPanel.vue @@ -0,0 +1,227 @@ + + + diff --git a/src/components/report/ReportBuilderSheet.vue b/src/components/report/ReportBuilderSheet.vue new file mode 100644 index 0000000..7c5522c --- /dev/null +++ b/src/components/report/ReportBuilderSheet.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/components/results/ChildSearchPanel.vue b/src/components/results/ChildSearchPanel.vue new file mode 100644 index 0000000..6aa21e4 --- /dev/null +++ b/src/components/results/ChildSearchPanel.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/components/results/ResultCard.vue b/src/components/results/ResultCard.vue new file mode 100644 index 0000000..3f8b605 --- /dev/null +++ b/src/components/results/ResultCard.vue @@ -0,0 +1,90 @@ + + + diff --git a/src/components/results/SearchResultBoard.vue b/src/components/results/SearchResultBoard.vue new file mode 100644 index 0000000..5df61bc --- /dev/null +++ b/src/components/results/SearchResultBoard.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/results/SourceGroup.vue b/src/components/results/SourceGroup.vue new file mode 100644 index 0000000..813cc4a --- /dev/null +++ b/src/components/results/SourceGroup.vue @@ -0,0 +1,176 @@ + + + diff --git a/src/composables/useConfigurationTree.js b/src/composables/useConfigurationTree.js new file mode 100644 index 0000000..ada7afc --- /dev/null +++ b/src/composables/useConfigurationTree.js @@ -0,0 +1,179 @@ +import { computed, ref } from 'vue' // Importa funciones reactivas de Vue para manejar el estado y las propiedades computadas +import { toast } from 'vue3-toastify' +import { fetchSelectedBases, fetchTiposArbol, saveSelectedBases } from '../services/config' // Importa funciones para interactuar con la API de configuración +// Este composable maneja la lógica de carga, validación y guardado de la configuración de tablas y tipos de búsqueda en la aplicación. Proporciona variables reactivas y funciones para interactuar con la interfaz de usuario. + +// Define la función useConfigurationTree que encapsula toda la lógica relacionada con la configuración de tablas y tipos de búsqueda +export function useConfigurationTree() { + // Define variables reactivas para manejar el estado de carga, guardado, errores, éxito, datos del árbol y tablas seleccionadas + const loading = ref(false) + const saving = ref(false) + const error = ref('') + const success = ref('') + const treeData = ref([]) + const selectedTables = ref(new Set()) + // Define propiedades computadas para contar las tablas seleccionadas y determinar si se puede guardar la configuración + const selectedCount = computed(() => selectedTables.value.size) + // Define una propiedad computada para determinar si se puede guardar la configuración (si hay al menos una tabla seleccionada) + const canSave = computed(() => Array.isArray(treeData.value) && treeData.value.length > 0) + + // Define una propiedad computada para validar la configuración actual, asegurando que haya al menos una tabla seleccionada por cada tipo de búsqueda + const validationState = computed(() => { + if (!Array.isArray(treeData.value) || treeData.value.length === 0) { + return { + valid: false, + incompleteTypes: [], + message: 'No hay tipos de busqueda disponibles para guardar configuracion.', + } + } + // Filtra los tipos de búsqueda que no tienen al menos una tabla seleccionada y construye un mensaje de error si es necesario + const incompleteTypes = treeData.value + .filter((tipo) => { + const keys = tipoTableKeys(tipo) + if (keys.length === 0) { + return true + } + return !keys.some((key) => selectedTables.value.has(key)) + }) + .map((tipo) => String(tipo?.nombre || tipo?.tipo_busqueda || 'Tipo sin nombre')) + + // Determina si la configuración es válida (si no hay tipos incompletos) y construye un mensaje de error si es necesario + const valid = incompleteTypes.length === 0 + return { + valid, + incompleteTypes, + message: valid ? '' : `Debes seleccionar al menos una tabla de cada tipo. Faltan: ${incompleteTypes.join(', ')}.`, + } + }) + // Función auxiliar para obtener las claves de las tablas asociadas a un tipo de búsqueda + function tipoTableKeys(tipo) { + if (!Array.isArray(tipo?.tablas)) { + return [] + } + + return tipo.tablas + .map((tabla) => String(tabla?.bd_tbl_nombre || '').trim()) + .filter(Boolean) + } + // Función para verificar si una tabla está seleccionada + function isTableChecked(tableKey) { + return selectedTables.value.has(String(tableKey || '').trim()) + } + // Función para verificar si un tipo de búsqueda tiene todas sus tablas seleccionadas + function isTipoChecked(tipo) { + const keys = tipoTableKeys(tipo) + return keys.length > 0 && keys.every((key) => selectedTables.value.has(key)) + } + // Función para verificar si un tipo de búsqueda tiene algunas, pero no todas, sus tablas seleccionadas + function isTipoIndeterminate(tipo) { + const keys = tipoTableKeys(tipo) + if (keys.length === 0) { + return false + } + + const selected = keys.filter((key) => selectedTables.value.has(key)).length + return selected > 0 && selected < keys.length + } + // Función para alternar la selección de todas las tablas asociadas a un tipo de búsqueda + function toggleTipo(tipo, checked) { + const next = new Set(selectedTables.value) + const keys = tipoTableKeys(tipo) + + keys.forEach((key) => { + if (checked) { + next.add(key) + } else { + next.delete(key) + } + }) + + selectedTables.value = next + } + // Función para alternar la selección de una tabla individual + function toggleTabla(tableKey, checked) { + const normalizedKey = String(tableKey || '').trim() + if (!normalizedKey) { + return + } + + const next = new Set(selectedTables.value) + if (checked) { + next.add(normalizedKey) + } else { + next.delete(normalizedKey) + } + + selectedTables.value = next + } + // Función para cargar la configuración inicial desde la API, manejando el estado de carga y errores + async function loadConfigTree() { + loading.value = true + error.value = '' + success.value = '' + + try { + const [tree, selected] = await Promise.all([ + fetchTiposArbol(), + fetchSelectedBases(), + ]) + + treeData.value = tree + selectedTables.value = new Set(selected) + } catch (err) { + treeData.value = [] + selectedTables.value = new Set() + error.value = err instanceof Error ? err.message : 'Error desconocido al cargar configuracion.' + toast.error(error.value) + } finally { + loading.value = false + } + } + + async function saveConfig() { + if (saving.value) { + return false + } + + error.value = '' + success.value = '' + + // Regla de negocio: al menos una tabla seleccionada por cada tipo. + if (!validationState.value.valid) { + error.value = validationState.value.message + toast.warning(error.value) + return false + } + + saving.value = true + try { + const payload = await saveSelectedBases([...selectedTables.value]) + success.value = `Configuracion guardada con ${payload.bases_seleccionadas.length} tabla(s) seleccionada(s).` + toast.success('Configuracion guardada correctamente.') + return true + } catch (err) { + error.value = err instanceof Error ? err.message : 'Error desconocido al guardar configuracion.' + toast.error(error.value) + return false + } finally { + saving.value = false + } + } + + return { + loading, + saving, + error, + success, + treeData, + selectedCount, + canSave, + validationState, + isTableChecked, + isTipoChecked, + isTipoIndeterminate, + toggleTipo, + toggleTabla, + loadConfigTree, + saveConfig, + } +} diff --git a/src/composables/useSearchResultsStore.js b/src/composables/useSearchResultsStore.js new file mode 100644 index 0000000..d8523ef --- /dev/null +++ b/src/composables/useSearchResultsStore.js @@ -0,0 +1,422 @@ +// Composable para gestionar los resultados de búsqueda y las sesiones de búsqueda. + +import { computed, ref, watch } from 'vue' +import { useWsMonitor } from './useWsMonitor' +import { adaptSearchPayload } from '../services/resultAdapters' + +const sessions = ref([]) +const activeSessionId = ref('') +const pendingRequests = ref({}) +const selectedCardIds = ref([]) + +let bridgeInitialized = false +let sequence = 0 +const processedLogIds = new Set() + +const uid = (prefix) => { + sequence += 1 + return `${prefix}_${Date.now()}_${sequence}` +} + +const hashString = (value) => { + const raw = String(value || '') + let hash = 5381 + for (let i = 0; i < raw.length; i += 1) { + hash = ((hash << 5) + hash) + raw.charCodeAt(i) + hash &= 0xffffffff + } + return `h${Math.abs(hash)}` +} + +const getActiveSession = () => sessions.value.find((item) => item.id === activeSessionId.value) || null + +const activeSession = computed(() => getActiveSession()) + +const selectedSet = computed(() => new Set(selectedCardIds.value)) + +const findSessionById = (sessionId) => sessions.value.find((item) => item.id === sessionId) || null + +const findCardById = (cardId) => { + for (const session of sessions.value) { + const card = session.cardsById[cardId] + if (card) { + return { session, card } + } + } + return { session: null, card: null } +} + +const ensureSourceGroup = (session, sourceKey, sourceLabel) => { + if (session.groupMap[sourceKey]) { + return session.groupMap[sourceKey] + } + + const group = { + id: uid('group'), + sourceKey, + sourceLabel, + status: 'ok', + error: '', + cards: [], + } + + session.groupMap[sourceKey] = group + session.groups.push(group) + return group +} + +const createSession = (rootRequestId, rootQuery, rootContext) => { + const session = { + id: uid('session'), + createdAt: new Date().toISOString(), + rootRequestId, + rootQuery, + rootContext, + groups: [], + groupMap: {}, + cardsById: {}, + } + + sessions.value.unshift(session) + activeSessionId.value = session.id + return session +} + +const pushChildRequestPlaceholder = (parentCardId, requestId, query, context) => { + if (!parentCardId) { + return + } + + const { card } = findCardById(parentCardId) + if (!card) { + return + } + + card.childRequests.unshift({ + requestId, + query, + context, + status: 'loading', + error: '', + resultCardIds: [], + createdAt: new Date().toISOString(), + }) +} +// Actualiza el estado de una solicitud de búsqueda secundaria asociada a un card padre, incluyendo el estado, los IDs de las cards resultantes y cualquier error. +const setChildRequestResult = (parentCardId, requestId, status, resultCardIds = [], error = '') => { + if (!parentCardId) { + return + } + + const { card } = findCardById(parentCardId) + if (!card) { + return + } + + const target = card.childRequests.find((item) => item.requestId === requestId) + if (!target) { + return + } + + target.status = status + target.error = error + target.resultCardIds = resultCardIds +} + + +// Ejecuta una busqueda enviando la query y el context al backend a través del WebSocket +const dispatchSearch = ({ query, context, parentCardId = '', sessionId = '' }) => { + // Genera un ID único para la solicitud de búsqueda + const requestId = uid('req') + // Obtiene la función sendCommand del composable useWsMonitor para enviar comandos al backend + const { sendCommand } = useWsMonitor() + // Busca la sesión correspondiente al sessionId proporcionado, si no se encuentra, intenta buscarla a través del parentCardId. Si aún no se encuentra, crea una nueva sesión con la query y el context proporcionados. + let targetSession = sessionId ? findSessionById(sessionId) : null + if (!targetSession && parentCardId) { + const found = findCardById(parentCardId) + targetSession = found.session + } + // Si no se encuentra ninguna sesión existente, crea una nueva sesión con la query y el context proporcionados. + if (!targetSession) { + targetSession = createSession(requestId, query, context) + } + // Registra la solicitud de búsqueda pendiente en el objeto pendingRequests, asociando el requestId con la sesión, el parentCardId, la query y el context. + pendingRequests.value[requestId] = { + requestId, + sessionId: targetSession.id, + parentCardId, + query, + context, + status: 'pending', + error: '', + createdAt: new Date().toISOString(), + } + // Si se proporciona un parentCardId, agrega un marcador de posición para la solicitud de búsqueda secundaria en el card correspondiente. + pushChildRequestPlaceholder(parentCardId, requestId, query, context) + // Envía el comando 'busqueda' al backend a través del WebSocket, incluyendo la query y el context en el payload. + + sendCommand('busqueda', { + busqueda: { + query, + context, + }, + }) + + return { requestId, sessionId: targetSession.id } +} + +const normalizeMessagePayload = (payload) => { + if (!payload) { + return null + } + if (typeof payload === 'string') { + try { + return JSON.parse(payload) + } catch { + return null + } + } + if (typeof payload === 'object') { + return payload + } + return null +} + +const extractRequestId = (payload) => { + return ( + payload?.requestId + || payload?.context?.requestId + || payload?.meta?.requestId + || payload?.source?.requestId + || '' + ) +} + +const pickLatestRequest = () => { + const entries = Object.values(pendingRequests.value) + .filter((item) => item.status !== 'error') + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + + return entries[0] || null +} + +// Agrega nuevas cards a un grupo de resultados de búsqueda dentro de una sesión, asegurando que no se dupliquen y devolviendo los IDs de las cards agregadas. +const addCardsToGroup = ({ session, group, cards, parentCardId, requestId }) => { + const appendedIds = [] + + for (const card of cards) { + const stableSeed = `${group.sourceKey}:${card.stableKey}` + const stableId = `card_${hashString(stableSeed)}` + + if (session.cardsById[stableId]) { + appendedIds.push(stableId) + continue + } + + const normalized = { + id: stableId, + stableKey: card.stableKey, + sourceKey: group.sourceKey, + sourceLabel: group.sourceLabel, + title: card.title, + fields: Array.isArray(card.fields) ? card.fields : [], + raw: card.raw, + actions: Array.isArray(card.actions) ? card.actions : [], + parentCardId: parentCardId || '', + createdByRequestId: requestId, + childRequests: [], + } + + session.cardsById[stableId] = normalized + group.cards.push(stableId) + appendedIds.push(stableId) + } + + return appendedIds +} + +const handleIncomingSearchPayload = (payload) => { + const requestId = extractRequestId(payload) + const pending = requestId ? pendingRequests.value[requestId] : pickLatestRequest() + + if (!pending) { + return + } + + const session = findSessionById(pending.sessionId) + if (!session) { + return + } + + if (payload?.ok === false) { + const normalized = adaptSearchPayload(payload) + const group = ensureSourceGroup(session, normalized.sourceKey, normalized.sourceLabel) + group.status = 'error' + group.error = String( + payload?.error?.message + || payload?.error?.detail + || payload?.message + || 'Error en respuesta' + ) + + // NOT_FOUND por fuente es esperado en búsquedas multi-base: no cancelar toda la solicitud. + const errorCode = String(payload?.error?.code || '').toUpperCase() + if (errorCode !== 'NOT_FOUND') { + pending.error = group.error + setChildRequestResult(pending.parentCardId, pending.requestId, 'error', [], pending.error) + } + + return + } + + const normalized = adaptSearchPayload(payload) + const group = ensureSourceGroup(session, normalized.sourceKey, normalized.sourceLabel) + + const cardIds = addCardsToGroup({ + session, + group, + cards: normalized.cards, + parentCardId: pending.parentCardId, + requestId: pending.requestId, + }) + + pending.status = 'done' + setChildRequestResult(pending.parentCardId, pending.requestId, 'done', cardIds) +} + +// Inicializa el puente WebSocket para recibir los resultados de búsqueda en tiempo real +const initWsBridge = () => { + if (bridgeInitialized) { + return + } + + bridgeInitialized = true + const { logs } = useWsMonitor() + // Observa los cambios en los logs del WebSocket para procesar los mensajes entrantes + watch( + logs, + (items) => { + for (const item of items) { + if (item.type !== 'incoming') { + continue + } + if (processedLogIds.has(item.id)) { + continue + } + + processedLogIds.add(item.id) + const payload = normalizeMessagePayload(item.payload) + if (!payload) { + continue + } + + const isSearchPayload = Object.prototype.hasOwnProperty.call(payload, 'ok') + && Object.prototype.hasOwnProperty.call(payload, 'source') + && Object.prototype.hasOwnProperty.call(payload, 'data') + + if (isSearchPayload) { + handleIncomingSearchPayload(payload) + } + } + }, + { deep: true } + ) +} + +const toggleCardSelection = (cardId) => { + if (selectedSet.value.has(cardId)) { + selectedCardIds.value = selectedCardIds.value.filter((id) => id !== cardId) + } else { + selectedCardIds.value = [...selectedCardIds.value, cardId] + } +} + +const isCardSelected = (cardId) => selectedSet.value.has(cardId) + +const runCardAction = ({ sessionId, parentCardId, action }) => { + if (!action?.query || !action?.context) { + return + } + dispatchSearch({ + query: action.query, + context: action.context, + parentCardId, + sessionId, + }) +} + +const selectedCards = computed(() => { + const session = activeSession.value + if (!session) { + return [] + } + return selectedCardIds.value.map((id) => session.cardsById[id]).filter(Boolean) +}) + +const exportSelectedAsJson = () => { + const data = { + generatedAt: new Date().toISOString(), + sessionId: activeSessionId.value, + cards: selectedCards.value, + } + + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `reporte_resultados_${Date.now()}.json` + link.click() + URL.revokeObjectURL(url) +} + +const escapeHtml = (value) => String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + +const exportSelectedAsPdf = () => { + if (!selectedCards.value.length) { + return + } + + const rows = selectedCards.value + .map((card, index) => { + const fields = card.fields + .map((field) => `
  • ${escapeHtml(field.label)}: ${escapeHtml(field.value)}
  • `) + .join('') + + return `

    ${index + 1}. ${escapeHtml(card.title)}

    Base: ${escapeHtml(card.sourceLabel || card.sourceKey)}

      ${fields}
    ` + }) + .join('') + + const html = `Reporte

    Reporte de resultados

    Generado: ${escapeHtml(new Date().toLocaleString())}

    Total de cards: ${selectedCards.value.length}

    ${rows}` + + const win = window.open('', '_blank', 'noopener,noreferrer,width=1100,height=800') + if (!win) { + return + } + + win.document.open() + win.document.write(html) + win.document.close() + win.focus() + win.print() +} + +export const useSearchResultsStore = () => ({ + sessions, + activeSession, + activeSessionId, + pendingRequests, + selectedCardIds, + selectedCards, + initWsBridge, + dispatchSearch, + toggleCardSelection, + isCardSelected, + runCardAction, + exportSelectedAsJson, + exportSelectedAsPdf, +}) diff --git a/src/composables/useWsMonitor.js b/src/composables/useWsMonitor.js new file mode 100644 index 0000000..00a6458 --- /dev/null +++ b/src/composables/useWsMonitor.js @@ -0,0 +1,169 @@ +import { computed, ref } from 'vue' +import { createWsClient } from '../services/wsService' +import { getAuthSession } from '../services/auth' + +const DEFAULT_MAX_HISTORY = 200 +const logs = ref([]) +const connectionStatus = ref('offline') +const connectionDetail = ref('') +const maxHistory = ref(DEFAULT_MAX_HISTORY) +const username = ref('') +const isConnected = computed(() => connectionStatus.value === 'open') + +let client = null + +function stringifyPayload(payload) { + if (typeof payload === 'string') { + return payload + } + try { + return JSON.stringify(payload) + } catch { + return String(payload) + } +} + +const appendLog = (entry) => { + const normalized = { + id: `${Date.now()}_${Math.random().toString(16).slice(2)}`, + timestamp: new Date().toISOString(), + type: entry.type, + label: entry.label, + payload: stringifyPayload(entry.payload), + } + + logs.value.unshift(normalized) + + if (logs.value.length > maxHistory.value) { + logs.value.length = maxHistory.value + } +} + +const ensureClient = () => { + if (client) { + return true + } + + const session = getAuthSession() + const token = session?.access_token + + if (!token) { + appendLog({ + type: 'error', + label: 'No hay token de sesion para conectar WS.', + payload: 'Inicia sesion nuevamente.', + }) + return false + } + + client = createWsClient({ + token, + username: session?.usuario, + onStatus: ({ status, detail }) => { + connectionStatus.value = status + connectionDetail.value = detail + + const isErrorLike = ['error', 'failed'].includes(status) + appendLog({ + type: isErrorLike ? 'error' : 'status', + label: `Estado: ${status}`, + payload: detail || '-', + }) + }, + onSend: ({ message, sent }) => { + appendLog({ + type: 'outgoing', + label: sent ? 'Solicitud enviada' : 'Solicitud en cola', + payload: message, + }) + }, + onMessage: (message) => { + appendLog({ + type: 'incoming', + label: 'Respuesta recibida', + payload: message, + }) + }, + }) + + username.value = client.getUsername() + return true +} + +export function useWsMonitor(initialMaxHistory = DEFAULT_MAX_HISTORY) { + if (initialMaxHistory > maxHistory.value) { + maxHistory.value = initialMaxHistory + } + + const connect = () => { + const ready = ensureClient() + if (!ready) { + return + } + client.connect() + } + + const disconnect = () => { + if (!client) { + connectionStatus.value = 'offline' + connectionDetail.value = '' + return + } + + client.disconnect() + client = null + connectionStatus.value = 'offline' + connectionDetail.value = '' + username.value = '' + } + // Envía un mensaje de texto sin envolver al servidor WebSocket. Si el socket no está abierto, el mensaje se encola para enviarse cuando se abra la conexión. + const sendRaw = (message) => { + if (!ensureClient()) { + return + } + client.sendRaw(message) + } + // Envía un comando al servidor WebSocket. El comando se envía como un objeto JSON con la propiedad "command" y cualquier otra propiedad adicional que se pase en "extra". + const sendCommand = (cmd, extra = {}) => { + // Verifica si el cliente WS está inicializado y listo para enviar comandos. Si no lo está, intenta inicializarlo. + if (!ensureClient()) { + return + } + // en este caso, el payload se envía como un objeto con la propiedad "command" y cualquier otra propiedad adicional que se pase en "extra" + client.sendCommand(cmd, extra) + } + + const clearLogs = () => { + logs.value = [] + } + + const setMaxHistory = (nextValue) => { + const normalized = Number(nextValue) + if (Number.isNaN(normalized) || normalized < 20) { + maxHistory.value = 20 + } else if (normalized > 1000) { + maxHistory.value = 1000 + } else { + maxHistory.value = normalized + } + + if (logs.value.length > maxHistory.value) { + logs.value.length = maxHistory.value + } + } + + return { + logs, + connectionStatus, + connectionDetail, + isConnected, + maxHistory, + username, + connect, + disconnect, + sendRaw, + sendCommand, + clearLogs, + setMaxHistory, + } +} diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..347c149 --- /dev/null +++ b/src/layouts/AdminLayout.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..85aa48e --- /dev/null +++ b/src/main.js @@ -0,0 +1,15 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' +import router from './router' +import Vue3Toastify from 'vue3-toastify' +import 'vue3-toastify/dist/index.css' + +createApp(App) + .use(router) + .use(Vue3Toastify, { + autoClose: 3500, + position: 'top-right', + theme: 'colored', + }) + .mount('#app') diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..58379ae --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,98 @@ +import { createRouter, createWebHistory } from 'vue-router' +import AdminLayout from '../layouts/AdminLayout.vue' +import ConfigurationView from '../views/ConfigurationView.vue' +import DashboardView from '../views/DashboardView.vue' +import Misbusquedas from '../views/Misbusquedas.vue' +import BillingView from '../views/BillingView.vue' +import IntegrationsView from '../views/IntegrationsView.vue' +import LoginView from '../views/LoginView.vue' +import ChangePasswordView from '../views/ChangePasswordView.vue' +import { isAuthenticated } from '../services/auth' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + redirect: '/login', + }, + { + path: '/login', + name: 'login', + component: LoginView, + meta: { + guestOnly: true, + }, + }, + { + path: '/admin', + component: AdminLayout, + meta: { + requiresAuth: true, + }, + children: [ + { + path: '', + redirect: '/admin/dashboard', + }, + { + path: 'dashboard', + name: 'dashboard', + component: DashboardView, + }, + { + path: 'configuracion', + name: 'configuracion', + component: ConfigurationView, + }, + { + path: 'misbusquedas', + name: 'misbusquedas', + component: Misbusquedas, + }, + { + path: 'facturacion', + name: 'facturacion', + component: BillingView, + }, + { + path: 'integraciones', + name: 'integraciones', + component: IntegrationsView, + }, + { + path: 'cambiar-contrasena', + name: 'cambiar-contrasena', + component: ChangePasswordView, + }, + ], + }, + { + path: '/:pathMatch(.*)*', + redirect: '/login', + }, + ], +}) + +router.beforeEach((to) => { + const loggedIn = isAuthenticated() + + if (to.meta.requiresAuth && !loggedIn) { + return { + path: '/login', + query: { + redirect: to.fullPath, + }, + } + } + + if (to.meta.guestOnly && loggedIn) { + return { + path: '/admin/dashboard', + } + } + + return true +}) + +export default router diff --git a/src/services/auth.js b/src/services/auth.js new file mode 100644 index 0000000..c23eed5 --- /dev/null +++ b/src/services/auth.js @@ -0,0 +1,322 @@ +const SESSION_STORAGE_KEY = 'auth.session' +const LOGIN_ENDPOINT = import.meta.env.VITE_AUTH_LOGIN_URL ?? 'https://api.leaks.mx/usuario/login' +const REFRESH_ENDPOINT = import.meta.env.VITE_AUTH_REFRESH_URL ?? 'https://api.leaks.mx/usuario/refresh' +const LOGOUT_ENDPOINT = import.meta.env.VITE_AUTH_LOGOUT_URL ?? 'https://api.leaks.mx/usuario/logout' +const LOGOUT_ALL_ENDPOINT = import.meta.env.VITE_AUTH_LOGOUT_ALL_URL ?? 'https://api.leaks.mx/usuario/logout-all' +const PROFILE_ENDPOINT = import.meta.env.VITE_AUTH_PROFILE_URL ?? 'https://api.leaks.mx/usuario/me' +const CHANGE_PASSWORD_ENDPOINT = import.meta.env.VITE_AUTH_CHANGE_PASSWORD_URL ?? 'https://api.leaks.mx/perfil/cambiarPassword' +const EXPIRY_SKEW_SECONDS = 45 + +const AUTH_CLIENT_ID = import.meta.env.VITE_AUTH_CLIENT_ID ?? '' +const AUTH_CLIENT_SECRET = import.meta.env.VITE_AUTH_CLIENT_SECRET ?? '' + +let refreshPromise = null + +const decodeJwtPayload = (token) => { + if (!token || typeof token !== 'string') { + return {} + } + + try { + const parts = token.split('.') + if (parts.length < 2) { + return {} + } + + const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/') + const normalized = payload.padEnd(Math.ceil(payload.length / 4) * 4, '=') + return JSON.parse(atob(normalized)) + } catch { + return {} + } +} + +const normalizeSession = (session) => { + if (!session || typeof session !== 'object') { + return null + } + + const accessToken = String(session.access_token || '').trim() + if (!accessToken) { + return null + } + + const payload = decodeJwtPayload(accessToken) + const expFromToken = Number(payload?.exp || 0) + const expiresIn = Number(session.expires_in || 0) + + let expiresAt = Number(session.expires_at || 0) + if (!Number.isFinite(expiresAt) || expiresAt <= 0) { + if (Number.isFinite(expFromToken) && expFromToken > 0) { + expiresAt = expFromToken * 1000 + } else if (Number.isFinite(expiresIn) && expiresIn > 0) { + expiresAt = Date.now() + (expiresIn * 1000) + } else { + expiresAt = Date.now() + (10 * 60 * 1000) + } + } + + return { + ...session, + access_token: accessToken, + idperfil: String(session.idperfil || payload?.idperfil || payload?.sub || ''), + usuario: String(session.usuario || payload?.usuario || ''), + nivel: String(session.nivel || payload?.nivel || ''), + expires_in: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : Math.max(1, Math.floor((expiresAt - Date.now()) / 1000)), + expires_at: expiresAt, + } +} + +const isNearExpiration = (session, skewSeconds = EXPIRY_SKEW_SECONDS) => { + const normalized = normalizeSession(session) + if (!normalized) { + return true + } + return (Date.now() + (skewSeconds * 1000)) >= Number(normalized.expires_at || 0) +} + +export const saveAuthSession = (session) => { + const normalized = normalizeSession(session) + if (!normalized) { + window.localStorage.removeItem(SESSION_STORAGE_KEY) + return + } + window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(normalized)) +} + +export const getAuthSession = () => { + const session = window.localStorage.getItem(SESSION_STORAGE_KEY) + if (!session) { + return null + } + + try { + const parsed = JSON.parse(session) + const normalized = normalizeSession(parsed) + if (!normalized) { + window.localStorage.removeItem(SESSION_STORAGE_KEY) + return null + } + return normalized + } catch { + window.localStorage.removeItem(SESSION_STORAGE_KEY) + return null + } +} + +export const clearAuthSession = () => { + window.localStorage.removeItem(SESSION_STORAGE_KEY) +} + +export const isAuthenticated = () => { + const session = getAuthSession() + if (!session?.access_token) { + return false + } + + if (isNearExpiration(session, 0)) { + clearAuthSession() + return false + } + + return true +} + +export const isTokenExpired = () => { + const session = getAuthSession() + return isNearExpiration(session, 0) +} + +export const loginUser = async ({ username, password }) => { + if (!AUTH_CLIENT_ID || !AUTH_CLIENT_SECRET) { + throw new Error('Faltan variables de entorno VITE_AUTH_CLIENT_ID o VITE_AUTH_CLIENT_SECRET.') + } + + const body = new URLSearchParams({ + grant_type: 'password', + username, + password, + scope: '', + client_id: AUTH_CLIENT_ID, + client_secret: AUTH_CLIENT_SECRET, + }) + + const response = await fetch(LOGIN_ENDPOINT, { + method: 'POST', + credentials: 'include', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: body.toString(), + }) + + const responseData = await response.json().catch(() => null) + + if (!response.ok) { + const message = responseData?.detail || responseData?.message || 'No se pudo iniciar sesion. Verifica tus credenciales.' + throw new Error(message) + } + + if (!responseData?.access_token) { + throw new Error('La API respondio sin access_token.') + } + + const normalized = normalizeSession(responseData) + saveAuthSession(normalized) + return normalized +} + +export const refreshAccessToken = async () => { + if (refreshPromise) { + return refreshPromise + } + + refreshPromise = (async () => { + const response = await fetch(REFRESH_ENDPOINT, { + method: 'POST', + credentials: 'include', + headers: { + Accept: 'application/json', + }, + }) + + const data = await response.json().catch(() => null) + if (!response.ok || !data?.access_token) { + clearAuthSession() + throw new Error(data?.detail || data?.message || 'No se pudo refrescar la sesion.') + } + + const normalized = normalizeSession(data) + saveAuthSession(normalized) + return normalized + })() + + try { + return await refreshPromise + } finally { + refreshPromise = null + } +} + +export const ensureValidAccessToken = async () => { + const session = getAuthSession() + if (!session?.access_token) { + throw new Error('No hay sesion activa.') + } + + if (!isNearExpiration(session)) { + return session.access_token + } + + const refreshed = await refreshAccessToken() + return refreshed.access_token +} + +export const authenticatedFetch = async (url, options = {}, retry = true) => { + const token = await ensureValidAccessToken() + const headers = new Headers(options?.headers || {}) + headers.set('Authorization', `Bearer ${token}`) + if (!headers.has('Accept')) { + headers.set('Accept', 'application/json') + } + + const response = await fetch(url, { + ...options, + headers, + credentials: options?.credentials ?? 'include', + }) + + if (response.status === 401 && retry) { + try { + await refreshAccessToken() + } catch { + clearAuthSession() + throw new Error('Sesion expirada. Inicia sesion nuevamente.') + } + + return authenticatedFetch(url, options, false) + } + + return response +} + +export const fetchProfile = async () => { + const response = await authenticatedFetch(PROFILE_ENDPOINT, { method: 'GET' }) + if (!response.ok) { + throw new Error(`No se pudo obtener perfil (HTTP ${response.status}).`) + } + return response.json() +} + +export const changePassword = async ({ currentPassword, newPassword, confirmPassword }) => { + const response = await authenticatedFetch(CHANGE_PASSWORD_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + password_o: currentPassword, + password_n: newPassword, + password_confirm: confirmPassword, + }), + }) + + const payload = await response.json().catch(() => null) + if (!response.ok) { + const detail = payload?.detail || payload?.msj || `HTTP ${response.status}` + throw new Error(String(detail)) + } + + const errorCode = Number(payload?.error ?? payload?.eror ?? 0) + if (errorCode !== 0) { + throw new Error(String(payload?.msj || 'No se pudo cambiar la contraseña.')) + } + + return payload +} + +export const logoutUser = async () => { + try { + const session = getAuthSession() + const headers = { + Accept: 'application/json', + } + if (session?.access_token) { + headers.Authorization = `Bearer ${session.access_token}` + } + + await fetch(LOGOUT_ENDPOINT, { + method: 'POST', + credentials: 'include', + headers, + }) + } catch { + // No bloquear logout local si falla logout remoto. + } finally { + clearAuthSession() + } +} + +export const logoutAllSessions = async () => { + const session = getAuthSession() + if (!session?.access_token) { + clearAuthSession() + return + } + + const response = await fetch(LOGOUT_ALL_ENDPOINT, { + method: 'POST', + credentials: 'include', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${session.access_token}`, + }, + }) + + if (!response.ok) { + throw new Error(`No se pudo cerrar todas las sesiones (HTTP ${response.status}).`) + } + + clearAuthSession() +} diff --git a/src/services/config.js b/src/services/config.js new file mode 100644 index 0000000..c75d532 --- /dev/null +++ b/src/services/config.js @@ -0,0 +1,93 @@ +import { authenticatedFetch } from './auth' + +const apiBase = (import.meta.env.VITE_API_BASE_URL || 'https://api.leaks.mx').replace(/\/$/, '') + +export const tiposArbolEndpoint = import.meta.env.VITE_TIPOS_ARBOL_ENDPOINT || `${apiBase}/busqueda/tiposArbol` +export const basesEndpoint = import.meta.env.VITE_BASES_ENDPOINT || `${apiBase}/busqueda/bases-seleccionadas` + +function normalizeSelected(payload) { + const sourceCandidates = [ + payload, + payload?.bases_seleccionadas, + payload?.basesSeleccionadas, + payload?.data, + payload?.data?.bases_seleccionadas, + payload?.result?.bases_seleccionadas, + ] + + const source = sourceCandidates.find((item) => Array.isArray(item)) + if (!Array.isArray(source)) { + return [] + } + + return source + .map((entry) => { + if (typeof entry === 'string') { + return entry.trim() + } + if (entry && typeof entry === 'object') { + return String(entry.bd_tbl_nombre || '').trim() + } + return '' + }) + .filter(Boolean) +} + +async function parseErrorDetail(response) { + let detail = `HTTP ${response.status}` + try { + const errorJson = await response.json() + if (errorJson?.detail) { + detail = String(errorJson.detail) + } + } catch { + // Mantener detalle por codigo HTTP. + } + return detail +} + +export async function fetchTiposArbol() { + const response = await authenticatedFetch(tiposArbolEndpoint, { + method: 'GET', + }) + + if (!response.ok) { + throw new Error(`No se pudo cargar tiposArbol (${await parseErrorDetail(response)}).`) + } + + const payload = await response.json() + return Array.isArray(payload) ? payload : [] +} + +export async function fetchSelectedBases() { + const response = await authenticatedFetch(basesEndpoint, { + method: 'GET', + }) + + if (!response.ok) { + throw new Error(`No se pudo cargar la configuracion actual (${await parseErrorDetail(response)}).`) + } + + const payload = await response.json() + return normalizeSelected(payload) +} + +export async function saveSelectedBases(baseKeys) { + const payload = { + bases_seleccionadas: Array.isArray(baseKeys) ? baseKeys : [], + } + + const response = await authenticatedFetch(basesEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + + if (!response.ok) { + throw new Error(`No se pudo guardar la configuracion (${await parseErrorDetail(response)}).`) + } + + return payload +} diff --git a/src/services/resultAdapters/adapterActas.js b/src/services/resultAdapters/adapterActas.js new file mode 100644 index 0000000..6e2cf01 --- /dev/null +++ b/src/services/resultAdapters/adapterActas.js @@ -0,0 +1,18 @@ +const clean = (value) => String(value || '').trim() + +export const adaptActas = (payload) => { + const rows = Array.isArray(payload?.data) ? payload.data : [] + + return rows.map((row, index) => ({ + stableKey: `ACTAS:${clean(row?.curp) || index}`, + title: `Actas #${index + 1}`, + fields: [ + { label: 'CURP', value: clean(row?.curp) || '—' }, + { label: 'Nombre', value: [row?.nombre, row?.pa_madre, row?.sa_madre].map(clean).filter(Boolean).join(' ') || '—' }, + { label: 'Padre', value: [row?.nombre_padre, row?.pa_padre, row?.sa_padre].map(clean).filter(Boolean).join(' ') || '—' }, + { label: 'Madre', value: [row?.nombre_madre, row?.pa_madre, row?.sa_madre].map(clean).filter(Boolean).join(' ') || '—' }, + ], + raw: row, + actions: [], + })) +} diff --git a/src/services/resultAdapters/adapterGeneric.js b/src/services/resultAdapters/adapterGeneric.js new file mode 100644 index 0000000..2f82ffd --- /dev/null +++ b/src/services/resultAdapters/adapterGeneric.js @@ -0,0 +1,70 @@ +const toDisplay = (value) => { + if (value === null || value === undefined || value === '') return '—' + if (typeof value === 'object') { + try { + return JSON.stringify(value) + } catch { + return String(value) + } + } + return String(value) +} + +const toRows = (payload) => { + if (Array.isArray(payload?.data)) return payload.data + if (payload?.data && typeof payload.data === 'object') return [payload.data] + return [] +} + +const toActions = (row) => { + const curp = String(row?.CURP || row?.curp || '').trim() + const rfc = String(row?.RFC || row?.rfc || '').trim() || (curp ? curp.substring(0, 10) : '') + const actions = [] + + if (rfc) { + actions.push({ + type: 'vehiculos', + label: 'Buscar vehiculos', + query: { rfc }, + context: { + searchType: 'datos_chiapas', + domain: 'chiapas', + resource: 'vehiculos', + operation: 'buscar', + variant: 'mis_vehiculos', + match: 'exacta', + }, + }) + } + + if (curp) { + actions.push({ + type: 'archivos', + label: 'Buscar archivos', + query: { curp }, + context: { + searchType: 'datos_chiapas', + domain: 'chiapas', + resource: 'mis_archivos', + operation: 'buscar', + variant: 'mis_archivos', + match: 'exacta', + }, + }) + } + + return actions +} + +export const adaptGeneric = (payload) => { + const rows = toRows(payload) + const sourceLabel = String(payload?.source?.label || payload?.source?.bd || 'Resultado').trim() + + return rows.map((row, index) => ({ + stableKey: `${sourceLabel}:${index}:${JSON.stringify(row)}`, + title: `${sourceLabel} #${index + 1}`, + fields: Object.entries(row || {}).map(([label, value]) => ({ label, value: toDisplay(value) })), + raw: row, + actions: toActions(row), + })) +} diff --git a/src/services/resultAdapters/adapterIne.js b/src/services/resultAdapters/adapterIne.js new file mode 100644 index 0000000..cf996af --- /dev/null +++ b/src/services/resultAdapters/adapterIne.js @@ -0,0 +1,94 @@ +// Este adaptador transforma los datos de INE para mostrar todos los campos del registro. +const clean = (value) => String(value || '').trim() + +const humanizeObject = (obj) => { + return Object.entries(obj || {}) + .map(([key, value]) => `${key}: ${toDisplay(value)}`) + .join(', ') +} + +const toDisplay = (value) => { + if (value === null || value === undefined || value === '') { + return '—' + } + + if (typeof value === 'boolean') { + return value ? 'Si' : 'No' + } + + if (Array.isArray(value)) { + if (value.length === 0) { + return '—' + } + + return value + .map((entry, index) => { + if (entry && typeof entry === 'object') { + return `${index + 1}) ${humanizeObject(entry)}` + } + return `${index + 1}) ${toDisplay(entry)}` + }) + .join(' | ') + } + + if (typeof value === 'object') { + return humanizeObject(value) + } + + return String(value) +} + +export const adaptIne = (payload) => { + const rows = Array.isArray(payload?.data) ? payload.data : [] + + return rows.map((row, index) => { + const nombre = [row?.Nombre, row?.Papellido, row?.Sapellido].map(clean).filter(Boolean).join(' ') + const curp = clean(row?.Curp) + const rfc = curp ? curp.substring(0, 10) : '' + + const actions = [] + if (rfc) { + actions.push({ + type: 'vehiculos', + label: 'Buscar vehiculos', + query: { rfc }, + context: { + searchType: 'datos_chiapas', + domain: 'chiapas', + resource: 'vehiculos', + operation: 'buscar', + variant: 'mis_vehiculos', + match: 'exacta', + }, + }) + } + + if (curp) { + actions.push({ + type: 'archivos', + label: 'Buscar archivos', + query: { curp }, + context: { + searchType: 'datos_chiapas', + domain: 'chiapas', + resource: 'mis_archivos', + operation: 'buscar', + variant: 'mis_archivos', + match: 'exacta', + }, + }) + } + + return { + stableKey: `INE:${curp || index}`, + title: nombre ? `INE - ${nombre}` : `INE #${index + 1}`, + // Imprimir registro completo del JSON (incluyendo objetos/arreglos anidados). + fields: Object.entries(row || {}).map(([label, value]) => ({ + label, + value: toDisplay(value), + })), + raw: row, + actions, + } + }) +} diff --git a/src/services/resultAdapters/adapterOportunidades.js b/src/services/resultAdapters/adapterOportunidades.js new file mode 100644 index 0000000..882cd0e --- /dev/null +++ b/src/services/resultAdapters/adapterOportunidades.js @@ -0,0 +1,91 @@ +const clean = (value) => String(value || '').trim() + +const toRows = (payload) => { + if (Array.isArray(payload?.data)) { + return payload.data + } + if (payload?.data && typeof payload.data === 'object') { + return [payload.data] + } + return [] +} + +const buildNombreCompleto = (row) => { + return [row?.Nombre, row?.Paterno, row?.Materno] + .map(clean) + .filter(Boolean) + .join(' ') +} + +const toActions = (row) => { + const curp = clean(row?.Curp || row?.CURP || row?.curp) + const actions = [] + + if (curp) { + actions.push({ + type: 'archivos', + label: 'Buscar archivos', + query: { curp }, + context: { + searchType: 'datos_chiapas', + domain: 'chiapas', + resource: 'mis_archivos', + operation: 'buscar', + variant: 'mis_archivos', + match: 'exacta', + }, + }) + } + + return actions +} + +const buildIdFamAction = (row) => { + const idfam = clean(row?.IdFam || row?.idfam) + if (!idfam) { + return null + } + + return { + type: 'idfamilia', + label: 'Buscar por IdFam', + query: { + idfam, + }, + context: { + searchType: 'datos_chiapas', + domain: 'chiapas', + resource: 'oportunidades', + operation: 'buscar', + variant: 'idfamilia', + match: 'exacta', + }, + } +} + +export const adaptOportunidades = (payload) => { + const rows = toRows(payload) + + return rows.map((row, index) => { + const nombreCompleto = buildNombreCompleto(row) + const curp = clean(row?.Curp || row?.CURP || row?.curp) + const idFamAction = buildIdFamAction(row) + + return { + stableKey: `OPORTUNIDADES:${clean(row?.IdFam)}:${clean(row?.IdInt)}:${curp || index}`, + title: nombreCompleto || `Oportunidades #${index + 1}`, + fields: [ + { label: 'IdFam', value: clean(row?.IdFam) || '—', action: idFamAction || undefined }, + { label: 'IdInt', value: clean(row?.IdInt) || '—' }, + { label: 'Nombre', value: clean(row?.Nombre) || '—' }, + { label: 'Paterno', value: clean(row?.Paterno) || '—' }, + { label: 'Materno', value: clean(row?.Materno) || '—' }, + { label: 'Sexo', value: clean(row?.Sexo) || '—' }, + { label: 'Curp', value: curp || '—' }, + { label: 'Fn', value: clean(row?.Fn) || '—' }, + ], + raw: row, + actions: toActions(row), + } + }) +} diff --git a/src/services/resultAdapters/adapterPlacas.js b/src/services/resultAdapters/adapterPlacas.js new file mode 100644 index 0000000..fabecfe --- /dev/null +++ b/src/services/resultAdapters/adapterPlacas.js @@ -0,0 +1,40 @@ +const clean = (value) => String(value || '').trim() + +export const adaptPlacas = (payload) => { + const rows = Array.isArray(payload?.data) ? payload.data : payload?.data ? [payload.data] : [] + + return rows.map((row, index) => { + const rfc = clean(row?.Rfc || row?.rfc) + const actions = [] + + if (rfc) { + actions.push({ + type: 'vehiculos', + label: 'Buscar vehiculos', + query: { rfc }, + context: { + searchType: 'datos_chiapas', + domain: 'chiapas', + resource: 'vehiculos', + operation: 'buscar', + variant: 'mis_vehiculos', + match: 'exacta', + }, + }) + } + + return { + stableKey: `PLACAS:${clean(row?.Placa || row?.placa) || index}`, + title: `Placas #${index + 1}`, + fields: [ + { label: 'Nombre', value: clean(row?.Nombre || row?.nombre) || '—' }, + { label: 'Placa', value: clean(row?.Placa || row?.placa) || '—' }, + { label: 'RFC', value: rfc || '—' }, + { label: 'Serie', value: clean(row?.Serie || row?.serie) || '—' }, + { label: 'Modelo', value: clean(row?.Modelo || row?.modelo) || '—' }, + ], + raw: row, + actions, + } + }) +} diff --git a/src/services/resultAdapters/adapterSaechf.js b/src/services/resultAdapters/adapterSaechf.js new file mode 100644 index 0000000..7c374fa --- /dev/null +++ b/src/services/resultAdapters/adapterSaechf.js @@ -0,0 +1,103 @@ +const toDisplay = (value) => { + if (value === null || value === undefined || value === '') { + return '—' + } + + if (typeof value === 'boolean') { + return value ? 'Si' : 'No' + } + + if (typeof value === 'object') { + try { + return JSON.stringify(value) + } catch { + return String(value) + } + } + + return String(value) +} + +const toRows = (payload) => { + if (Array.isArray(payload?.data)) { + return payload.data + } + if (payload?.data && typeof payload.data === 'object') { + return [payload.data] + } + return [] +} + +const normalizeRender = (payload) => String(payload?.source?.render || '').trim().toLowerCase() + +const targetResourceByRender = (render) => { + if (render === 'saechf_alumnos') { + return 'saechf_tutores' + } + if (render === 'saechf_tutores') { + return 'saechf_alumnos' + } + return '' +} + +const buildMatriculaAction = (matricula, sourceRender, sourceLabel) => { + const targetTb = targetResourceByRender(sourceRender) + if (!matricula || !targetTb) { + return null + } + + return { + type: 'matricula', + label: `Consultar ${targetTb}`, + query: { + tb: targetTb, + uidb: 'string', + mat: matricula, + matricula, + }, + context: { + searchType: 'datos_chiapas', + domain: 'chiapas', + resource: targetTb, + operation: 'buscar', + variant: 'matricula', + match: 'exacta', + source: sourceLabel || sourceRender || 'saechf', + }, + } +} + +export const adaptSaechf = (payload) => { + const rows = toRows(payload) + const sourceLabel = String(payload?.source?.label || payload?.source?.tabla || 'SAECHF').trim() + const sourceRender = normalizeRender(payload) + + return rows.map((row, index) => { + const matricula = String(row?.Matricula || row?.matricula || '').trim() + const fieldAction = buildMatriculaAction(matricula, sourceRender, sourceLabel) + + const fields = Object.entries(row || {}).map(([label, value]) => { + const normalizedLabel = String(label || '').trim().toLowerCase() + if (normalizedLabel === 'matricula' && fieldAction) { + return { + label, + value: toDisplay(value), + action: fieldAction, + } + } + + return { + label, + value: toDisplay(value), + } + }) + + return { + stableKey: `SAECHF:${sourceRender}:${matricula || index}:${JSON.stringify(row)}`, + title: `${sourceLabel} #${index + 1}`, + fields, + raw: row, + actions: [], + } + }) +} diff --git a/src/services/resultAdapters/index.js b/src/services/resultAdapters/index.js new file mode 100644 index 0000000..a5e066e --- /dev/null +++ b/src/services/resultAdapters/index.js @@ -0,0 +1,57 @@ +import { adaptActas } from './adapterActas' +import { adaptGeneric } from './adapterGeneric' +import { adaptIne } from './adapterIne' +import { adaptOportunidades } from './adapterOportunidades' +import { adaptPlacas } from './adapterPlacas' +import { adaptSaechf } from './adapterSaechf' + +const pickSourceLabel = (payload) => String(payload?.source?.label || payload?.source?.bd || 'resultado').trim() +const pickRender = (payload) => String(payload?.source?.render || '').trim().toLowerCase() +const pickSourceGroupKey = (payload, render) => { + const bd = String(payload?.source?.bd || '').trim().toLowerCase() + const tabla = String(payload?.source?.tabla || '').trim().toLowerCase() + const normalizedRender = String(render || '').trim().toLowerCase() + + const pieces = [normalizedRender || 'resultado', bd || 'sin-bd', tabla || 'sin-tabla'] + return pieces.join(':') +} + +const ADAPTER_BY_RENDER = { + // Actas + actas: adaptActas, + actasv2: adaptActas, + + // Placas + placa: adaptPlacas, + placas: adaptPlacas, + + // INE + ine2018: adaptIne, + + // Oportunidades + oportunidades: adaptOportunidades, + + // SAECHF + saechf_alumnos: adaptSaechf, + saechf_tutores: adaptSaechf, + + // Generic/fallback renders from Tornado + generic: adaptGeneric, + chiapas: adaptGeneric, +} + +// Función principal para adaptar el payload de búsqueda a un formato estandarizado +export const adaptSearchPayload = (payload) => { + const sourceLabel = pickSourceLabel(payload) + const render = pickRender(payload) + const byRender = ADAPTER_BY_RENDER[render] || adaptGeneric + const cards = byRender(payload) + const sourceKey = pickSourceGroupKey(payload, render) + + return { + sourceKey, + sourceLabel, + cards, + raw: payload, + } +} diff --git a/src/services/wsService.js b/src/services/wsService.js new file mode 100644 index 0000000..5184869 --- /dev/null +++ b/src/services/wsService.js @@ -0,0 +1,269 @@ +/** + * Convierte un valor de entorno a booleano. + * + * @param {unknown} value Valor recibido (string, boolean, etc.). + * @param {boolean} fallback Valor por defecto cuando value es nulo/vacio. + * @returns {boolean} + */ +function parseBoolean(value, fallback = true) { + if (value === undefined || value === null || value === '') { + return fallback; + } + return String(value).toLowerCase() === 'true'; +} + +/** + * Decodifica el payload de un JWT de forma tolerante a errores. + * + * @param {string} token JWT en formato header.payload.signature. + * @returns {Record} Objeto payload o {} si no se puede decodificar. + */ +function decodeTokenPayload(token) { + try { + const parts = token.split('.'); + if (parts.length < 2) { + return {}; + } + const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/'); + const normalized = payload.padEnd(Math.ceil(payload.length / 4) * 4, '='); + return JSON.parse(atob(normalized)); + } catch (err) { + return {}; + } +} + +const wsHost = import.meta.env.VITE_WS_HOST; +const wsPort = Number(import.meta.env.VITE_WS_PORT); +const wsPath = import.meta.env.VITE_WS_PATH; +const wsSecure = parseBoolean(import.meta.env.VITE_WS_SECURE, true); + +const WS_DEFAULTS = { + maxRetries: 5, + retryDelayMs: 3000 +}; + +/** + * Construye la URL final del websocket a partir de variables de entorno. + * + * @returns {string} + */ +function buildWsUrl() { + const protocol = wsSecure ? 'wss' : 'ws'; + return `${protocol}://${wsHost}:${wsPort}${wsPath}`; +} + +/** + * Crea un cliente WS reusable para Vue con reconexion, cola de salida y registro inicial. + * + * Protocolo soportado: + * - Mensaje de registro en apertura: { type: 'reg', username, token } + * - Comandos: { type: 'cmd', cmd, username, token, ...extra } + * + * @param {Object} options + * @param {string} options.token Token Bearer usado para registrar y enviar comandos. + * @param {string} [options.username] Username opcional; si no existe se intenta extraer del JWT. + * @param {(payload:{status:string,detail:string}) => void} [options.onStatus] Callback de estado. + * @param {(message:unknown) => void} [options.onMessage] Callback de mensajes entrantes. + * @param {(payload:{message:string,sent:boolean}) => void} [options.onSend] Callback de mensajes salientes. + * @param {number} [options.maxRetries] Maximo de reintentos de reconexion. + * @param {number} [options.retryDelayMs] Delay entre reintentos en ms. + * @returns {{ + * connect: () => void, + * disconnect: () => void, + * sendRaw: (message:string) => void, + * sendCommand: (cmd:string, extra?:Record) => void, + * getUsername: () => string + * }} API publica del cliente. + */ +export function createWsClient(options) { + const { + token, + username, + onStatus, + onMessage, + onSend, + maxRetries = WS_DEFAULTS.maxRetries, + retryDelayMs = WS_DEFAULTS.retryDelayMs + } = options; + + const payload = decodeTokenPayload(token || ''); + const resolvedUsername = username || payload.usuario || 'vue_client'; + + let socket = null; + let retries = 0; + let reconnectTimer = null; + let forceClose = false; + const outbox = []; + + /** + * Emite cambios de estado para actualizar UI/diagnostico. + * @param {string} status + * @param {string} [detail=''] + */ + function emitStatus(status, detail = '') { + if (onStatus) { + onStatus({ status, detail }); + } + } + + /** + * Vacia la cola local cuando el socket ya esta abierto. + */ + function flushOutbox() { + while (socket && socket.readyState === WebSocket.OPEN && outbox.length > 0) { + const next = outbox.shift(); + socket.send(next); + } + } + + /** + * Envia texto sin envolver; si el socket no esta abierto, se encola. + * @param {string} message + */ + function sendRaw(message) { + if (!message) { + return; + } + + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(message); + if (onSend) { + onSend({ message, sent: true }); + } + return; + } + + outbox.push(message); + if (onSend) { + onSend({ message, sent: false }); + } + } + + /** + * Envia un comando siguiendo el sobre usado por el backend WS. + * @param {string} cmd Nombre del comando. + * @param {Record} [extra={}] Campos extra (por ejemplo busqueda). + */ + function sendCommand(cmd, extra = {}) { + const envelope = { + type: 'cmd', + cmd, + username: resolvedUsername, + token, + ...extra + }; + sendRaw(JSON.stringify(envelope)); + } + + /** + * Envia el mensaje de registro al abrir la conexion. + */ + function registerOnOpen() { + const reg = { + type: 'reg', + username: resolvedUsername, + token + }; + sendRaw(JSON.stringify(reg)); + } + + /** + * Programa reconexion con limite de intentos, salvo cierre manual. + * @param {string} reason + */ + function scheduleReconnect(reason) { + if (forceClose) { + return; + } + + if (retries >= maxRetries) { + emitStatus('failed', reason || 'max_retries'); + return; + } + + retries += 1; + emitStatus('reconnecting', `attempt_${retries}`); + + reconnectTimer = window.setTimeout(() => { + connect(); + }, retryDelayMs); + } + + /** + * Abre la conexion websocket y enlaza handlers de ciclo de vida. + */ + function connect() { + if (!token) { + emitStatus('error', 'missing_token'); + return; + } + + if (socket && (socket.readyState === WebSocket.CONNECTING || socket.readyState === WebSocket.OPEN)) { + emitStatus('status', socket.readyState === WebSocket.OPEN ? 'already_open' : 'already_connecting'); + return; + } + + if (reconnectTimer) { + window.clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + forceClose = false; + const url = buildWsUrl(); + emitStatus('connecting', url); + + socket = new WebSocket(url); + + socket.onopen = () => { + retries = 0; + emitStatus('open', url); + registerOnOpen(); + flushOutbox(); + }; + + socket.onmessage = (event) => { + // Intentar parsear JSON, pero si falla, mantener el mensaje crudo. + let parsed = event.data; + try { + parsed = JSON.parse(event.data); + } catch (err) { + // Mantener mensaje crudo si no es JSON. + } + if (onMessage) { + onMessage(parsed); + } + }; + + socket.onerror = () => { + emitStatus('error', 'socket_error'); + }; + + socket.onclose = (event) => { + emitStatus('closed', String(event.code || 'unknown')); + scheduleReconnect(`close_${event.code || 'unknown'}`); + }; + } + + /** + * Cierra la conexion de forma explicita y detiene reintentos pendientes. + */ + function disconnect() { + forceClose = true; + if (reconnectTimer) { + window.clearTimeout(reconnectTimer); + reconnectTimer = null; + } + if (socket && socket.readyState <= WebSocket.OPEN) { + socket.close(); + } + emitStatus('offline'); + } + + return { + connect, + disconnect, + sendRaw, + sendCommand, + getUsername: () => resolvedUsername + }; +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..4c198fa --- /dev/null +++ b/src/style.css @@ -0,0 +1,10 @@ +@import "tailwindcss"; + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Plus Jakarta Sans", "Segoe UI", sans-serif; +} diff --git a/src/views/BillingView.vue b/src/views/BillingView.vue new file mode 100644 index 0000000..2a306cb --- /dev/null +++ b/src/views/BillingView.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/views/ChangePasswordView.vue b/src/views/ChangePasswordView.vue new file mode 100644 index 0000000..434754c --- /dev/null +++ b/src/views/ChangePasswordView.vue @@ -0,0 +1,159 @@ + + + diff --git a/src/views/ConfigurationView.vue b/src/views/ConfigurationView.vue new file mode 100644 index 0000000..917067e --- /dev/null +++ b/src/views/ConfigurationView.vue @@ -0,0 +1,127 @@ + + + diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue new file mode 100644 index 0000000..a906e7a --- /dev/null +++ b/src/views/DashboardView.vue @@ -0,0 +1,262 @@ + + + diff --git a/src/views/IntegrationsView.vue b/src/views/IntegrationsView.vue new file mode 100644 index 0000000..59dcbbf --- /dev/null +++ b/src/views/IntegrationsView.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue new file mode 100644 index 0000000..3c80ab8 --- /dev/null +++ b/src/views/LoginView.vue @@ -0,0 +1,101 @@ + + + diff --git a/src/views/Misbusquedas.vue b/src/views/Misbusquedas.vue new file mode 100644 index 0000000..d9aeb59 --- /dev/null +++ b/src/views/Misbusquedas.vue @@ -0,0 +1,344 @@ + + + diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue new file mode 100644 index 0000000..f01fd6a --- /dev/null +++ b/src/views/UsersView.vue @@ -0,0 +1,66 @@ + + + diff --git a/test.db b/test.db new file mode 100644 index 0000000..e69de29 diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..4815a12 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue(), tailwindcss()], +})