@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.
- package/README.md +95 -2
- package/dist-lib/chunks/AppLayout-COCPfLWo.js +303 -0
- package/dist-lib/chunks/AuditoriaView-Bcf_kOqI.js +643 -0
- package/dist-lib/chunks/CarnetView-BsdUtYeu.js +450 -0
- package/dist-lib/chunks/CarteleraView-CRnVjt-S.js +996 -0
- package/dist-lib/chunks/CertificadosView-DvHDflcN.js +549 -0
- package/dist-lib/chunks/ChatBandejaView-HqhdSw7G.js +274 -0
- package/dist-lib/chunks/ChatConfiguracionView-BdH5gDdR.js +716 -0
- package/dist-lib/chunks/ChatConocimientoView-nKTAKxFa.js +263 -0
- package/dist-lib/chunks/ChatConversacionView-Do74V09l.js +477 -0
- package/dist-lib/chunks/ChatInformeView-BIiAH9L_.js +280 -0
- package/dist-lib/chunks/ChatRespuestasView-QFXbyg8U.js +183 -0
- package/dist-lib/chunks/EmpleadosView-BtqrO5-8.js +311 -0
- package/dist-lib/chunks/HomologacionView-UCZk4u6R.js +949 -0
- package/dist-lib/chunks/IngresoSsoView-BKkcnrYO.js +98 -0
- package/dist-lib/chunks/SolicitudesView-CYPD2A1s.js +900 -0
- package/dist-lib/chunks/TiposSolicitudView-CrWTVizl.js +168 -0
- package/dist-lib/chunks/ToggleSwitch-BYrIyheq.js +55 -0
- package/dist-lib/chunks/_plugin-vue_export-helper-BOaGB7Aw.js +8 -0
- package/dist-lib/chunks/api-M7D1c3IT.js +97 -0
- package/dist-lib/chunks/apiError-BHGbwz97.js +15 -0
- package/dist-lib/chunks/auth.store-BrGKZg-7.js +220 -0
- package/dist-lib/chunks/chat.service-C7ZQf4w3.js +241 -0
- package/dist-lib/chunks/cliente-interno.store-De39VsEE.js +203 -0
- package/dist-lib/chunks/homologacion-CoeHD7cB.js +131 -0
- package/dist-lib/chunks/portal-empleado-6KQrnsQJ.js +120 -0
- package/dist-lib/chunks/tipos-D93bLfFC.js +22 -0
- package/dist-lib/portal-admin.js +196 -0
- package/dist-lib/style.css +4813 -0
- package/dist-lib/types/App.vue.d.ts +3 -0
- package/dist-lib/types/components/AppLayout.vue.d.ts +3 -0
- package/dist-lib/types/components/CarnetClientesInternos.vue.d.ts +11 -0
- package/dist-lib/types/components/InvitacionPortalModal.vue.d.ts +20 -0
- package/dist-lib/types/components/MenuUsuario.vue.d.ts +3 -0
- package/dist-lib/types/components/VistoBuenoPortalModal.vue.d.ts +23 -0
- package/dist-lib/types/components/auditoria/AuditoriaFila.vue.d.ts +16 -0
- package/dist-lib/types/components/auditoria/AuditoriaFiltros.vue.d.ts +30 -0
- package/dist-lib/types/components/auditoria/AuditoriaPaginador.vue.d.ts +14 -0
- package/dist-lib/types/components/cartelera/PaginaCarteleraImagen.vue.d.ts +14 -0
- package/dist-lib/types/components/cartelera/PublicacionFormModal.vue.d.ts +27 -0
- package/dist-lib/types/components/cartelera/VistaCelularCartelera.vue.d.ts +34 -0
- package/dist-lib/types/components/certificados/AtenderCertificadoModal.vue.d.ts +22 -0
- package/dist-lib/types/components/certificados/RechazarCertificadoModal.vue.d.ts +18 -0
- package/dist-lib/types/components/certificados/ResumenSolicitudCertificado.vue.d.ts +8 -0
- package/dist-lib/types/components/chat/ChatClientesInternos.vue.d.ts +21 -0
- package/dist-lib/types/components/chat/ChatHorarioCampos.vue.d.ts +21 -0
- package/dist-lib/types/components/forms/ExportExcelButton.vue.d.ts +30 -0
- package/dist-lib/types/components/forms/ToggleSwitch.vue.d.ts +17 -0
- package/dist-lib/types/components/homologacion/HomologacionFila.vue.d.ts +30 -0
- package/dist-lib/types/components/homologacion/TipoPermisoFormModal.vue.d.ts +26 -0
- package/dist-lib/types/components/homologacion/TipoSolicitudFormModal.vue.d.ts +27 -0
- package/dist-lib/types/lib/auditoria.d.ts +25 -0
- package/dist-lib/types/lib/cartelera-paginas.d.ts +26 -0
- package/dist-lib/types/lib/cartelera-vigencia.d.ts +3 -0
- package/dist-lib/types/lib/export-excel-csv.d.ts +4 -0
- package/dist-lib/types/lib/export-excel.d.ts +9 -0
- package/dist-lib/types/lib/exportar-listado.d.ts +68 -0
- package/dist-lib/types/lib/homologacion.d.ts +41 -0
- package/dist-lib/types/lib/portal-empleado.d.ts +32 -0
- package/dist-lib/types/modulo.d.ts +31 -0
- package/dist-lib/types/portal-admin/config.d.ts +17 -0
- package/dist-lib/types/portal-admin/rutas.d.ts +17 -0
- package/dist-lib/types/portal-admin/tipos.d.ts +68 -0
- package/dist-lib/types/router/index.d.ts +7 -0
- package/dist-lib/types/services/api.d.ts +29 -0
- package/dist-lib/types/services/auditoria.service.d.ts +63 -0
- package/dist-lib/types/services/auth.service.d.ts +61 -0
- package/dist-lib/types/services/carnet.service.d.ts +56 -0
- package/dist-lib/types/services/cartelera.service.d.ts +69 -0
- package/dist-lib/types/services/chat.service.d.ts +279 -0
- package/dist-lib/types/services/configuracion-cliente-interno.service.d.ts +23 -0
- package/dist-lib/types/services/homologacion.service.d.ts +105 -0
- package/dist-lib/types/services/portal-empleado.service.d.ts +112 -0
- package/dist-lib/types/services/solicitudes-certificado.service.d.ts +59 -0
- package/dist-lib/types/stores/auth.store.d.ts +125 -0
- package/dist-lib/types/stores/cliente-interno.store.d.ts +65 -0
- package/dist-lib/types/utils/apiError.d.ts +4 -0
- package/dist-lib/types/views/AuditoriaView.vue.d.ts +3 -0
- package/dist-lib/types/views/CarnetView.vue.d.ts +3 -0
- package/dist-lib/types/views/CarteleraView.vue.d.ts +3 -0
- package/dist-lib/types/views/CertificadosView.vue.d.ts +3 -0
- package/dist-lib/types/views/EmpleadosView.vue.d.ts +3 -0
- package/dist-lib/types/views/HomologacionView.vue.d.ts +4 -0
- package/dist-lib/types/views/IngresoSsoView.vue.d.ts +3 -0
- package/dist-lib/types/views/LoginView.vue.d.ts +3 -0
- package/dist-lib/types/views/OlvideView.vue.d.ts +3 -0
- package/dist-lib/types/views/RestablecerView.vue.d.ts +3 -0
- package/dist-lib/types/views/SolicitudesView.vue.d.ts +3 -0
- package/dist-lib/types/views/TiposSolicitudView.vue.d.ts +3 -0
- package/dist-lib/types/views/chat/ChatBandejaView.vue.d.ts +3 -0
- package/dist-lib/types/views/chat/ChatConfiguracionView.vue.d.ts +3 -0
- package/dist-lib/types/views/chat/ChatConocimientoView.vue.d.ts +3 -0
- package/dist-lib/types/views/chat/ChatConversacionView.vue.d.ts +11 -0
- package/dist-lib/types/views/chat/ChatInformeView.vue.d.ts +3 -0
- package/dist-lib/types/views/chat/ChatRespuestasView.vue.d.ts +3 -0
- package/package.json +66 -4
package/README.md
CHANGED
|
@@ -1,3 +1,96 @@
|
|
|
1
|
-
#
|
|
1
|
+
# softdin-portal-central — Administración del portal
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|