@softdin/portal-admin 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +95 -2
  2. package/dist-lib/chunks/AppLayout-COCPfLWo.js +303 -0
  3. package/dist-lib/chunks/AuditoriaView-Bcf_kOqI.js +643 -0
  4. package/dist-lib/chunks/CarnetView-BsdUtYeu.js +450 -0
  5. package/dist-lib/chunks/CarteleraView-CRnVjt-S.js +996 -0
  6. package/dist-lib/chunks/CertificadosView-DvHDflcN.js +549 -0
  7. package/dist-lib/chunks/ChatBandejaView-HqhdSw7G.js +274 -0
  8. package/dist-lib/chunks/ChatConfiguracionView-BdH5gDdR.js +716 -0
  9. package/dist-lib/chunks/ChatConocimientoView-nKTAKxFa.js +263 -0
  10. package/dist-lib/chunks/ChatConversacionView-Do74V09l.js +477 -0
  11. package/dist-lib/chunks/ChatInformeView-BIiAH9L_.js +280 -0
  12. package/dist-lib/chunks/ChatRespuestasView-QFXbyg8U.js +183 -0
  13. package/dist-lib/chunks/EmpleadosView-BtqrO5-8.js +311 -0
  14. package/dist-lib/chunks/HomologacionView-UCZk4u6R.js +949 -0
  15. package/dist-lib/chunks/IngresoSsoView-BKkcnrYO.js +98 -0
  16. package/dist-lib/chunks/SolicitudesView-CYPD2A1s.js +900 -0
  17. package/dist-lib/chunks/TiposSolicitudView-CrWTVizl.js +168 -0
  18. package/dist-lib/chunks/ToggleSwitch-BYrIyheq.js +55 -0
  19. package/dist-lib/chunks/_plugin-vue_export-helper-BOaGB7Aw.js +8 -0
  20. package/dist-lib/chunks/api-M7D1c3IT.js +97 -0
  21. package/dist-lib/chunks/apiError-BHGbwz97.js +15 -0
  22. package/dist-lib/chunks/auth.store-BrGKZg-7.js +220 -0
  23. package/dist-lib/chunks/chat.service-C7ZQf4w3.js +241 -0
  24. package/dist-lib/chunks/cliente-interno.store-De39VsEE.js +203 -0
  25. package/dist-lib/chunks/homologacion-CoeHD7cB.js +131 -0
  26. package/dist-lib/chunks/portal-empleado-6KQrnsQJ.js +120 -0
  27. package/dist-lib/chunks/tipos-D93bLfFC.js +22 -0
  28. package/dist-lib/portal-admin.js +196 -0
  29. package/dist-lib/style.css +4813 -0
  30. package/dist-lib/types/App.vue.d.ts +3 -0
  31. package/dist-lib/types/components/AppLayout.vue.d.ts +3 -0
  32. package/dist-lib/types/components/CarnetClientesInternos.vue.d.ts +11 -0
  33. package/dist-lib/types/components/InvitacionPortalModal.vue.d.ts +20 -0
  34. package/dist-lib/types/components/MenuUsuario.vue.d.ts +3 -0
  35. package/dist-lib/types/components/VistoBuenoPortalModal.vue.d.ts +23 -0
  36. package/dist-lib/types/components/auditoria/AuditoriaFila.vue.d.ts +16 -0
  37. package/dist-lib/types/components/auditoria/AuditoriaFiltros.vue.d.ts +30 -0
  38. package/dist-lib/types/components/auditoria/AuditoriaPaginador.vue.d.ts +14 -0
  39. package/dist-lib/types/components/cartelera/PaginaCarteleraImagen.vue.d.ts +14 -0
  40. package/dist-lib/types/components/cartelera/PublicacionFormModal.vue.d.ts +27 -0
  41. package/dist-lib/types/components/cartelera/VistaCelularCartelera.vue.d.ts +34 -0
  42. package/dist-lib/types/components/certificados/AtenderCertificadoModal.vue.d.ts +22 -0
  43. package/dist-lib/types/components/certificados/RechazarCertificadoModal.vue.d.ts +18 -0
  44. package/dist-lib/types/components/certificados/ResumenSolicitudCertificado.vue.d.ts +8 -0
  45. package/dist-lib/types/components/chat/ChatClientesInternos.vue.d.ts +21 -0
  46. package/dist-lib/types/components/chat/ChatHorarioCampos.vue.d.ts +21 -0
  47. package/dist-lib/types/components/forms/ExportExcelButton.vue.d.ts +30 -0
  48. package/dist-lib/types/components/forms/ToggleSwitch.vue.d.ts +17 -0
  49. package/dist-lib/types/components/homologacion/HomologacionFila.vue.d.ts +30 -0
  50. package/dist-lib/types/components/homologacion/TipoPermisoFormModal.vue.d.ts +26 -0
  51. package/dist-lib/types/components/homologacion/TipoSolicitudFormModal.vue.d.ts +27 -0
  52. package/dist-lib/types/lib/auditoria.d.ts +25 -0
  53. package/dist-lib/types/lib/cartelera-paginas.d.ts +26 -0
  54. package/dist-lib/types/lib/cartelera-vigencia.d.ts +3 -0
  55. package/dist-lib/types/lib/export-excel-csv.d.ts +4 -0
  56. package/dist-lib/types/lib/export-excel.d.ts +9 -0
  57. package/dist-lib/types/lib/exportar-listado.d.ts +68 -0
  58. package/dist-lib/types/lib/homologacion.d.ts +41 -0
  59. package/dist-lib/types/lib/portal-empleado.d.ts +32 -0
  60. package/dist-lib/types/modulo.d.ts +31 -0
  61. package/dist-lib/types/portal-admin/config.d.ts +17 -0
  62. package/dist-lib/types/portal-admin/rutas.d.ts +17 -0
  63. package/dist-lib/types/portal-admin/tipos.d.ts +68 -0
  64. package/dist-lib/types/router/index.d.ts +7 -0
  65. package/dist-lib/types/services/api.d.ts +29 -0
  66. package/dist-lib/types/services/auditoria.service.d.ts +63 -0
  67. package/dist-lib/types/services/auth.service.d.ts +61 -0
  68. package/dist-lib/types/services/carnet.service.d.ts +56 -0
  69. package/dist-lib/types/services/cartelera.service.d.ts +69 -0
  70. package/dist-lib/types/services/chat.service.d.ts +279 -0
  71. package/dist-lib/types/services/configuracion-cliente-interno.service.d.ts +23 -0
  72. package/dist-lib/types/services/homologacion.service.d.ts +105 -0
  73. package/dist-lib/types/services/portal-empleado.service.d.ts +112 -0
  74. package/dist-lib/types/services/solicitudes-certificado.service.d.ts +59 -0
  75. package/dist-lib/types/stores/auth.store.d.ts +125 -0
  76. package/dist-lib/types/stores/cliente-interno.store.d.ts +65 -0
  77. package/dist-lib/types/utils/apiError.d.ts +4 -0
  78. package/dist-lib/types/views/AuditoriaView.vue.d.ts +3 -0
  79. package/dist-lib/types/views/CarnetView.vue.d.ts +3 -0
  80. package/dist-lib/types/views/CarteleraView.vue.d.ts +3 -0
  81. package/dist-lib/types/views/CertificadosView.vue.d.ts +3 -0
  82. package/dist-lib/types/views/EmpleadosView.vue.d.ts +3 -0
  83. package/dist-lib/types/views/HomologacionView.vue.d.ts +4 -0
  84. package/dist-lib/types/views/IngresoSsoView.vue.d.ts +3 -0
  85. package/dist-lib/types/views/LoginView.vue.d.ts +3 -0
  86. package/dist-lib/types/views/OlvideView.vue.d.ts +3 -0
  87. package/dist-lib/types/views/RestablecerView.vue.d.ts +3 -0
  88. package/dist-lib/types/views/SolicitudesView.vue.d.ts +3 -0
  89. package/dist-lib/types/views/TiposSolicitudView.vue.d.ts +3 -0
  90. package/dist-lib/types/views/chat/ChatBandejaView.vue.d.ts +3 -0
  91. package/dist-lib/types/views/chat/ChatConfiguracionView.vue.d.ts +3 -0
  92. package/dist-lib/types/views/chat/ChatConocimientoView.vue.d.ts +3 -0
  93. package/dist-lib/types/views/chat/ChatConversacionView.vue.d.ts +11 -0
  94. package/dist-lib/types/views/chat/ChatInformeView.vue.d.ts +3 -0
  95. package/dist-lib/types/views/chat/ChatRespuestasView.vue.d.ts +3 -0
  96. package/package.json +66 -4
package/README.md CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # softdin-portal-central — Administración del portal
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ App web de RRHH de cada cliente del **Portal de autogestión**:
4
+
5
+ - **Solicitudes:** bandeja y visto bueno de nómina.
6
+ - **Empleados:** estado de la cuenta e invitación por QR o correo.
7
+ - **Tipos de solicitud:** cuáles puede radicar el empleado y si exigen visto bueno.
8
+
9
+ Habla solo con **Gestión del Portal** (`softdin-portal-api`, rutas `/api/v1/rrhh`). No usa los usuarios de SoftDIN:
10
+
11
+ - Los usuarios de RRHH viven en la central del portal y los crea `php artisan portal:usuario`.
12
+ - Cada usuario fija su contraseña con el enlace que le llega al correo (`/restablecer`).
13
+ - La sesión queda ligada a un cliente; quien tiene varios cambia desde la barra superior.
14
+
15
+ El visto bueno es lo único que toca SoftDIN, y viaja firmado de servidor a servidor desde el API del portal.
16
+
17
+ ## Desarrollo
18
+
19
+ ```bash
20
+ npm install
21
+ npm run dev # http://localhost:5177
22
+ npm test
23
+ npm run build
24
+ ```
25
+
26
+ `VITE_API_URL` (en `.env`, ver `.env.example`) apunta al API del portal. En el API, `PORTAL_RRHH_URL` debe apuntar a esta app, porque arma con ella los enlaces de los correos, y `CORS_ALLOWED_ORIGINS` debe incluirla.
27
+
28
+ Antes de subir cambios al `package-lock.json`, valídelo con npm 10.9.2 (el de Cloudflare Pages), con `npm ci` sobre una copia limpia.
29
+
30
+ ## Despliegue (Cloudflare Pages)
31
+
32
+ El proyecto de Pages **softdin-portal-central** está conectado a este repositorio: cada push a `main`
33
+ compila y publica en `gestionportal.dinsoft.online`; las otras ramas generan vistas previas. Configuración:
34
+ `npm run build`, salida `dist`, variable `VITE_API_URL=https://apiportal.dinsoft.shop/api/v1`.
35
+ No hace falta subir nada a mano con wrangler.
36
+
37
+ ## Paquete `@softdin/portal-admin`
38
+
39
+ Las pantallas del portal también se publican como módulo para montarlas en otra app Vue. Lo
40
+ usan **esta misma app** (en la raíz, con su ingreso de usuario y contraseña) y
41
+ **softdin-central** (en `/central/portal`, con ingreso SSO de superusuarios): un solo código.
42
+
43
+ ```ts
44
+ import { instalarPortalAdmin } from '@softdin/portal-admin'
45
+ import '@softdin/portal-admin/style.css'
46
+
47
+ // Con Pinia instalada y antes de app.use(router).
48
+ const portal = instalarPortalAdmin(router, {
49
+ apiUrl: 'https://apiportal.dinsoft.shop/api/v1', // opcional con SSO: se usa portal_api_url
50
+ basePath: '/central/portal',
51
+ rutaPadre: 'central-layout', // ruta con nombre bajo la que cuelgan (su layout y guardias)
52
+ metaRutas: { module: 'Portal de autogestión' },
53
+ sesion: { obtenerAsercion: () => miApi.post('/admin/portal/sso').then((r) => r.data) },
54
+ alExpirar: () => {/* no se pudo renovar */},
55
+ alSalir: () => router.push('/central'),
56
+ })
57
+ await portal.cerrarSesion() // al salir de la app anfitriona
58
+ ```
59
+
60
+ - **Rutas:** nombres `portal-*` (`RUTAS_PORTAL`), con `meta.portalAdmin`; la guardia
61
+ (`guardiaPortalAdmin`) solo mira esas. Con `sesion` se agrega `portal-ingreso`
62
+ (`{basePath}/ingreso`): pide la aserción, muestra el selector si hay varios clientes y pide
63
+ otra aserción para el segundo paso. Sin `sesion`, sin token va a la ruta `login` de la app.
64
+ - **Sesión:** stores `portal-auth` y `portal-cliente-interno`; cliente axios propio
65
+ (`portalApi`, base `${apiUrl}/rrhh`). Con `sesion`, un 401 se renueva una sola vez con una
66
+ aserción nueva (las peticiones simultáneas comparten la renovación) y se repite la petición.
67
+ - **Estilos:** el paquete no trae Tailwind. La app debe escanear sus clases con
68
+ `@source '<ruta>/node_modules/@softdin/portal-admin/dist-lib';` en el mismo CSS donde importa
69
+ `@softdin/shared/style.css`, e importar `@softdin/portal-admin/style.css` (solo los
70
+ `<style scoped>`).
71
+ - **pdf.js:** `pdfjs-dist` y `qrcode-generator` son dependencias del paquete; el worker se
72
+ importa como `pdfjs-dist/build/pdf.worker.min.mjs?url` y queda **sin resolver** en el
73
+ paquete, para que el Vite de la app lo copie a sus assets. La app debe poner
74
+ `@softdin/portal-admin` en `optimizeDeps.exclude` (esbuild no entiende `?url`).
75
+ - **Peers:** `vue`, `vue-router`, `pinia`, `axios` y `@softdin/shared` (`^0.4.0`).
76
+ - **Ámbito tenant (0.2.0, softdin-tenant):** `cambioDeTenant: false` (o una sesión con
77
+ `ambito: "tenant"`, `usePortalAuthStore().ambito`) quita de la barra el selector de tenant:
78
+ la sesión SSO es solo del tenant de la app. Si `rrhh/me` (o el ingreso) trae
79
+ `clientes_internos` con una lista, el selector «Cliente» ofrece solo esos y no «Todo el
80
+ tenant»; con `[]` (ninguno) ofrece solo «Todo el tenant», para consultar; con `null` o sin
81
+ el campo, como antes. Tampoco ofrece los que `GET rrhh/clientes-internos` marca
82
+ `permitido: false` (`usePortalAuthStore().clientesInternos`,
83
+ `usePortalClienteInternoStore().opciones`). Con SSO, si el ingreso no trae `clientes_internos`
84
+ se confirma con `rrhh/me`. Un 403 «No tiene acceso a ese cliente.» (escritura en un cliente
85
+ interno fuera del alcance) ya no cierra la sesión: lo muestra la pantalla.
86
+
87
+ ```bash
88
+ npm run build:lib # dist-lib/: ESM + style.css + types/ (sin el alias @/)
89
+ npm pack # tarball para probar en otra app sin publicar
90
+ npm publish # corre las pruebas y build:lib (prepublishOnly)
91
+ ```
92
+
93
+ Las rutas que eran de esta app (`solicitudes`, `chat`, `auditoria`…) se llaman ahora
94
+ `portal-solicitudes`, `portal-chat`, `portal-auditoria`…, y se registran en
95
+ `src/portal-admin/rutas.ts` (ya no en `src/router/index.ts`, que se quedó con el ingreso,
96
+ `olvide` y `restablecer`). Las direcciones no cambiaron.
@@ -0,0 +1,303 @@
1
+ import { n as configPortal, t as useAuthStore } from "./auth.store-BrGKZg-7.js";
2
+ import { t as useClienteInternoStore } from "./cliente-interno.store-De39VsEE.js";
3
+ import { t as RUTAS_PORTAL } from "./tipos-D93bLfFC.js";
4
+ import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
5
+ import { i as chatService } from "./chat.service-C7ZQf4w3.js";
6
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, onBeforeUnmount, onMounted, openBlock, ref, renderList, toDisplayString, unref, vModelSelect, watch, withCtx, withDirectives, withModifiers } from "vue";
7
+ import { RouterLink, RouterView, useRoute, useRouter } from "vue-router";
8
+ //#region src/components/MenuUsuario.vue?vue&type=script&setup=true&lang.ts
9
+ var _hoisted_1$1 = ["aria-expanded"];
10
+ var _hoisted_2$1 = { class: "info" };
11
+ var _hoisted_3$1 = { class: "nombre" };
12
+ var _hoisted_4$1 = { class: "tenant" };
13
+ var _hoisted_5$1 = {
14
+ class: "avatar",
15
+ "aria-hidden": "true"
16
+ };
17
+ var _hoisted_6$1 = {
18
+ class: "chevron",
19
+ "aria-hidden": "true"
20
+ };
21
+ var _hoisted_7$1 = { class: "cabecera" };
22
+ var _hoisted_8$1 = {
23
+ class: "avatar avatar-grande",
24
+ "aria-hidden": "true"
25
+ };
26
+ var _hoisted_9$1 = { class: "min-w-0" };
27
+ var _hoisted_10$1 = { class: "panel-nombre" };
28
+ var _hoisted_11$1 = { class: "panel-dato" };
29
+ var _hoisted_12$1 = { class: "contexto" };
30
+ //#endregion
31
+ //#region src/components/MenuUsuario.vue
32
+ var MenuUsuario_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
33
+ __name: "MenuUsuario",
34
+ setup(__props) {
35
+ const auth = useAuthStore();
36
+ const clientesInternos = useClienteInternoStore();
37
+ const router = useRouter();
38
+ const route = useRoute();
39
+ const abierto = ref(false);
40
+ const raiz = ref(null);
41
+ const disparador = ref(null);
42
+ const panel = ref(null);
43
+ const iniciales = computed(() => {
44
+ return (auth.usuario?.nombre?.trim() ?? "").split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "RH";
45
+ });
46
+ function opciones() {
47
+ return Array.from(panel.value?.querySelectorAll("[role=\"menuitem\"]") ?? []);
48
+ }
49
+ async function abrir() {
50
+ abierto.value = true;
51
+ await nextTick();
52
+ opciones()[0]?.focus();
53
+ }
54
+ function cerrar(devolverFoco = false) {
55
+ if (!abierto.value) return;
56
+ abierto.value = false;
57
+ if (devolverFoco) disparador.value?.focus();
58
+ }
59
+ function alternar() {
60
+ if (abierto.value) cerrar();
61
+ else abrir();
62
+ }
63
+ /** Flechas entre las opciones del menú; Escape lo cierra y devuelve el foco al botón. */
64
+ function teclado(e) {
65
+ if (!abierto.value) return;
66
+ if (e.key === "Escape") {
67
+ e.preventDefault();
68
+ cerrar(true);
69
+ return;
70
+ }
71
+ if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
72
+ const lista = opciones();
73
+ if (!lista.length) return;
74
+ e.preventDefault();
75
+ lista[(lista.indexOf(document.activeElement) + (e.key === "ArrowDown" ? 1 : -1) + lista.length) % lista.length]?.focus();
76
+ }
77
+ function clicAfuera(e) {
78
+ if (abierto.value && raiz.value && !raiz.value.contains(e.target)) cerrar();
79
+ }
80
+ async function salir() {
81
+ cerrar();
82
+ await auth.logout();
83
+ if (configPortal.alSalir) configPortal.alSalir();
84
+ else await router.replace({ name: "login" });
85
+ }
86
+ watch(() => route.fullPath, () => cerrar());
87
+ onMounted(() => document.addEventListener("click", clicAfuera));
88
+ onBeforeUnmount(() => document.removeEventListener("click", clicAfuera));
89
+ return (_ctx, _cache) => {
90
+ return openBlock(), createElementBlock("div", {
91
+ ref_key: "raiz",
92
+ ref: raiz,
93
+ class: "menu-usuario",
94
+ onKeydown: teclado
95
+ }, [createElementVNode("button", {
96
+ ref_key: "disparador",
97
+ ref: disparador,
98
+ type: "button",
99
+ class: "disparador",
100
+ "aria-label": "Abrir menú de usuario",
101
+ "aria-haspopup": "menu",
102
+ "aria-expanded": abierto.value,
103
+ "aria-controls": "menu-usuario-panel",
104
+ onClick: withModifiers(alternar, ["stop"])
105
+ }, [
106
+ createElementVNode("span", _hoisted_2$1, [createElementVNode("span", _hoisted_3$1, toDisplayString(unref(auth).usuario?.nombre), 1), createElementVNode("span", _hoisted_4$1, toDisplayString(unref(auth).cliente?.nombre), 1)]),
107
+ createElementVNode("span", _hoisted_5$1, toDisplayString(iniciales.value), 1),
108
+ createElementVNode("span", _hoisted_6$1, toDisplayString(abierto.value ? "▴" : "▾"), 1)
109
+ ], 8, _hoisted_1$1), abierto.value ? (openBlock(), createElementBlock("div", {
110
+ key: 0,
111
+ id: "menu-usuario-panel",
112
+ ref_key: "panel",
113
+ ref: panel,
114
+ class: "panel",
115
+ role: "menu",
116
+ "aria-label": "Opciones de usuario"
117
+ }, [
118
+ createElementVNode("div", _hoisted_7$1, [createElementVNode("span", _hoisted_8$1, toDisplayString(iniciales.value), 1), createElementVNode("div", _hoisted_9$1, [createElementVNode("p", _hoisted_10$1, toDisplayString(unref(auth).usuario?.nombre), 1), createElementVNode("p", _hoisted_11$1, toDisplayString(unref(auth).usuario?.email), 1)])]),
119
+ createElementVNode("dl", _hoisted_12$1, [createElementVNode("div", null, [_cache[0] || (_cache[0] = createElementVNode("dt", null, "Tenant", -1)), createElementVNode("dd", null, toDisplayString(unref(auth).cliente?.nombre ?? "—"), 1)]), createElementVNode("div", null, [_cache[1] || (_cache[1] = createElementVNode("dt", null, "Cliente", -1)), createElementVNode("dd", null, toDisplayString(unref(clientesInternos).nombre(unref(clientesInternos).seleccionado)), 1)])]),
120
+ createElementVNode("div", { class: "acciones" }, [createElementVNode("button", {
121
+ type: "button",
122
+ role: "menuitem",
123
+ class: "accion accion-peligro",
124
+ onClick: salir
125
+ }, "Salir")])
126
+ ], 512)) : createCommentVNode("", true)], 544);
127
+ };
128
+ }
129
+ }), [["__scopeId", "data-v-e5b2d0eb"]]);
130
+ //#endregion
131
+ //#region src/components/AppLayout.vue?vue&type=script&setup=true&lang.ts
132
+ var _hoisted_1 = { class: "min-h-full bg-surface" };
133
+ var _hoisted_2 = { class: "bg-sidebar text-white" };
134
+ var _hoisted_3 = { class: "mx-auto flex max-w-6xl flex-wrap items-center gap-3 px-4 py-3" };
135
+ var _hoisted_4 = {
136
+ key: 0,
137
+ class: "text-sm"
138
+ };
139
+ var _hoisted_5 = ["value", "disabled"];
140
+ var _hoisted_6 = ["value"];
141
+ var _hoisted_7 = {
142
+ key: 1,
143
+ class: "text-sm text-brand-100"
144
+ };
145
+ var _hoisted_8 = {
146
+ key: 2,
147
+ class: "flex min-w-0 items-center gap-2 text-sm",
148
+ for: "barra-cliente-interno"
149
+ };
150
+ var _hoisted_9 = {
151
+ key: 0,
152
+ value: null,
153
+ class: "text-slate-900"
154
+ };
155
+ var _hoisted_10 = ["value"];
156
+ var _hoisted_11 = {
157
+ class: "mx-auto flex max-w-6xl gap-1 overflow-x-auto px-4",
158
+ "aria-label": "Secciones"
159
+ };
160
+ var _hoisted_12 = ["aria-label"];
161
+ var _hoisted_13 = { class: "mx-auto max-w-6xl px-4 py-6" };
162
+ //#endregion
163
+ //#region src/components/AppLayout.vue
164
+ var AppLayout_default = /* @__PURE__ */ defineComponent({
165
+ __name: "AppLayout",
166
+ setup(__props) {
167
+ const auth = useAuthStore();
168
+ const clientesInternos = useClienteInternoStore();
169
+ const router = useRouter();
170
+ const cambiando = ref(false);
171
+ clientesInternos.cargar();
172
+ /** Selector «Cliente» de la barra: lo usan todas las pestañas (null = todo el tenant). */
173
+ const clienteInterno = computed({
174
+ get: () => clientesInternos.seleccionado,
175
+ set: (id) => clientesInternos.elegir(id)
176
+ });
177
+ /** El portal-api responde 403 a `cambiar-cliente` en una sesión de ámbito tenant. */
178
+ const puedeCambiarTenant = computed(() => configPortal.cambioDeTenant && auth.ambito !== "tenant" && auth.clientes.length > 1);
179
+ const pestanas = [
180
+ {
181
+ to: { name: RUTAS_PORTAL.solicitudes },
182
+ texto: "Solicitudes"
183
+ },
184
+ {
185
+ to: { name: RUTAS_PORTAL.certificados },
186
+ texto: "Certificados"
187
+ },
188
+ {
189
+ to: { name: RUTAS_PORTAL.empleados },
190
+ texto: "Empleados"
191
+ },
192
+ {
193
+ to: { name: RUTAS_PORTAL.tiposSolicitud },
194
+ texto: "Tipos de solicitud"
195
+ },
196
+ {
197
+ to: { name: RUTAS_PORTAL.homologacion },
198
+ texto: "Homologación"
199
+ },
200
+ {
201
+ to: { name: RUTAS_PORTAL.carnet },
202
+ texto: "Carnet digital"
203
+ },
204
+ {
205
+ to: { name: RUTAS_PORTAL.cartelera },
206
+ texto: "Cartelera"
207
+ }
208
+ ];
209
+ const chat = ref(null);
210
+ const pendientesChat = computed(() => chat.value ? chat.value.sin_asignar + chat.value.no_leidas : 0);
211
+ async function consultarChat() {
212
+ try {
213
+ chat.value = await chatService.resumen();
214
+ } catch {}
215
+ }
216
+ let reloj = null;
217
+ onMounted(() => {
218
+ consultarChat();
219
+ reloj = setInterval(() => void consultarChat(), 3e4);
220
+ });
221
+ const quitarGuardia = router.afterEach(() => void consultarChat());
222
+ onBeforeUnmount(quitarGuardia);
223
+ onBeforeUnmount(() => {
224
+ if (reloj) clearInterval(reloj);
225
+ });
226
+ async function cambiarCliente(e) {
227
+ const id = Number(e.target.value);
228
+ if (!id || id === auth.cliente?.id) return;
229
+ cambiando.value = true;
230
+ try {
231
+ await auth.cambiarCliente(id);
232
+ window.location.assign(configPortal.basePath);
233
+ } finally {
234
+ cambiando.value = false;
235
+ }
236
+ }
237
+ return (_ctx, _cache) => {
238
+ return openBlock(), createElementBlock("div", _hoisted_1, [createElementVNode("header", _hoisted_2, [createElementVNode("div", _hoisted_3, [
239
+ _cache[3] || (_cache[3] = createElementVNode("div", { class: "min-w-0 flex-1" }, [createElementVNode("p", { class: "text-xs uppercase tracking-wider text-brand-100" }, "Portal de autogestión"), createElementVNode("h1", { class: "text-lg font-semibold" }, "Administración del portal")], -1)),
240
+ puedeCambiarTenant.value ? (openBlock(), createElementBlock("label", _hoisted_4, [_cache[1] || (_cache[1] = createElementVNode("span", { class: "sr-only" }, "Tenant", -1)), createElementVNode("select", {
241
+ value: unref(auth).cliente?.id,
242
+ class: "rounded-lg border border-white/30 bg-white/10 px-2 py-1 text-sm text-white",
243
+ disabled: cambiando.value,
244
+ onChange: cambiarCliente
245
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(auth).clientes, (c) => {
246
+ return openBlock(), createElementBlock("option", {
247
+ key: c.id,
248
+ value: c.id,
249
+ class: "text-slate-900"
250
+ }, toDisplayString(c.nombre), 9, _hoisted_6);
251
+ }), 128))], 40, _hoisted_5)])) : (openBlock(), createElementBlock("p", _hoisted_7, toDisplayString(unref(auth).cliente?.nombre), 1)),
252
+ unref(auth).isAuthenticated ? (openBlock(), createElementBlock("label", _hoisted_8, [_cache[2] || (_cache[2] = createElementVNode("span", { class: "text-brand-100" }, "Cliente", -1)), withDirectives(createElementVNode("select", {
253
+ id: "barra-cliente-interno",
254
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => clienteInterno.value = $event),
255
+ class: "min-w-0 max-w-[13rem] rounded-lg border border-white/30 bg-white/10 px-2 py-1 text-sm text-white sm:max-w-xs"
256
+ }, [!unref(clientesInternos).restringido || unref(clientesInternos).ninguno ? (openBlock(), createElementBlock("option", _hoisted_9, "Todo el tenant")) : createCommentVNode("", true), (openBlock(true), createElementBlock(Fragment, null, renderList(unref(clientesInternos).opciones, (c) => {
257
+ return openBlock(), createElementBlock("option", {
258
+ key: c.id,
259
+ value: c.id,
260
+ class: "text-slate-900"
261
+ }, toDisplayString(c.nombre), 9, _hoisted_10);
262
+ }), 128))], 512), [[vModelSelect, clienteInterno.value]])])) : createCommentVNode("", true),
263
+ createVNode(MenuUsuario_default)
264
+ ]), createElementVNode("nav", _hoisted_11, [
265
+ (openBlock(), createElementBlock(Fragment, null, renderList(pestanas, (p) => {
266
+ return createVNode(unref(RouterLink), {
267
+ key: p.texto,
268
+ to: p.to,
269
+ class: "whitespace-nowrap rounded-t-lg px-4 py-2 text-sm text-brand-100 hover:text-white",
270
+ "active-class": "!bg-surface !text-slate-900 font-semibold"
271
+ }, {
272
+ default: withCtx(() => [createTextVNode(toDisplayString(p.texto), 1)]),
273
+ _: 2
274
+ }, 1032, ["to"]);
275
+ }), 64)),
276
+ chat.value && (chat.value.atiende || chat.value.es_admin) ? (openBlock(), createBlock(unref(RouterLink), {
277
+ key: 0,
278
+ to: { name: unref(RUTAS_PORTAL).chat },
279
+ class: "whitespace-nowrap rounded-t-lg px-4 py-2 text-sm text-brand-100 hover:text-white",
280
+ "active-class": "!bg-surface !text-slate-900 font-semibold"
281
+ }, {
282
+ default: withCtx(() => [_cache[4] || (_cache[4] = createTextVNode(" Chat ", -1)), pendientesChat.value > 0 ? (openBlock(), createElementBlock("span", {
283
+ key: 0,
284
+ class: "ml-1 rounded-full bg-red-600 px-1.5 text-xs font-semibold text-white",
285
+ "aria-label": `${pendientesChat.value} por atender`
286
+ }, toDisplayString(pendientesChat.value), 9, _hoisted_12)) : createCommentVNode("", true)]),
287
+ _: 1
288
+ }, 8, ["to"])) : createCommentVNode("", true),
289
+ unref(auth).esAdmin ? (openBlock(), createBlock(unref(RouterLink), {
290
+ key: 1,
291
+ to: { name: unref(RUTAS_PORTAL).auditoria },
292
+ class: "whitespace-nowrap rounded-t-lg px-4 py-2 text-sm text-brand-100 hover:text-white",
293
+ "active-class": "!bg-surface !text-slate-900 font-semibold"
294
+ }, {
295
+ default: withCtx(() => [..._cache[5] || (_cache[5] = [createTextVNode(" Auditoría ", -1)])]),
296
+ _: 1
297
+ }, 8, ["to"])) : createCommentVNode("", true)
298
+ ])]), createElementVNode("main", _hoisted_13, [createVNode(unref(RouterView))])]);
299
+ };
300
+ }
301
+ });
302
+ //#endregion
303
+ export { AppLayout_default as default };