shared-lib-angular 1.0.39 → 2.0.1

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, inject, Input, Component, HostListener } from '@angular/core';
2
+ import { signal, computed, Injectable, inject, input, Component, HostListener } from '@angular/core';
3
3
  import * as i1 from 'angular-oauth2-oidc';
4
4
  import { OAuthEvent } from 'angular-oauth2-oidc';
5
5
  import { BehaviorSubject, firstValueFrom, of, Observable } from 'rxjs';
@@ -20,14 +20,14 @@ import { CommonModule } from '@angular/common';
20
20
  * Versión actual de la librería @dinafi/frmk
21
21
  * Sincronizada con package.json
22
22
  */
23
- const VERSION = '1.0.39';
23
+ const VERSION = '2.0.1';
24
24
  /**
25
25
  * Información completa de la versión
26
26
  */
27
27
  const VERSION_INFO = {
28
- version: '1.0.39',
28
+ version: '2.0.1',
29
29
  name: 'shared-lib-angular',
30
- buildDate: '2026-02-05T18:53:10.247Z',
30
+ buildDate: '2026-02-05T20:39:22.686Z',
31
31
  angular: '^20.0.0'
32
32
  };
33
33
 
@@ -73,398 +73,286 @@ const DEFAULT_LIBRARY_CONFIG = {
73
73
  requireHttps: true,
74
74
  showDebugInformation: false
75
75
  };
76
+
76
77
  /**
77
- * Token de inyección para la configuración de la librería
78
+ * Store centralizado de configuración del framework.
78
79
  *
79
- * @example
80
- * // En el app.config.ts o app.module.ts de la aplicación consumidora:
81
- * providers: [
82
- * {
83
- * provide: FRMK_LIBRARY_CONFIG,
84
- * useValue: {
85
- * configServerHost: 'config-server.miempresa.com',
86
- * frontendId: 'mi-aplicacion-frontend',
87
- * production: environment.production
88
- * }
89
- * }
90
- * ]
91
- */
92
- const FRMK_LIBRARY_CONFIG = new InjectionToken('FrmkLibraryConfig');
93
- /**
94
- * Función helper para crear la configuración de la librería
80
+ * Reemplaza las variables globales mutables (`currentAppConfig`, `libraryConfig`)
81
+ * con un servicio inyectable basado en signals de Angular.
95
82
  *
96
- * @example
97
- * // En el app.config.ts:
98
- * import { provideFrmkConfig } from '@mh-dinafi-frmk/shared-lib-angular';
83
+ * Ciclo de vida:
84
+ * 1. `initialize(config)` llamado en APP_INITIALIZER con FrmkLibraryConfig
85
+ * 2. `applyServerConfig(data)` llamado por ConfigService tras obtener datos del Config Server
86
+ *
87
+ * Todas las propiedades computadas lanzan error si se leen antes de `initialize()`.
99
88
  *
100
- * export const appConfig: ApplicationConfig = {
101
- * providers: [
102
- * provideFrmkConfig({
103
- * configServerHost: environment.configServerHost,
104
- * frontendId: 'mi-aplicacion'
105
- * })
106
- * ]
107
- * };
89
+ * @example
90
+ * // En APP_INITIALIZER:
91
+ * const store = inject(FrmkConfigStore);
92
+ * store.initialize(myLibraryConfig);
93
+ * await configService.loadConfig(); // internamente llama store.applyServerConfig()
94
+ * authService.initializeAuth(store.authConfig());
108
95
  */
109
- function provideFrmkConfig(config) {
110
- return {
111
- provide: FRMK_LIBRARY_CONFIG,
112
- useValue: {
96
+ class FrmkConfigStore {
97
+ // ── Signals internos ──────────────────────────────────────────────
98
+ _libraryConfig = signal(null, ...(ngDevMode ? [{ debugName: "_libraryConfig" }] : []));
99
+ _serverConfig = signal(null, ...(ngDevMode ? [{ debugName: "_serverConfig" }] : []));
100
+ // ── Estado público (read-only) ────────────────────────────────────
101
+ /** Indica si la librería ha sido inicializada con `initialize()`. */
102
+ isConfigured = computed(() => this._libraryConfig() !== null, ...(ngDevMode ? [{ debugName: "isConfigured" }] : []));
103
+ /** Configuración de la librería (merged con defaults). Lanza si no inicializada. */
104
+ libraryConfig = computed(() => {
105
+ const cfg = this._libraryConfig();
106
+ if (!cfg) {
107
+ throw new Error('[FRMK] La librería no ha sido configurada. ' +
108
+ 'Llame a FrmkConfigStore.initialize() en su APP_INITIALIZER.');
109
+ }
110
+ return cfg;
111
+ }, ...(ngDevMode ? [{ debugName: "libraryConfig" }] : []));
112
+ /** Configuración del componente Login (defaults + libraryConfig.loginConfig). */
113
+ loginConfig = computed(() => {
114
+ const lib = this.libraryConfig();
115
+ return { ...DEFAULT_LOGIN_CONFIG, ...(lib.loginConfig || {}) };
116
+ }, ...(ngDevMode ? [{ debugName: "loginConfig" }] : []));
117
+ /** Configuración del componente Dashboard (defaults + libraryConfig.dashboardConfig). */
118
+ dashboardConfig = computed(() => {
119
+ const lib = this.libraryConfig();
120
+ return { ...DEFAULT_DASHBOARD_CONFIG, ...(lib.dashboardConfig || {}) };
121
+ }, ...(ngDevMode ? [{ debugName: "dashboardConfig" }] : []));
122
+ /**
123
+ * Configuración completa de la aplicación.
124
+ * Si hay datos del Config Server se aplican; en caso contrario se usan solo los defaults + libraryConfig.
125
+ */
126
+ appConfig = computed(() => {
127
+ const lib = this.libraryConfig(); // lanza si no inicializada
128
+ let config = this.createDefaultAppConfig(lib);
129
+ const serverData = this._serverConfig();
130
+ if (serverData) {
131
+ const mapped = this.mapConfigServerResponse(serverData, config);
132
+ config = this.mergeConfigurations(config, mapped);
133
+ }
134
+ return this.buildDynamicUrls(config);
135
+ }, ...(ngDevMode ? [{ debugName: "appConfig" }] : []));
136
+ /** AuthConfig de angular-oauth2-oidc derivada de appConfig. */
137
+ authConfig = computed(() => {
138
+ const app = this.appConfig();
139
+ return {
140
+ issuer: app.auth.issuer,
141
+ loginUrl: app.auth.loginUrl,
142
+ tokenEndpoint: app.auth.tokenEndpoint,
143
+ userinfoEndpoint: app.auth.userinfoEndpoint,
144
+ logoutUrl: app.auth.logoutUrl,
145
+ redirectUri: app.auth.redirectUri,
146
+ postLogoutRedirectUri: app.auth.postLogoutRedirectUri,
147
+ clientId: app.auth.clientId,
148
+ responseType: 'code',
149
+ scope: app.auth.scope,
150
+ disableAtHashCheck: true,
151
+ requireHttps: app.environment.requireHttps,
152
+ showDebugInformation: app.environment.showDebugInformation,
153
+ strictDiscoveryDocumentValidation: false,
154
+ skipIssuerCheck: true,
155
+ requestAccessToken: true,
156
+ customQueryParams: {},
157
+ silentRefreshTimeout: 5000,
158
+ timeoutFactor: 0.25,
159
+ sessionChecksEnabled: false,
160
+ clearHashAfterLogin: true,
161
+ oidc: true,
162
+ clockSkewInSec: 30,
163
+ disableIdTokenTimer: true
164
+ };
165
+ }, ...(ngDevMode ? [{ debugName: "authConfig" }] : []));
166
+ // ── Métodos públicos ──────────────────────────────────────────────
167
+ /**
168
+ * Inicializa la librería con la configuración proporcionada por la app consumidora.
169
+ * Debe llamarse UNA vez dentro del APP_INITIALIZER, antes de cualquier otro uso.
170
+ *
171
+ * @param config Configuración de la librería (se fusiona con DEFAULT_LIBRARY_CONFIG)
172
+ */
173
+ initialize(config) {
174
+ this._libraryConfig.set({
113
175
  ...DEFAULT_LIBRARY_CONFIG,
114
176
  ...config
115
- }
116
- };
117
- }
118
-
119
- // Variable global para almacenar la configuración actual
120
- let currentAppConfig = null;
121
- // Variable global para almacenar la configuración de la librería
122
- let libraryConfig = null;
123
- /**
124
- * Inicializa la configuración de la librería con los valores proporcionados por la aplicación
125
- * Esta función debe ser llamada antes de usar cualquier servicio de la librería
126
- *
127
- * @param config Configuración proporcionada por la aplicación consumidora
128
- */
129
- function initializeLibraryConfig(config) {
130
- libraryConfig = {
131
- ...DEFAULT_LIBRARY_CONFIG,
132
- ...config
133
- };
134
- // Resetear la configuración actual para que se reconstruya con los nuevos valores
135
- currentAppConfig = null;
136
- }
137
- /**
138
- * Obtiene la configuración de la librería
139
- * Lanza error si no ha sido inicializada
140
- */
141
- function getLibraryConfig() {
142
- if (!libraryConfig) {
143
- throw new Error('[FRMK] La librería no ha sido configurada. ' +
144
- 'Debe proporcionar FRMK_LIBRARY_CONFIG en los providers de su aplicación. ' +
145
- 'Ejemplo: provideFrmkConfig({ configServerHost: "...", frontendId: "..." })');
177
+ });
178
+ // Resetear server config para que appConfig se recalcule con los nuevos defaults
179
+ this._serverConfig.set(null);
146
180
  }
147
- return libraryConfig;
148
- }
149
- /**
150
- * Verifica si la librería ha sido configurada
151
- */
152
- function isLibraryConfigured() {
153
- return libraryConfig !== null;
154
- }
155
- /**
156
- * Obtiene la configuración del componente Login
157
- * Combina los valores por defecto con los configurados en la librería
158
- */
159
- function getLoginConfig() {
160
- const libConfig = isLibraryConfigured() ? getLibraryConfig() : null;
161
- return {
162
- ...DEFAULT_LOGIN_CONFIG,
163
- ...(libConfig?.loginConfig || {})
164
- };
165
- }
166
- /**
167
- * Obtiene la configuración del componente Dashboard
168
- * Combina los valores por defecto con los configurados en la librería
169
- */
170
- function getDashboardConfig() {
171
- const libConfig = isLibraryConfigured() ? getLibraryConfig() : null;
172
- return {
173
- ...DEFAULT_DASHBOARD_CONFIG,
174
- ...(libConfig?.dashboardConfig || {})
175
- };
176
- }
177
- // Función para crear la configuración por defecto usando los valores de la librería
178
- function createDefaultAppConfig() {
179
- const libConfig = getLibraryConfig();
180
- return {
181
- hosts: {
182
- configServer: libConfig.configServerHost,
183
- authServer: '', // Leído desde Config Server
184
- keycloak: '', // Leído desde Config Server
185
- localApp: '' // Leído desde Config Server
186
- },
187
- paths: {
188
- configService: libConfig.configServicePath || '/api/v1/config/service',
189
- realm: libConfig.realm || 'realm-to-create'
190
- },
191
- configServer: {
192
- url: '', // Se construirá dinámicamente
193
- frontend: libConfig.frontendId
194
- },
195
- auth: {
196
- issuer: '', // Se construirá dinámicamente
197
- loginUrl: '', // Se construirá dinámicamente
198
- tokenEndpoint: '', // Se construirá dinámicamente
199
- userinfoEndpoint: '', // Se construirá dinámicamente
200
- logoutUrl: '', // Se construirá dinámicamente
201
- clientId: '',
202
- redirectUri: '', // Se construirá dinámicamente
203
- postLogoutRedirectUri: '', // Se construirá dinámicamente
204
- silentRefreshRedirectUri: '', // Se construirá dinámicamente
205
- scope: 'openid email profile'
206
- },
207
- authorization: {
208
- baseUrl: '',
209
- verifyEndpoint: '/api/v1/authz/verify-groups',
210
- hierarchyEndpoint: '/api/v1/authz/hierarchy-by-component-and-groups',
211
- },
212
- environment: {
213
- production: libConfig.production ?? false,
214
- requireHttps: libConfig.requireHttps ?? true,
215
- showDebugInformation: libConfig.showDebugInformation ?? false
216
- }
217
- };
218
- }
219
- // Getter para obtener la configuración por defecto (lazy initialization)
220
- function getDefaultAppConfig() {
221
- return createDefaultAppConfig();
222
- }
223
- // Función para mapear la configuración del config server a la configuración de la aplicación
224
- function mapConfigServerResponse(configServerData) {
225
- const mappedConfig = {};
226
- const defaultConfig = getDefaultAppConfig();
227
- // Mapear Auth URL
228
- configureAuthServer();
229
- // Mapear client ID
230
- initializeClientId();
231
- // Mapear realm
232
- initializeRealmMapping();
233
- // Mapear URL de autorización (authServer)
234
- initializeAuthorizationConfig();
235
- // Mapear scope de autorización
236
- initializeAuthScope();
237
- // Mapear keycloak URL
238
- initializeKeycloakHost();
239
- // Mapear callback URL (local app host)
240
- configureLocalAppCallback();
241
- // Mapear callback URL (local app host)
242
- initializeHttpsSettings();
243
- return mappedConfig;
244
- function initializeHttpsSettings() {
245
- if (configServerData["security.url.https"]) {
246
- mappedConfig.environment = {
247
- ...defaultConfig.environment,
248
- ...(mappedConfig.environment || {}),
249
- requireHttps: configServerData["security.url.https"]
250
- };
251
- }
181
+ /**
182
+ * Aplica los datos devueltos por el Config Server.
183
+ * Llamado por `ConfigService.loadConfig()` después de la petición HTTP.
184
+ *
185
+ * @param data Respuesta del Config Server (o undefined si falló la carga)
186
+ */
187
+ applyServerConfig(data) {
188
+ this._serverConfig.set(data ?? null);
189
+ }
190
+ // ── Lógica privada (migrada de app.config.ts) ─────────────────────
191
+ createDefaultAppConfig(lib) {
192
+ return {
193
+ hosts: {
194
+ configServer: lib.configServerHost,
195
+ authServer: '',
196
+ keycloak: '',
197
+ localApp: ''
198
+ },
199
+ paths: {
200
+ configService: lib.configServicePath || '/api/v1/config/service',
201
+ realm: lib.realm || 'realm-to-create'
202
+ },
203
+ configServer: {
204
+ url: '',
205
+ frontend: lib.frontendId
206
+ },
207
+ auth: {
208
+ issuer: '',
209
+ loginUrl: '',
210
+ tokenEndpoint: '',
211
+ userinfoEndpoint: '',
212
+ logoutUrl: '',
213
+ clientId: '',
214
+ redirectUri: '',
215
+ postLogoutRedirectUri: '',
216
+ silentRefreshRedirectUri: '',
217
+ scope: 'openid email profile'
218
+ },
219
+ authorization: {
220
+ baseUrl: '',
221
+ verifyEndpoint: '/api/v1/authz/verify-groups',
222
+ hierarchyEndpoint: '/api/v1/authz/hierarchy-by-component-and-groups'
223
+ },
224
+ environment: {
225
+ production: lib.production ?? false,
226
+ requireHttps: lib.requireHttps ?? true,
227
+ showDebugInformation: lib.showDebugInformation ?? false
228
+ }
229
+ };
252
230
  }
253
- function configureLocalAppCallback() {
254
- if (configServerData["security.url.callback"]) {
255
- mappedConfig.hosts = {
231
+ mapConfigServerResponse(data, defaultConfig) {
232
+ const mapped = {};
233
+ if (data['security.url.auth']) {
234
+ mapped.hosts = {
256
235
  ...defaultConfig.hosts,
257
- ...(mappedConfig.hosts || {}),
258
- localApp: configServerData["security.url.callback"]
236
+ ...(mapped.hosts || {}),
237
+ authServer: data['security.url.auth']
259
238
  };
260
239
  }
261
- }
262
- function initializeKeycloakHost() {
263
- if (configServerData["security.url.keycloak"]) {
264
- mappedConfig.hosts = {
265
- ...defaultConfig.hosts,
266
- ...(mappedConfig.hosts || {}),
267
- keycloak: configServerData["security.url.keycloak"]
240
+ if (data['quarkus.oidc.client-id']) {
241
+ mapped.auth = {
242
+ ...defaultConfig.auth,
243
+ ...(mapped.auth || {}),
244
+ clientId: data['quarkus.oidc.client-id']
268
245
  };
269
246
  }
270
- }
271
- function initializeAuthScope() {
272
- if (configServerData["security.scope"]) {
273
- mappedConfig.auth = {
274
- ...defaultConfig.auth,
275
- ...(mappedConfig.auth || {}),
276
- scope: configServerData["security.scope"]
247
+ if (data['security.realm']) {
248
+ mapped.paths = {
249
+ ...defaultConfig.paths,
250
+ ...(mapped.paths || {}),
251
+ realm: data['security.realm']
277
252
  };
278
253
  }
279
- }
280
- function initializeAuthorizationConfig() {
281
- if (configServerData["security.url.authz"]) {
282
- mappedConfig.authorization = {
254
+ if (data['security.url.authz']) {
255
+ mapped.authorization = {
283
256
  ...defaultConfig.authorization,
284
- ...(mappedConfig.authorization || {}),
285
- baseUrl: configServerData["security.url.authz"]
257
+ ...(mapped.authorization || {}),
258
+ baseUrl: data['security.url.authz']
286
259
  };
287
260
  }
288
- }
289
- function initializeRealmMapping() {
290
- if (configServerData["security.realm"]) {
291
- mappedConfig.paths = {
292
- ...defaultConfig.paths,
293
- ...(mappedConfig.paths || {}),
294
- realm: configServerData["security.realm"]
261
+ if (data['security.scope']) {
262
+ mapped.auth = {
263
+ ...defaultConfig.auth,
264
+ ...(mapped.auth || {}),
265
+ scope: data['security.scope']
295
266
  };
296
267
  }
297
- }
298
- function initializeClientId() {
299
- if (configServerData["quarkus.oidc.client-id"]) {
300
- mappedConfig.auth = {
301
- ...defaultConfig.auth,
302
- ...(mappedConfig.auth || {}),
303
- clientId: configServerData["quarkus.oidc.client-id"]
268
+ if (data['security.url.keycloak']) {
269
+ mapped.hosts = {
270
+ ...defaultConfig.hosts,
271
+ ...(mapped.hosts || {}),
272
+ keycloak: data['security.url.keycloak']
304
273
  };
305
274
  }
306
- }
307
- function configureAuthServer() {
308
- if (configServerData["security.url.auth"]) {
309
- mappedConfig.hosts = {
275
+ if (data['security.url.callback']) {
276
+ mapped.hosts = {
310
277
  ...defaultConfig.hosts,
311
- ...(mappedConfig.hosts || {}),
312
- authServer: configServerData["security.url.auth"]
278
+ ...(mapped.hosts || {}),
279
+ localApp: data['security.url.callback']
313
280
  };
314
281
  }
315
- }
316
- }
317
- /**
318
- * Función para establecer la configuración actual con datos del config server
319
- * Esta función debe ser llamada desde el ConfigService después de cargar la configuración
320
- */
321
- function setCurrentAppConfig(configServerData) {
322
- currentAppConfig = getAppConfigWithServerData(configServerData);
323
- }
324
- /**
325
- * Función para obtener la configuración actual
326
- * Si no se ha establecido una configuración, devuelve la configuración por defecto
327
- */
328
- function getCurrentAppConfig() {
329
- return currentAppConfig || getAppConfig();
330
- }
331
- // Función para obtener la configuración con posibles sobrescrituras de variables de entorno
332
- function getAppConfig() {
333
- const config = { ...getDefaultAppConfig() };
334
- // Construir URLs dinámicamente
335
- const localProtocol = config.environment.requireHttps ? 'https' : 'http';
336
- // Config Server URL
337
- config.configServer.url = `${localProtocol}://${config.hosts.configServer}${config.paths.configService}`;
338
- // Auth URLs
339
- config.auth.issuer = `${localProtocol}://${config.hosts.keycloak}/realms/${config.paths.realm}`;
340
- config.auth.loginUrl = `${localProtocol}://${config.hosts.keycloak}/realms/${config.paths.realm}/protocol/openid-connect/auth`;
341
- config.auth.tokenEndpoint = `${localProtocol}://${config.hosts.authServer}/oidc/realm/${config.paths.realm}/protocol/openid-connect/token`;
342
- config.auth.userinfoEndpoint = `${localProtocol}://${config.hosts.authServer}/oidc/realm/${config.paths.realm}/protocol/openid-connect/userinfo`;
343
- config.auth.logoutUrl = `${localProtocol}://${config.hosts.authServer}/oidc/realm/${config.paths.realm}/protocol/openid-connect/logout`;
344
- // Redirect URLs (locales)
345
- const currentOrigin = globalThis.window?.location?.origin ?? config.hosts.localApp;
346
- config.auth.redirectUri = `${currentOrigin}/login`;
347
- config.auth.postLogoutRedirectUri = `${currentOrigin}/login`;
348
- // Authorization URL
349
- config.authorization.baseUrl = `${localProtocol}://${config.authorization.baseUrl}`;
350
- return config;
351
- }
352
- // Función para obtener la configuración aplicando primero la del config server y luego variables de entorno
353
- function getAppConfigWithServerData(configServerData) {
354
- let config = { ...getDefaultAppConfig() };
355
- // Aplicar configuración del config server si está disponible
356
- if (configServerData) {
357
- const serverMappedConfig = mapConfigServerResponse(configServerData);
358
- config = mergeConfigurations(config, serverMappedConfig);
359
- }
360
- // Construir URLs dinámicamente
361
- config = buildDynamicUrls(config);
362
- return config;
363
- }
364
- // Función auxiliar para fusionar configuraciones
365
- function mergeConfigurations(base, override) {
366
- const merged = { ...base };
367
- if (override.hosts) {
368
- merged.hosts = { ...merged.hosts, ...override.hosts };
369
- }
370
- if (override.paths) {
371
- merged.paths = { ...merged.paths, ...override.paths };
372
- }
373
- if (override.configServer) {
374
- merged.configServer = { ...merged.configServer, ...override.configServer };
375
- }
376
- if (override.auth) {
377
- merged.auth = { ...merged.auth, ...override.auth };
378
- }
379
- if (override.authorization) {
380
- merged.authorization = { ...merged.authorization, ...override.authorization };
381
- }
382
- if (override.environment) {
383
- merged.environment = { ...merged.environment, ...override.environment };
384
- }
385
- return merged;
386
- }
387
- // Función para construir URLs dinámicamente
388
- function buildDynamicUrls(config) {
389
- const localProtocol = config.environment.requireHttps ? 'https' : 'http';
390
- // Config Server URL
391
- config.configServer.url = `${localProtocol}://${config.hosts.configServer}${config.paths.configService}`;
392
- // Auth URLs
393
- config.auth.issuer = `${localProtocol}://${config.hosts.keycloak}/realms/${config.paths.realm}`;
394
- config.auth.loginUrl = `${localProtocol}://${config.hosts.keycloak}/realms/${config.paths.realm}/protocol/openid-connect/auth`;
395
- config.auth.tokenEndpoint = `${localProtocol}://${config.hosts.authServer}/oidc/realm/${config.paths.realm}/protocol/openid-connect/token`;
396
- config.auth.userinfoEndpoint = `${localProtocol}://${config.hosts.authServer}/oidc/realm/${config.paths.realm}/protocol/openid-connect/userinfo`;
397
- config.auth.logoutUrl = `${localProtocol}://${config.hosts.authServer}/oidc/realm/${config.paths.realm}/protocol/openid-connect/logout`;
398
- // Redirect URLs (locales)
399
- const currentOrigin = globalThis.window?.location?.origin ?? config.hosts.localApp;
400
- config.auth.redirectUri = `${currentOrigin}/login`;
401
- config.auth.postLogoutRedirectUri = `${currentOrigin}/login`;
402
- // Authorization URL
403
- config.authorization.baseUrl = `${localProtocol}://${config.authorization.baseUrl}`;
404
- return config;
405
- }
406
-
407
- /**
408
- * Función para obtener la configuración de autenticación actual
409
- * Esta función se evalúa cada vez que se llama, por lo que siempre tendrá la configuración más reciente
410
- */
411
- function getAuthConfig() {
412
- const appConfig = getCurrentAppConfig();
413
- return {
414
- // URL base del servidor de identidad
415
- issuer: appConfig.auth.issuer,
416
- // URLs específicas para evitar discovery document
417
- loginUrl: appConfig.auth.loginUrl,
418
- tokenEndpoint: appConfig.auth.tokenEndpoint,
419
- userinfoEndpoint: appConfig.auth.userinfoEndpoint,
420
- logoutUrl: appConfig.auth.logoutUrl,
421
- // IMPORTANTE: URLs de redirección completas
422
- redirectUri: appConfig.auth.redirectUri,
423
- postLogoutRedirectUri: appConfig.auth.postLogoutRedirectUri,
424
- // Configuración del cliente
425
- clientId: appConfig.auth.clientId,
426
- // Configuración OAuth
427
- responseType: 'code',
428
- scope: appConfig.auth.scope,
429
- // Configuraciones para desarrollo
430
- disableAtHashCheck: true,
431
- requireHttps: appConfig.environment.requireHttps,
432
- showDebugInformation: appConfig.environment.showDebugInformation,
433
- strictDiscoveryDocumentValidation: false,
434
- skipIssuerCheck: true,
435
- // IMPORTANTE: Configuraciones para manejar state y session_state
436
- requestAccessToken: true,
437
- customQueryParams: {},
438
- // Timeouts
439
- silentRefreshTimeout: 5000,
440
- timeoutFactor: 0.25,
441
- // Session checks
442
- sessionChecksEnabled: false,
443
- clearHashAfterLogin: true,
444
- // Configuración OIDC
445
- oidc: true,
446
- // Tolerancia de tiempo para tokens (10 minutos)
447
- clockSkewInSec: 30,
448
- // Deshabilitar timer de id_token para evitar validaciones innecesarias
449
- disableIdTokenTimer: true
450
- };
451
- }
452
- // Mantener la exportación original para compatibilidad
453
- function getAuthConfigEnv() {
454
- return getAuthConfig();
282
+ if (data['security.url.https'] !== undefined) {
283
+ mapped.environment = {
284
+ ...defaultConfig.environment,
285
+ ...(mapped.environment || {}),
286
+ requireHttps: data['security.url.https']
287
+ };
288
+ }
289
+ return mapped;
290
+ }
291
+ mergeConfigurations(base, override) {
292
+ const merged = { ...base };
293
+ if (override.hosts) {
294
+ merged.hosts = { ...merged.hosts, ...override.hosts };
295
+ }
296
+ if (override.paths) {
297
+ merged.paths = { ...merged.paths, ...override.paths };
298
+ }
299
+ if (override.configServer) {
300
+ merged.configServer = { ...merged.configServer, ...override.configServer };
301
+ }
302
+ if (override.auth) {
303
+ merged.auth = { ...merged.auth, ...override.auth };
304
+ }
305
+ if (override.authorization) {
306
+ merged.authorization = { ...merged.authorization, ...override.authorization };
307
+ }
308
+ if (override.environment) {
309
+ merged.environment = { ...merged.environment, ...override.environment };
310
+ }
311
+ return merged;
312
+ }
313
+ buildDynamicUrls(config) {
314
+ const c = { ...config };
315
+ const proto = c.environment.requireHttps ? 'https' : 'http';
316
+ // Config Server URL
317
+ c.configServer = { ...c.configServer };
318
+ c.configServer.url = `${proto}://${c.hosts.configServer}${c.paths.configService}`;
319
+ // Auth URLs
320
+ c.auth = { ...c.auth };
321
+ c.auth.issuer = `${proto}://${c.hosts.keycloak}/realms/${c.paths.realm}`;
322
+ c.auth.loginUrl = `${proto}://${c.hosts.keycloak}/realms/${c.paths.realm}/protocol/openid-connect/auth`;
323
+ c.auth.tokenEndpoint = `${proto}://${c.hosts.authServer}/oidc/realm/${c.paths.realm}/protocol/openid-connect/token`;
324
+ c.auth.userinfoEndpoint = `${proto}://${c.hosts.authServer}/oidc/realm/${c.paths.realm}/protocol/openid-connect/userinfo`;
325
+ c.auth.logoutUrl = `${proto}://${c.hosts.authServer}/oidc/realm/${c.paths.realm}/protocol/openid-connect/logout`;
326
+ // Redirect URLs (locales)
327
+ const currentOrigin = globalThis.window?.location?.origin ?? c.hosts.localApp;
328
+ c.auth.redirectUri = `${currentOrigin}/login`;
329
+ c.auth.postLogoutRedirectUri = `${currentOrigin}/login`;
330
+ // Authorization URL
331
+ c.authorization = { ...c.authorization };
332
+ c.authorization.baseUrl = `${proto}://${c.authorization.baseUrl}`;
333
+ return c;
334
+ }
335
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FrmkConfigStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
336
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FrmkConfigStore, providedIn: 'root' });
455
337
  }
338
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FrmkConfigStore, decorators: [{
339
+ type: Injectable,
340
+ args: [{ providedIn: 'root' }]
341
+ }] });
456
342
 
457
343
  class AuthService {
458
344
  oauthService;
459
345
  router;
346
+ store;
460
347
  isAuthenticatedSubject$ = new BehaviorSubject(false);
461
348
  isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();
462
349
  isDoneLoadingSubject$ = new BehaviorSubject(false);
463
350
  isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();
464
351
  isInitialLogin = true;
465
- constructor(oauthService, router) {
352
+ constructor(oauthService, router, store) {
466
353
  this.oauthService = oauthService;
467
354
  this.router = router;
355
+ this.store = store;
468
356
  // IMPORTANTE: Limpiar tokens expirados ANTES de cualquier otra operación
469
357
  this.clearExpiredTokensOnStartup();
470
358
  this.oauthService.events
@@ -666,7 +554,7 @@ class AuthService {
666
554
  }
667
555
  }
668
556
  setupManualConfiguration() {
669
- const authConfig = getAuthConfigEnv();
557
+ const authConfig = this.store.authConfig();
670
558
  this.oauthService.tokenEndpoint = authConfig.tokenEndpoint;
671
559
  this.oauthService.userinfoEndpoint = authConfig.userinfoEndpoint;
672
560
  this.oauthService.loginUrl = authConfig.loginUrl;
@@ -723,7 +611,7 @@ class AuthService {
723
611
  }
724
612
  }
725
613
  getAuthConfig() {
726
- return getAuthConfigEnv();
614
+ return this.store.authConfig();
727
615
  }
728
616
  login(targetUrl) {
729
617
  this.isInitialLogin = true;
@@ -748,7 +636,7 @@ class AuthService {
748
636
  }
749
637
  }
750
638
  async callKeycloakLogoutEndpoint() {
751
- const authConfig = getAuthConfigEnv();
639
+ const authConfig = this.store.authConfig();
752
640
  const logoutUrl = authConfig.logoutUrl;
753
641
  let refreshToken = this.oauthService.getRefreshToken();
754
642
  if (!refreshToken) {
@@ -831,7 +719,7 @@ class AuthService {
831
719
  if (claims.realm_access?.roles) {
832
720
  roles.push(...claims.realm_access.roles);
833
721
  }
834
- const authConfig = getAuthConfigEnv();
722
+ const authConfig = this.store.authConfig();
835
723
  if (claims.resource_access?.[authConfig.clientId]?.roles) {
836
724
  roles.push(...claims.resource_access[authConfig.clientId].roles);
837
725
  }
@@ -924,7 +812,7 @@ class AuthService {
924
812
  return {};
925
813
  }
926
814
  }
927
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AuthService, deps: [{ token: i1.OAuthService }, { token: i4.Router }], target: i0.ɵɵFactoryTarget.Injectable });
815
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AuthService, deps: [{ token: i1.OAuthService }, { token: i4.Router }, { token: FrmkConfigStore }], target: i0.ɵɵFactoryTarget.Injectable });
928
816
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AuthService, providedIn: 'root' });
929
817
  }
930
818
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AuthService, decorators: [{
@@ -932,33 +820,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
932
820
  args: [{
933
821
  providedIn: 'root'
934
822
  }]
935
- }], ctorParameters: () => [{ type: i1.OAuthService }, { type: i4.Router }] });
823
+ }], ctorParameters: () => [{ type: i1.OAuthService }, { type: i4.Router }, { type: FrmkConfigStore }] });
936
824
 
937
825
  class ConfigService {
938
826
  http;
827
+ store;
939
828
  configServerData = null;
940
- finalAppConfig = null;
941
829
  isLoaded = false;
942
- initialAppConfig = getAppConfig();
943
- constructor(http) {
830
+ constructor(http, store) {
944
831
  this.http = http;
832
+ this.store = store;
945
833
  }
946
834
  /**
947
835
  * Carga la configuración desde el config-server
948
836
  */
949
837
  async loadConfig() {
950
838
  try {
951
- const url = `${this.initialAppConfig.configServer.url}/${this.initialAppConfig.configServer.frontend}`;
839
+ const appConfig = this.store.appConfig();
840
+ const url = `${appConfig.configServer.url}/${appConfig.configServer.frontend}`;
952
841
  this.configServerData = await firstValueFrom(this.http.get(url));
953
- this.finalAppConfig = getAppConfigWithServerData(this.configServerData);
954
- // IMPORTANTE: Establecer la configuración actual para que esté disponible globalmente
955
- setCurrentAppConfig(this.configServerData);
842
+ // Aplicar datos del server al store — todos los computeds se recalculan automáticamente
843
+ this.store.applyServerConfig(this.configServerData);
956
844
  this.isLoaded = true;
957
845
  }
958
846
  catch (error) {
959
847
  console.log(`Exception while loading config: ${error}`);
960
- this.finalAppConfig = this.initialAppConfig;
961
- setCurrentAppConfig();
848
+ this.store.applyServerConfig();
962
849
  this.isLoaded = true;
963
850
  }
964
851
  }
@@ -984,7 +871,7 @@ class ConfigService {
984
871
  * Obtiene la configuración final de la aplicación (con valores del config server aplicados)
985
872
  */
986
873
  getAppConfig() {
987
- return this.finalAppConfig || this.initialAppConfig;
874
+ return this.store.appConfig();
988
875
  }
989
876
  /**
990
877
  * Obtiene los datos raw del config server
@@ -992,7 +879,7 @@ class ConfigService {
992
879
  getConfigServerData() {
993
880
  return this.configServerData;
994
881
  }
995
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ConfigService, deps: [{ token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
882
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ConfigService, deps: [{ token: i1$1.HttpClient }, { token: FrmkConfigStore }], target: i0.ɵɵFactoryTarget.Injectable });
996
883
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ConfigService, providedIn: 'root' });
997
884
  }
998
885
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ConfigService, decorators: [{
@@ -1000,7 +887,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1000
887
  args: [{
1001
888
  providedIn: 'root'
1002
889
  }]
1003
- }], ctorParameters: () => [{ type: i1$1.HttpClient }] });
890
+ }], ctorParameters: () => [{ type: i1$1.HttpClient }, { type: FrmkConfigStore }] });
1004
891
 
1005
892
  class AuthorizationService {
1006
893
  http;
@@ -1175,40 +1062,28 @@ class LoginComponent {
1175
1062
  /**
1176
1063
  * Configuration object to customize the login component appearance and behavior
1177
1064
  */
1178
- config = {};
1065
+ config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
1066
+ store = inject(FrmkConfigStore);
1179
1067
  isLoggingIn = false;
1180
1068
  statusMessage = '';
1181
1069
  authSubscription;
1182
1070
  isProcessingCallback = false;
1183
- // Merged configuration with defaults - initialize with defaults immediately
1184
- _mergedConfig;
1071
+ /** Configuración merged: defaults libraryConfig.loginConfig @Input config */
1072
+ mergedConfig = computed(() => {
1073
+ const storeConfig = this.store.isConfigured()
1074
+ ? this.store.loginConfig()
1075
+ : DEFAULT_LOGIN_CONFIG;
1076
+ const inputCfg = this.config();
1077
+ return {
1078
+ ...storeConfig,
1079
+ ...inputCfg,
1080
+ version: inputCfg.version || storeConfig.version || VERSION
1081
+ };
1082
+ }, ...(ngDevMode ? [{ debugName: "mergedConfig" }] : []));
1185
1083
  constructor(authService) {
1186
1084
  this.authService = authService;
1187
- // Initialize with global config in constructor to avoid template errors
1188
- try {
1189
- this._mergedConfig = getLoginConfig();
1190
- }
1191
- catch {
1192
- this._mergedConfig = { ...DEFAULT_LOGIN_CONFIG };
1193
- }
1194
1085
  }
1195
1086
  ngOnInit() {
1196
- // Get global config from library, then merge with component input
1197
- try {
1198
- const globalConfig = getLoginConfig();
1199
- this._mergedConfig = {
1200
- ...globalConfig,
1201
- ...this.config,
1202
- version: this.config?.version || globalConfig.version || VERSION
1203
- };
1204
- }
1205
- catch {
1206
- this._mergedConfig = {
1207
- ...DEFAULT_LOGIN_CONFIG,
1208
- ...this.config,
1209
- version: this.config?.version || VERSION
1210
- };
1211
- }
1212
1087
  // Check if processing OAuth callback
1213
1088
  // Also check for 'iss' and 'session_state' which indicate we came from OAuth even if 'code' was already processed
1214
1089
  const urlParams = new URLSearchParams(globalThis.location?.search || '');
@@ -1241,21 +1116,22 @@ class LoginComponent {
1241
1116
  * Get merged configuration value
1242
1117
  */
1243
1118
  getConfig(key) {
1244
- return this._mergedConfig[key];
1119
+ return this.mergedConfig()[key];
1245
1120
  }
1246
1121
  /**
1247
1122
  * Get custom CSS styles for theming
1248
1123
  */
1249
1124
  getCustomStyles() {
1250
1125
  const styles = {};
1251
- if (this._mergedConfig.primaryColor) {
1252
- styles['--color-primary'] = this._mergedConfig.primaryColor;
1126
+ const cfg = this.mergedConfig();
1127
+ if (cfg.primaryColor) {
1128
+ styles['--color-primary'] = cfg.primaryColor;
1253
1129
  }
1254
- if (this._mergedConfig.backgroundColor) {
1255
- styles['--login-bg-color'] = this._mergedConfig.backgroundColor;
1130
+ if (cfg.backgroundColor) {
1131
+ styles['--login-bg-color'] = cfg.backgroundColor;
1256
1132
  }
1257
- if (this._mergedConfig.textColor) {
1258
- styles['--login-text-color'] = this._mergedConfig.textColor;
1133
+ if (cfg.textColor) {
1134
+ styles['--login-text-color'] = cfg.textColor;
1259
1135
  }
1260
1136
  return styles;
1261
1137
  }
@@ -1276,26 +1152,25 @@ class LoginComponent {
1276
1152
  this.navigateToRedirect();
1277
1153
  }
1278
1154
  navigateToRedirect() {
1279
- const redirectUrl = this._mergedConfig.redirectUrl;
1155
+ const redirectUrl = this.mergedConfig().redirectUrl;
1280
1156
  if (typeof globalThis.location !== 'undefined') {
1281
1157
  const baseUrl = `${globalThis.location.protocol}//${globalThis.location.host}`;
1282
1158
  globalThis.location.href = `${baseUrl}${redirectUrl}`;
1283
1159
  }
1284
1160
  }
1285
1161
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: LoginComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
1286
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: LoginComponent, isStandalone: true, selector: "lib-login", inputs: { config: "config" }, ngImport: i0, template: "<div class=\"login-page\" [ngStyle]=\"getCustomStyles()\">\r\n <div class=\"login-page__container\">\r\n <!-- Logo -->\r\n <div class=\"login-page__logo\">\r\n <img \r\n [src]=\"getConfig('logoUrl')\" \r\n [alt]=\"getConfig('logoAlt')\" \r\n class=\"login-page__logo-img\"\r\n [style.width]=\"getConfig('logoWidth')\"\r\n [style.height]=\"getConfig('logoHeight')\" />\r\n </div>\r\n\r\n <!-- Header -->\r\n <div class=\"login-page__header\">\r\n <h1>{{ getConfig('title') }}</h1>\r\n <p *ngIf=\"getConfig('subtitle')\" class=\"subtitle\">{{ getConfig('subtitle') }}</p>\r\n </div>\r\n \r\n <!-- Login Form -->\r\n <div *ngIf=\"(authService.isDoneLoading$ | async)\" class=\"login-page__form\">\r\n <div *ngIf=\"!(authService.isAuthenticated$ | async)\">\r\n <!-- Status Message -->\r\n <div *ngIf=\"statusMessage\" class=\"auth-alert auth-alert--info\">\r\n <div class=\"auth-alert__content\">\r\n <p>{{ statusMessage }}</p>\r\n </div>\r\n </div>\r\n\r\n <!-- Login Button -->\r\n <button \r\n class=\"btn btn--primary btn--lg\" \r\n (click)=\"login()\" \r\n [disabled]=\"isLoggingIn\"\r\n [class.loading]=\"isLoggingIn\">\r\n {{ isLoggingIn ? 'Redirigiendo...' : getConfig('loginButtonText') }}\r\n </button>\r\n\r\n <!-- Footer -->\r\n <div class=\"login-page__footer\" *ngIf=\"getConfig('showFooter')\">\r\n <p *ngIf=\"getConfig('footerText'); else defaultFooter\">\r\n {{ getConfig('footerText') }}\r\n </p>\r\n <ng-template #defaultFooter>\r\n <p>\r\n Acceso seguro para usuarios de \r\n <strong>{{ getConfig('institutionName') }}</strong>\r\n </p>\r\n </ng-template>\r\n </div>\r\n </div>\r\n \r\n <!-- Already Authenticated -->\r\n <div *ngIf=\"authService.isAuthenticated$ | async\" class=\"auth-alert auth-alert--success\">\r\n <div class=\"auth-alert__content\">\r\n <h4>\u00A1Sesi\u00F3n activa!</h4>\r\n <p>Ya tienes una sesi\u00F3n iniciada en el sistema.</p>\r\n </div>\r\n \r\n <div class=\"login-page__actions\">\r\n <button class=\"btn btn--primary btn--lg\" (click)=\"goToDashboard()\">\r\n {{ getConfig('dashboardButtonText') }}\r\n </button>\r\n \r\n <button \r\n *ngIf=\"getConfig('showLogoutButton')\"\r\n class=\"btn btn--secondary btn--lg\" \r\n (click)=\"logout()\">\r\n {{ getConfig('logoutButtonText') }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n \r\n <!-- Loading State -->\r\n <div *ngIf=\"!(authService.isDoneLoading$ | async)\" class=\"auth-loading\">\r\n <div class=\"auth-loading__spinner\"></div>\r\n <p class=\"auth-loading__text\">Procesando autenticaci\u00F3n...</p>\r\n </div>\r\n\r\n <!-- Version -->\r\n <div *ngIf=\"getConfig('showVersion')\" class=\"login-page__version\">\r\n <span>v{{ getConfig('version') }}</span>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100vh}.login-page{min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--color-primary, #313945);padding:var(--spacing-4, 1rem);position:relative}.login-page__container{width:100%;max-width:450px;background:var(--login-bg-color, var(--color-primary, #313945));color:var(--login-text-color, var(--color-white, #ffffff));border-radius:var(--border-radius-2xl, 1rem);box-shadow:0 25px 50px -12px #00000040;padding:var(--spacing-8, 2rem);position:relative;overflow:hidden;border:1px solid rgba(255,255,255,.2);animation:slideInUp .6s cubic-bezier(.4,0,.2,1)}.login-page__container:before{content:\"\";position:absolute;top:0;left:0;right:0;height:6px;background:linear-gradient(90deg,#313945,#3c4557)}@media(max-width:576px){.login-page__container{padding:var(--spacing-6, 1.5rem);margin:var(--spacing-4, 1rem);max-width:380px}}.login-page__logo{width:120px;height:120px;margin:0 auto var(--spacing-6, 1.5rem) auto;display:flex;justify-content:center;align-items:center}.login-page__logo-img{width:100%;height:100%;object-fit:contain;filter:drop-shadow(0 4px 8px rgba(30,58,95,.1));transition:transform .25s cubic-bezier(.4,0,.2,1)}.login-page__logo-img:hover{transform:scale(1.05)}.login-page__header{text-align:center;margin-bottom:var(--spacing-8, 2rem)}.login-page__header h1{font-family:var(--font-family-display, \"Bembo Std\", Georgia, serif);font-size:var(--font-size-3xl, 1.875rem);font-weight:var(--font-weight-bold, 700);color:var(--login-text-color, var(--color-white, #ffffff));margin:0 0 var(--spacing-3, .75rem) 0}@media(max-width:576px){.login-page__header h1{font-size:var(--font-size-2xl, 1.5rem)}}.login-page__header .subtitle{color:var(--login-text-color, var(--color-white, #ffffff));font-size:var(--font-size-base, 1rem);margin:0;font-weight:var(--font-weight-normal, 400);opacity:.8}.login-page__form .btn{width:100%;margin-bottom:var(--spacing-3, .75rem)}.login-page__actions{margin-top:var(--spacing-6, 1.5rem);display:flex;flex-direction:column;gap:var(--spacing-3, .75rem)}.login-page__footer{text-align:center;margin-top:var(--spacing-8, 2rem);padding-top:var(--spacing-6, 1.5rem);border-top:1px solid rgba(255,255,255,.2)}.login-page__footer p{color:#fffc;font-size:var(--font-size-sm, .875rem);margin:0}.login-page__version{text-align:center;margin-top:var(--spacing-4, 1rem)}.login-page__version span{font-size:var(--font-size-xs, .75rem);color:#ffffff80}.btn{display:inline-flex;align-items:center;justify-content:center;padding:var(--spacing-3, .75rem) var(--spacing-6, 1.5rem);font-size:var(--font-size-base, 1rem);font-weight:var(--font-weight-medium, 500);border-radius:var(--border-radius-lg, .5rem);border:none;cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1)}.btn--lg{height:48px;font-size:var(--font-size-base, 1rem)}.btn--primary{background-color:var(--color-gray-900, #212529);color:var(--color-white, #ffffff);border:1px solid var(--color-primary, #313945)}.btn--primary:hover:not(:disabled){background-color:var(--color-primary-hover, #1c1e4d);border-color:var(--color-primary-hover, #1c1e4d);transform:translateY(-2px);box-shadow:0 10px 15px -3px #0000001a}.btn--primary:active:not(:disabled){transform:translateY(0)}.btn--primary:disabled{opacity:.7;cursor:not-allowed}.btn--primary.loading{pointer-events:none}.btn--secondary{background-color:transparent;color:var(--color-white, #ffffff);border:1px solid rgba(255,255,255,.3)}.btn--secondary:hover:not(:disabled){background-color:#ffffff1a;border-color:#ffffff80}.auth-alert{padding:var(--spacing-4, 1rem);border-radius:var(--border-radius-lg, .5rem);margin-bottom:var(--spacing-4, 1rem)}.auth-alert--info{background:#2ca8ff1a;border:1px solid rgba(44,168,255,.3)}.auth-alert--success{background:#018e111a;border:1px solid rgba(1,142,17,.3)}.auth-alert__content h4{margin:0 0 var(--spacing-2, .5rem) 0;font-size:var(--font-size-lg, 1.125rem);font-weight:var(--font-weight-semibold, 600)}.auth-alert__content p{margin:0;font-size:var(--font-size-sm, .875rem);opacity:.9}.auth-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--spacing-8, 2rem)}.auth-loading__spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.2);border-top:3px solid var(--color-white, #ffffff);border-radius:50%;animation:spin 1s linear infinite}.auth-loading__text{margin-top:var(--spacing-4, 1rem);font-size:var(--font-size-sm, .875rem);color:#fffc}@keyframes slideInUp{0%{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.login-page__container{animation:none}.login-page__logo-img{transition:none}.auth-loading__spinner{animation:none}}@media(prefers-contrast:high){.login-page__container{border:2px solid var(--color-white, #ffffff)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i2.AsyncPipe, name: "async" }] });
1162
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.16", type: LoginComponent, isStandalone: true, selector: "lib-login", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"login-page\" [ngStyle]=\"getCustomStyles()\">\r\n <div class=\"login-page__container\">\r\n <!-- Logo -->\r\n <div class=\"login-page__logo\">\r\n <img \r\n [src]=\"getConfig('logoUrl')\" \r\n [alt]=\"getConfig('logoAlt')\" \r\n class=\"login-page__logo-img\"\r\n [style.width]=\"getConfig('logoWidth')\"\r\n [style.height]=\"getConfig('logoHeight')\" />\r\n </div>\r\n\r\n <!-- Header -->\r\n <div class=\"login-page__header\">\r\n <h1>{{ getConfig('title') }}</h1>\r\n <p *ngIf=\"getConfig('subtitle')\" class=\"subtitle\">{{ getConfig('subtitle') }}</p>\r\n </div>\r\n \r\n <!-- Login Form -->\r\n <div *ngIf=\"(authService.isDoneLoading$ | async)\" class=\"login-page__form\">\r\n <div *ngIf=\"!(authService.isAuthenticated$ | async)\">\r\n <!-- Status Message -->\r\n <div *ngIf=\"statusMessage\" class=\"auth-alert auth-alert--info\">\r\n <div class=\"auth-alert__content\">\r\n <p>{{ statusMessage }}</p>\r\n </div>\r\n </div>\r\n\r\n <!-- Login Button -->\r\n <button \r\n class=\"btn btn--primary btn--lg\" \r\n (click)=\"login()\" \r\n [disabled]=\"isLoggingIn\"\r\n [class.loading]=\"isLoggingIn\">\r\n {{ isLoggingIn ? 'Redirigiendo...' : getConfig('loginButtonText') }}\r\n </button>\r\n\r\n <!-- Footer -->\r\n <div class=\"login-page__footer\" *ngIf=\"getConfig('showFooter')\">\r\n <p *ngIf=\"getConfig('footerText'); else defaultFooter\">\r\n {{ getConfig('footerText') }}\r\n </p>\r\n <ng-template #defaultFooter>\r\n <p>\r\n Acceso seguro para usuarios de \r\n <strong>{{ getConfig('institutionName') }}</strong>\r\n </p>\r\n </ng-template>\r\n </div>\r\n </div>\r\n \r\n <!-- Already Authenticated -->\r\n <div *ngIf=\"authService.isAuthenticated$ | async\" class=\"auth-alert auth-alert--success\">\r\n <div class=\"auth-alert__content\">\r\n <h4>\u00A1Sesi\u00F3n activa!</h4>\r\n <p>Ya tienes una sesi\u00F3n iniciada en el sistema.</p>\r\n </div>\r\n \r\n <div class=\"login-page__actions\">\r\n <button class=\"btn btn--primary btn--lg\" (click)=\"goToDashboard()\">\r\n {{ getConfig('dashboardButtonText') }}\r\n </button>\r\n \r\n <button \r\n *ngIf=\"getConfig('showLogoutButton')\"\r\n class=\"btn btn--secondary btn--lg\" \r\n (click)=\"logout()\">\r\n {{ getConfig('logoutButtonText') }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n \r\n <!-- Loading State -->\r\n <div *ngIf=\"!(authService.isDoneLoading$ | async)\" class=\"auth-loading\">\r\n <div class=\"auth-loading__spinner\"></div>\r\n <p class=\"auth-loading__text\">Procesando autenticaci\u00F3n...</p>\r\n </div>\r\n\r\n <!-- Version -->\r\n <div *ngIf=\"getConfig('showVersion')\" class=\"login-page__version\">\r\n <span>v{{ getConfig('version') }}</span>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100vh}.login-page{min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--color-primary, #313945);padding:var(--spacing-4, 1rem);position:relative}.login-page__container{width:100%;max-width:450px;background:var(--login-bg-color, var(--color-primary, #313945));color:var(--login-text-color, var(--color-white, #ffffff));border-radius:var(--border-radius-2xl, 1rem);box-shadow:0 25px 50px -12px #00000040;padding:var(--spacing-8, 2rem);position:relative;overflow:hidden;border:1px solid rgba(255,255,255,.2);animation:slideInUp .6s cubic-bezier(.4,0,.2,1)}.login-page__container:before{content:\"\";position:absolute;top:0;left:0;right:0;height:6px;background:linear-gradient(90deg,#313945,#3c4557)}@media(max-width:576px){.login-page__container{padding:var(--spacing-6, 1.5rem);margin:var(--spacing-4, 1rem);max-width:380px}}.login-page__logo{width:120px;height:120px;margin:0 auto var(--spacing-6, 1.5rem) auto;display:flex;justify-content:center;align-items:center}.login-page__logo-img{width:100%;height:100%;object-fit:contain;filter:drop-shadow(0 4px 8px rgba(30,58,95,.1));transition:transform .25s cubic-bezier(.4,0,.2,1)}.login-page__logo-img:hover{transform:scale(1.05)}.login-page__header{text-align:center;margin-bottom:var(--spacing-8, 2rem)}.login-page__header h1{font-family:var(--font-family-display, \"Bembo Std\", Georgia, serif);font-size:var(--font-size-3xl, 1.875rem);font-weight:var(--font-weight-bold, 700);color:var(--login-text-color, var(--color-white, #ffffff));margin:0 0 var(--spacing-3, .75rem) 0}@media(max-width:576px){.login-page__header h1{font-size:var(--font-size-2xl, 1.5rem)}}.login-page__header .subtitle{color:var(--login-text-color, var(--color-white, #ffffff));font-size:var(--font-size-base, 1rem);margin:0;font-weight:var(--font-weight-normal, 400);opacity:.8}.login-page__form .btn{width:100%;margin-bottom:var(--spacing-3, .75rem)}.login-page__actions{margin-top:var(--spacing-6, 1.5rem);display:flex;flex-direction:column;gap:var(--spacing-3, .75rem)}.login-page__footer{text-align:center;margin-top:var(--spacing-8, 2rem);padding-top:var(--spacing-6, 1.5rem);border-top:1px solid rgba(255,255,255,.2)}.login-page__footer p{color:#fffc;font-size:var(--font-size-sm, .875rem);margin:0}.login-page__version{text-align:center;margin-top:var(--spacing-4, 1rem)}.login-page__version span{font-size:var(--font-size-xs, .75rem);color:#ffffff80}.btn{display:inline-flex;align-items:center;justify-content:center;padding:var(--spacing-3, .75rem) var(--spacing-6, 1.5rem);font-size:var(--font-size-base, 1rem);font-weight:var(--font-weight-medium, 500);border-radius:var(--border-radius-lg, .5rem);border:none;cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1)}.btn--lg{height:48px;font-size:var(--font-size-base, 1rem)}.btn--primary{background-color:var(--color-gray-900, #212529);color:var(--color-white, #ffffff);border:1px solid var(--color-primary, #313945)}.btn--primary:hover:not(:disabled){background-color:var(--color-primary-hover, #1c1e4d);border-color:var(--color-primary-hover, #1c1e4d);transform:translateY(-2px);box-shadow:0 10px 15px -3px #0000001a}.btn--primary:active:not(:disabled){transform:translateY(0)}.btn--primary:disabled{opacity:.7;cursor:not-allowed}.btn--primary.loading{pointer-events:none}.btn--secondary{background-color:transparent;color:var(--color-white, #ffffff);border:1px solid rgba(255,255,255,.3)}.btn--secondary:hover:not(:disabled){background-color:#ffffff1a;border-color:#ffffff80}.auth-alert{padding:var(--spacing-4, 1rem);border-radius:var(--border-radius-lg, .5rem);margin-bottom:var(--spacing-4, 1rem)}.auth-alert--info{background:#2ca8ff1a;border:1px solid rgba(44,168,255,.3)}.auth-alert--success{background:#018e111a;border:1px solid rgba(1,142,17,.3)}.auth-alert__content h4{margin:0 0 var(--spacing-2, .5rem) 0;font-size:var(--font-size-lg, 1.125rem);font-weight:var(--font-weight-semibold, 600)}.auth-alert__content p{margin:0;font-size:var(--font-size-sm, .875rem);opacity:.9}.auth-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--spacing-8, 2rem)}.auth-loading__spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.2);border-top:3px solid var(--color-white, #ffffff);border-radius:50%;animation:spin 1s linear infinite}.auth-loading__text{margin-top:var(--spacing-4, 1rem);font-size:var(--font-size-sm, .875rem);color:#fffc}@keyframes slideInUp{0%{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.login-page__container{animation:none}.login-page__logo-img{transition:none}.auth-loading__spinner{animation:none}}@media(prefers-contrast:high){.login-page__container{border:2px solid var(--color-white, #ffffff)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i2.AsyncPipe, name: "async" }] });
1287
1163
  }
1288
1164
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: LoginComponent, decorators: [{
1289
1165
  type: Component,
1290
1166
  args: [{ selector: 'lib-login', standalone: true, imports: [CommonModule], template: "<div class=\"login-page\" [ngStyle]=\"getCustomStyles()\">\r\n <div class=\"login-page__container\">\r\n <!-- Logo -->\r\n <div class=\"login-page__logo\">\r\n <img \r\n [src]=\"getConfig('logoUrl')\" \r\n [alt]=\"getConfig('logoAlt')\" \r\n class=\"login-page__logo-img\"\r\n [style.width]=\"getConfig('logoWidth')\"\r\n [style.height]=\"getConfig('logoHeight')\" />\r\n </div>\r\n\r\n <!-- Header -->\r\n <div class=\"login-page__header\">\r\n <h1>{{ getConfig('title') }}</h1>\r\n <p *ngIf=\"getConfig('subtitle')\" class=\"subtitle\">{{ getConfig('subtitle') }}</p>\r\n </div>\r\n \r\n <!-- Login Form -->\r\n <div *ngIf=\"(authService.isDoneLoading$ | async)\" class=\"login-page__form\">\r\n <div *ngIf=\"!(authService.isAuthenticated$ | async)\">\r\n <!-- Status Message -->\r\n <div *ngIf=\"statusMessage\" class=\"auth-alert auth-alert--info\">\r\n <div class=\"auth-alert__content\">\r\n <p>{{ statusMessage }}</p>\r\n </div>\r\n </div>\r\n\r\n <!-- Login Button -->\r\n <button \r\n class=\"btn btn--primary btn--lg\" \r\n (click)=\"login()\" \r\n [disabled]=\"isLoggingIn\"\r\n [class.loading]=\"isLoggingIn\">\r\n {{ isLoggingIn ? 'Redirigiendo...' : getConfig('loginButtonText') }}\r\n </button>\r\n\r\n <!-- Footer -->\r\n <div class=\"login-page__footer\" *ngIf=\"getConfig('showFooter')\">\r\n <p *ngIf=\"getConfig('footerText'); else defaultFooter\">\r\n {{ getConfig('footerText') }}\r\n </p>\r\n <ng-template #defaultFooter>\r\n <p>\r\n Acceso seguro para usuarios de \r\n <strong>{{ getConfig('institutionName') }}</strong>\r\n </p>\r\n </ng-template>\r\n </div>\r\n </div>\r\n \r\n <!-- Already Authenticated -->\r\n <div *ngIf=\"authService.isAuthenticated$ | async\" class=\"auth-alert auth-alert--success\">\r\n <div class=\"auth-alert__content\">\r\n <h4>\u00A1Sesi\u00F3n activa!</h4>\r\n <p>Ya tienes una sesi\u00F3n iniciada en el sistema.</p>\r\n </div>\r\n \r\n <div class=\"login-page__actions\">\r\n <button class=\"btn btn--primary btn--lg\" (click)=\"goToDashboard()\">\r\n {{ getConfig('dashboardButtonText') }}\r\n </button>\r\n \r\n <button \r\n *ngIf=\"getConfig('showLogoutButton')\"\r\n class=\"btn btn--secondary btn--lg\" \r\n (click)=\"logout()\">\r\n {{ getConfig('logoutButtonText') }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n \r\n <!-- Loading State -->\r\n <div *ngIf=\"!(authService.isDoneLoading$ | async)\" class=\"auth-loading\">\r\n <div class=\"auth-loading__spinner\"></div>\r\n <p class=\"auth-loading__text\">Procesando autenticaci\u00F3n...</p>\r\n </div>\r\n\r\n <!-- Version -->\r\n <div *ngIf=\"getConfig('showVersion')\" class=\"login-page__version\">\r\n <span>v{{ getConfig('version') }}</span>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100vh}.login-page{min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--color-primary, #313945);padding:var(--spacing-4, 1rem);position:relative}.login-page__container{width:100%;max-width:450px;background:var(--login-bg-color, var(--color-primary, #313945));color:var(--login-text-color, var(--color-white, #ffffff));border-radius:var(--border-radius-2xl, 1rem);box-shadow:0 25px 50px -12px #00000040;padding:var(--spacing-8, 2rem);position:relative;overflow:hidden;border:1px solid rgba(255,255,255,.2);animation:slideInUp .6s cubic-bezier(.4,0,.2,1)}.login-page__container:before{content:\"\";position:absolute;top:0;left:0;right:0;height:6px;background:linear-gradient(90deg,#313945,#3c4557)}@media(max-width:576px){.login-page__container{padding:var(--spacing-6, 1.5rem);margin:var(--spacing-4, 1rem);max-width:380px}}.login-page__logo{width:120px;height:120px;margin:0 auto var(--spacing-6, 1.5rem) auto;display:flex;justify-content:center;align-items:center}.login-page__logo-img{width:100%;height:100%;object-fit:contain;filter:drop-shadow(0 4px 8px rgba(30,58,95,.1));transition:transform .25s cubic-bezier(.4,0,.2,1)}.login-page__logo-img:hover{transform:scale(1.05)}.login-page__header{text-align:center;margin-bottom:var(--spacing-8, 2rem)}.login-page__header h1{font-family:var(--font-family-display, \"Bembo Std\", Georgia, serif);font-size:var(--font-size-3xl, 1.875rem);font-weight:var(--font-weight-bold, 700);color:var(--login-text-color, var(--color-white, #ffffff));margin:0 0 var(--spacing-3, .75rem) 0}@media(max-width:576px){.login-page__header h1{font-size:var(--font-size-2xl, 1.5rem)}}.login-page__header .subtitle{color:var(--login-text-color, var(--color-white, #ffffff));font-size:var(--font-size-base, 1rem);margin:0;font-weight:var(--font-weight-normal, 400);opacity:.8}.login-page__form .btn{width:100%;margin-bottom:var(--spacing-3, .75rem)}.login-page__actions{margin-top:var(--spacing-6, 1.5rem);display:flex;flex-direction:column;gap:var(--spacing-3, .75rem)}.login-page__footer{text-align:center;margin-top:var(--spacing-8, 2rem);padding-top:var(--spacing-6, 1.5rem);border-top:1px solid rgba(255,255,255,.2)}.login-page__footer p{color:#fffc;font-size:var(--font-size-sm, .875rem);margin:0}.login-page__version{text-align:center;margin-top:var(--spacing-4, 1rem)}.login-page__version span{font-size:var(--font-size-xs, .75rem);color:#ffffff80}.btn{display:inline-flex;align-items:center;justify-content:center;padding:var(--spacing-3, .75rem) var(--spacing-6, 1.5rem);font-size:var(--font-size-base, 1rem);font-weight:var(--font-weight-medium, 500);border-radius:var(--border-radius-lg, .5rem);border:none;cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1)}.btn--lg{height:48px;font-size:var(--font-size-base, 1rem)}.btn--primary{background-color:var(--color-gray-900, #212529);color:var(--color-white, #ffffff);border:1px solid var(--color-primary, #313945)}.btn--primary:hover:not(:disabled){background-color:var(--color-primary-hover, #1c1e4d);border-color:var(--color-primary-hover, #1c1e4d);transform:translateY(-2px);box-shadow:0 10px 15px -3px #0000001a}.btn--primary:active:not(:disabled){transform:translateY(0)}.btn--primary:disabled{opacity:.7;cursor:not-allowed}.btn--primary.loading{pointer-events:none}.btn--secondary{background-color:transparent;color:var(--color-white, #ffffff);border:1px solid rgba(255,255,255,.3)}.btn--secondary:hover:not(:disabled){background-color:#ffffff1a;border-color:#ffffff80}.auth-alert{padding:var(--spacing-4, 1rem);border-radius:var(--border-radius-lg, .5rem);margin-bottom:var(--spacing-4, 1rem)}.auth-alert--info{background:#2ca8ff1a;border:1px solid rgba(44,168,255,.3)}.auth-alert--success{background:#018e111a;border:1px solid rgba(1,142,17,.3)}.auth-alert__content h4{margin:0 0 var(--spacing-2, .5rem) 0;font-size:var(--font-size-lg, 1.125rem);font-weight:var(--font-weight-semibold, 600)}.auth-alert__content p{margin:0;font-size:var(--font-size-sm, .875rem);opacity:.9}.auth-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--spacing-8, 2rem)}.auth-loading__spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.2);border-top:3px solid var(--color-white, #ffffff);border-radius:50%;animation:spin 1s linear infinite}.auth-loading__text{margin-top:var(--spacing-4, 1rem);font-size:var(--font-size-sm, .875rem);color:#fffc}@keyframes slideInUp{0%{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.login-page__container{animation:none}.login-page__logo-img{transition:none}.auth-loading__spinner{animation:none}}@media(prefers-contrast:high){.login-page__container{border:2px solid var(--color-white, #ffffff)}}\n"] }]
1291
- }], ctorParameters: () => [{ type: AuthService }], propDecorators: { config: [{
1292
- type: Input
1293
- }] } });
1167
+ }], ctorParameters: () => [{ type: AuthService }], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }] } });
1294
1168
 
1295
1169
  class DashboardComponent {
1296
1170
  authService;
1297
1171
  authorizationService;
1298
- config = {};
1172
+ config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
1173
+ store = inject(FrmkConfigStore);
1299
1174
  userInfo = null;
1300
1175
  userRoles = [];
1301
1176
  menuHierarchy = [];
@@ -1304,43 +1179,31 @@ class DashboardComponent {
1304
1179
  openMenuItems = new Set();
1305
1180
  isUserPanelOpen = false;
1306
1181
  isMobileMenuOpen = false;
1307
- _mergedConfig;
1182
+ /** Configuración merged: defaults ← libraryConfig.dashboardConfig ← @Input config */
1183
+ mergedConfig = computed(() => {
1184
+ const storeConfig = this.store.isConfigured()
1185
+ ? this.store.dashboardConfig()
1186
+ : DEFAULT_DASHBOARD_CONFIG;
1187
+ const inputCfg = this.config();
1188
+ return {
1189
+ ...storeConfig,
1190
+ ...inputCfg,
1191
+ version: inputCfg.version || storeConfig.version || VERSION
1192
+ };
1193
+ }, ...(ngDevMode ? [{ debugName: "mergedConfig" }] : []));
1308
1194
  constructor(authService, authorizationService) {
1309
1195
  this.authService = authService;
1310
1196
  this.authorizationService = authorizationService;
1311
- // Initialize with global config in constructor to avoid template errors
1312
- try {
1313
- this._mergedConfig = getDashboardConfig();
1314
- }
1315
- catch {
1316
- this._mergedConfig = { ...DEFAULT_DASHBOARD_CONFIG };
1317
- }
1318
1197
  }
1319
1198
  ngOnInit() {
1320
- // Get global config from library, then merge with component input
1321
- try {
1322
- const globalConfig = getDashboardConfig();
1323
- this._mergedConfig = {
1324
- ...globalConfig,
1325
- ...this.config,
1326
- version: this.config?.version || globalConfig.version || VERSION
1327
- };
1328
- }
1329
- catch {
1330
- this._mergedConfig = {
1331
- ...DEFAULT_DASHBOARD_CONFIG,
1332
- ...this.config,
1333
- version: this.config?.version || VERSION
1334
- };
1335
- }
1336
1199
  this.loadUserInfo();
1337
1200
  this.loadMenuHierarchy();
1338
1201
  }
1339
1202
  getConfig(key) {
1340
- return this._mergedConfig[key];
1203
+ return this.mergedConfig()[key];
1341
1204
  }
1342
1205
  get appVersion() {
1343
- return this._mergedConfig.version;
1206
+ return this.mergedConfig().version;
1344
1207
  }
1345
1208
  loadUserInfo() {
1346
1209
  this.userInfo = this.authService.identityClaims;
@@ -1542,14 +1405,12 @@ class DashboardComponent {
1542
1405
  document.body.style.overflow = '';
1543
1406
  }
1544
1407
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DashboardComponent, deps: [{ token: AuthService }, { token: AuthorizationService }], target: i0.ɵɵFactoryTarget.Component });
1545
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: DashboardComponent, isStandalone: true, selector: "lib-dashboard", inputs: { config: "config" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, ngImport: i0, template: "<div class=\"dashboard\">\r\n <!-- Header -->\r\n <header class=\"dashboard-header\">\r\n <!-- Mobile Header Top -->\r\n <div class=\"header-mobile-top\">\r\n <div class=\"ministry-logo-mobile\">\r\n <img [src]=\"getConfig('logoUrl')\" [alt]=\"getConfig('logoAlt')\" />\r\n </div>\r\n <h1 class=\"mobile-title\">{{ getConfig('title') }}</h1>\r\n </div>\r\n\r\n <!-- Mobile Header Bottom -->\r\n <div class=\"header-mobile-bottom\">\r\n <button class=\"hamburger-btn\" \r\n (click)=\"toggleMobileMenu()\"\r\n [class.active]=\"isMobileMenuOpen\"\r\n aria-label=\"Toggle menu\">\r\n <span class=\"hamburger-line\"></span>\r\n <span class=\"hamburger-line\"></span>\r\n <span class=\"hamburger-line\"></span>\r\n </button>\r\n\r\n <div class=\"user-profile mobile-user\" *ngIf=\"userInfo\">\r\n <button class=\"user-info-trigger\" \r\n (click)=\"toggleUserPanel($event)\"\r\n [attr.aria-expanded]=\"isUserPanelOpen\"\r\n aria-label=\"Abrir men\u00FA de usuario\">\r\n <div class=\"user-avatar\">\r\n <span>{{ getUserInitials() }}</span>\r\n </div>\r\n <div class=\"user-details\">\r\n <span class=\"user-name\">{{ getUsername() }}</span>\r\n </div>\r\n <span class=\"material-symbols-outlined dropdown-icon\">\r\n {{ isUserPanelOpen ? 'expand_less' : 'expand_more' }}\r\n </span>\r\n </button>\r\n \r\n <!-- User Panel -->\r\n <ng-container *ngTemplateOutlet=\"userPanelTemplate\"></ng-container>\r\n </div>\r\n </div>\r\n\r\n <!-- Desktop Header -->\r\n <div class=\"header-desktop-layout\">\r\n <div class=\"header-left\">\r\n <div class=\"ministry-logo\">\r\n <div class=\"logo-icon\">\r\n <img [src]=\"getConfig('logoUrl')\" [alt]=\"getConfig('logoAlt')\" />\r\n </div>\r\n <div class=\"ministry-info\">\r\n <h1>{{ getConfig('title') }}</h1>\r\n <span class=\"subtitle\" *ngIf=\"getConfig('subtitle')\">{{ getConfig('subtitle') }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-right\">\r\n <div class=\"user-profile desktop-user\" *ngIf=\"userInfo\">\r\n <button class=\"user-info-trigger\" \r\n (click)=\"toggleUserPanel($event)\"\r\n [attr.aria-expanded]=\"isUserPanelOpen\"\r\n aria-label=\"Abrir men\u00FA de usuario\">\r\n <div class=\"user-avatar\">\r\n <span>{{ getUserInitials() }}</span>\r\n </div>\r\n <div class=\"user-details\">\r\n <span class=\"user-name\">{{ getUsername() }}</span>\r\n </div>\r\n <span class=\"material-symbols-outlined dropdown-icon\">\r\n {{ isUserPanelOpen ? 'expand_less' : 'expand_more' }}\r\n </span>\r\n </button>\r\n \r\n <!-- User Panel -->\r\n <ng-container *ngTemplateOutlet=\"userPanelTemplate\"></ng-container>\r\n </div>\r\n </div>\r\n </div>\r\n </header>\r\n\r\n <!-- User Panel Template -->\r\n <ng-template #userPanelTemplate>\r\n <div class=\"user-panel\" *ngIf=\"isUserPanelOpen\">\r\n <div class=\"user-panel-header\">\r\n <span class=\"panel-title\">Perfil de Usuario</span>\r\n </div>\r\n <div class=\"user-panel-content\">\r\n <div class=\"user-info-item\">\r\n <span class=\"material-symbols-outlined info-icon\">person</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">C\u00F3digo de Usuario:</span>\r\n <span class=\"info-value\">{{ getUsername() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item\">\r\n <span class=\"material-symbols-outlined info-icon\">badge</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Nombre de Usuario:</span>\r\n <span class=\"info-value\">{{ getUserFullName() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item\">\r\n <span class=\"material-symbols-outlined info-icon\">credit_card</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Documento:</span>\r\n <span class=\"info-value\">{{ getDocument() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item mobile-hidden\">\r\n <span class=\"material-symbols-outlined info-icon\">business</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Instituci\u00F3n:</span>\r\n <span class=\"info-value\">{{ getInstitution() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item mobile-hidden\">\r\n <span class=\"material-symbols-outlined info-icon\">apartment</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Dependencia:</span>\r\n <span class=\"info-value\">{{ getDependency() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item mobile-hidden\">\r\n <span class=\"material-symbols-outlined info-icon\">work</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Rol:</span>\r\n <span class=\"info-value\">{{ getRole() }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"user-panel-footer\">\r\n <button class=\"logout-btn-panel\" (click)=\"logoutAndRedirect()\" title=\"Cerrar sesi\u00F3n\">\r\n <span class=\"material-symbols-outlined\">logout</span>\r\n <span class=\"logout-text\">CERRAR SESI\u00D3N</span>\r\n </button>\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Mobile Menu Overlay -->\r\n <div class=\"nav-overlay\" *ngIf=\"isMobileMenuOpen\" (click)=\"closeMobileMenu()\"></div>\r\n\r\n <!-- Navigation (Horizontal Desktop / Sidebar Mobile) -->\r\n <nav class=\"dashboard-nav\" [class.mobile-open]=\"isMobileMenuOpen\">\r\n <div class=\"nav-mobile-header\">\r\n <div class=\"nav-mobile-logo\">\r\n <img [src]=\"getConfig('logoUrl')\" [alt]=\"getConfig('logoAlt')\" />\r\n <span class=\"nav-mobile-title\">Men\u00FA</span>\r\n </div>\r\n <button class=\"nav-close-btn\" (click)=\"closeMobileMenu()\" aria-label=\"Cerrar men\u00FA\">\r\n <span class=\"material-symbols-outlined\">close</span>\r\n </button>\r\n </div>\r\n\r\n <div class=\"nav-container\">\r\n <!-- Static Menu Items -->\r\n <ng-container *ngIf=\"getConfig('showStaticMenu')\">\r\n <a routerLink=\".\" \r\n routerLinkActive=\"active\" \r\n [routerLinkActiveOptions]=\"{exact: true}\" \r\n class=\"nav-item\"\r\n (click)=\"closeMobileMenu()\">\r\n <span class=\"material-symbols-outlined nav-icon\">home</span>\r\n <span class=\"nav-text\">Inicio</span>\r\n </a>\r\n </ng-container>\r\n\r\n <!-- Separator -->\r\n <div class=\"nav-separator\" *ngIf=\"!isLoadingMenu && menuHierarchy.length > 0\"></div>\r\n \r\n <!-- Dynamic Menu -->\r\n <div class=\"dynamic-menu\" *ngIf=\"!isLoadingMenu && menuHierarchy.length > 0\">\r\n <ng-container *ngFor=\"let rootItem of getRootMenuItems()\">\r\n <div class=\"menu-item-container\" [class.has-children]=\"hasChildren(rootItem)\">\r\n <a \r\n [routerLink]=\"hasChildren(rootItem) ? null : rootItem.path\" \r\n routerLinkActive=\"active\" \r\n class=\"nav-item root-item\"\r\n [class.expandable]=\"hasChildren(rootItem)\"\r\n (click)=\"onMenuItemClick($event, rootItem)\">\r\n <span class=\"material-symbols-outlined nav-icon\">{{ getMenuIcon(rootItem) }}</span>\r\n <span class=\"nav-text\">{{ rootItem.name }}</span>\r\n <span class=\"material-symbols-outlined expand-icon\" *ngIf=\"hasChildren(rootItem)\">\r\n {{ isMenuItemOpen(rootItem.code) ? 'expand_more' : 'chevron_right' }}\r\n </span>\r\n </a>\r\n \r\n <!-- Submenu -->\r\n <div class=\"submenu\" *ngIf=\"hasChildren(rootItem) && isMenuItemOpen(rootItem.code)\">\r\n <ng-container *ngTemplateOutlet=\"menuTemplate; context: { items: rootItem.children, level: 2 }\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </div>\r\n \r\n <!-- Loading Indicator -->\r\n <div class=\"menu-loading\" *ngIf=\"isLoadingMenu\">\r\n <span class=\"material-symbols-outlined nav-icon\">hourglass_empty</span>\r\n <span class=\"nav-text\">Cargando men\u00FA...</span>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <!-- Recursive Menu Template -->\r\n <ng-template #menuTemplate let-items=\"items\" let-level=\"level\">\r\n <ng-container *ngFor=\"let item of items\">\r\n <div class=\"menu-item-container\" [class.has-children]=\"hasChildren(item)\" [attr.data-level]=\"level\">\r\n <a \r\n [routerLink]=\"hasChildren(item) ? null : item.path\" \r\n routerLinkActive=\"active\" \r\n class=\"nav-item submenu-item\"\r\n [class.expandable]=\"hasChildren(item)\"\r\n (click)=\"onMenuItemClick($event, item)\">\r\n <span class=\"material-symbols-outlined nav-icon\">{{ getMenuIcon(item) }}</span>\r\n <span class=\"nav-text\">{{ item.name }}</span>\r\n <span class=\"material-symbols-outlined expand-icon\" *ngIf=\"hasChildren(item)\">\r\n {{ isMenuItemOpen(item.code) ? 'expand_more' : 'chevron_right' }}\r\n </span>\r\n </a>\r\n\r\n <!-- Recursive Submenu (max 5 levels) -->\r\n <div class=\"submenu\" *ngIf=\"hasChildren(item) && isMenuItemOpen(item.code) && level < 5\">\r\n <ng-container *ngTemplateOutlet=\"menuTemplate; context: { items: item.children, level: level + 1 }\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </ng-template>\r\n\r\n <!-- Main Content -->\r\n <main class=\"dashboard-content\">\r\n <div class=\"content-wrapper\">\r\n <router-outlet></router-outlet>\r\n </div>\r\n </main>\r\n\r\n <!-- Footer -->\r\n <footer class=\"dashboard-footer\">\r\n <div class=\"footer-content\">\r\n <span class=\"footer-text\">\r\n {{ getConfig('footerText') }}\r\n <ng-container *ngIf=\"getConfig('showVersion')\">\r\n Versi\u00F3n {{ appVersion }}.\r\n </ng-container>\r\n Todos los derechos reservados.\r\n </span>\r\n </div>\r\n </footer>\r\n</div>\r\n", styles: [":host{display:block;width:100%;min-height:100vh}.dashboard{min-height:100vh;display:flex;flex-direction:column;background:var(--color-primary, #313945);font-family:var(--font-family-primary, \"Museo Sans\", sans-serif)}.dashboard-header{background:var(--color-primary, #313945);color:var(--color-white, #ffffff);padding:var(--spacing-2, .5rem) var(--spacing-4, 1rem);border-bottom:1px solid rgba(255,255,255,.1);display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:1000}@media(max-width:768px){.dashboard-header{flex-direction:column;gap:var(--spacing-2, .5rem);padding:var(--spacing-2, .5rem);align-items:stretch}}.header-mobile-top{display:none}@media(max-width:768px){.header-mobile-top{display:flex;justify-content:space-between;align-items:center;width:100%;gap:var(--spacing-2, .5rem)}}.ministry-logo-mobile{display:none}@media(max-width:768px){.ministry-logo-mobile{display:flex;align-items:center}.ministry-logo-mobile img{height:50px;width:auto;filter:brightness(0) invert(1)}}.mobile-title{display:none}@media(max-width:768px){.mobile-title{display:block;font-size:var(--font-size-md, 1rem);font-weight:var(--font-weight-bold, 700);color:var(--color-white, #ffffff);margin:0;text-align:right;flex:1}}.header-mobile-bottom{display:none}@media(max-width:768px){.header-mobile-bottom{display:flex;justify-content:space-between;align-items:center;width:100%;gap:var(--spacing-2, .5rem);padding-top:var(--spacing-1, .25rem)}}.header-desktop-layout{display:flex;justify-content:space-between;align-items:center;width:100%}@media(max-width:768px){.header-desktop-layout{display:none}}.header-left{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.header-right{display:flex;align-items:center}.ministry-logo{display:flex;align-items:center;gap:var(--spacing-3, .75rem)}.ministry-logo .logo-icon{display:flex;width:80px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.2))}.ministry-logo .logo-icon img{width:100%;height:auto}.ministry-logo .ministry-info h1{font-family:var(--font-family-primary, sans-serif);font-size:var(--font-size-xl, 1.25rem);font-weight:var(--font-weight-bold, 700);margin:0;color:var(--color-white, #ffffff);line-height:1.2}.ministry-logo .ministry-info .subtitle{font-size:var(--font-size-xs, .75rem);color:#ffffffd9;font-weight:var(--font-weight-medium, 500)}.hamburger-btn{display:none;flex-direction:column;justify-content:space-around;width:36px;height:36px;background:#ffffff1a;border:none;border-radius:var(--border-radius-md, .375rem);cursor:pointer;padding:6px;z-index:1003;transition:all .3s ease;flex-shrink:0}@media(max-width:768px){.hamburger-btn{display:flex}}.hamburger-btn .hamburger-line{width:100%;height:2px;background:var(--color-white, #ffffff);border-radius:2px;transition:all .3s ease;transform-origin:center}.hamburger-btn.active{background:#fff3}.hamburger-btn.active .hamburger-line:nth-child(1){transform:translateY(7px) rotate(45deg)}.hamburger-btn.active .hamburger-line:nth-child(2){opacity:0}.hamburger-btn.active .hamburger-line:nth-child(3){transform:translateY(-7px) rotate(-45deg)}.hamburger-btn:hover{background:#fff3}.user-profile{position:relative;display:flex;align-items:center}.user-profile .user-info-trigger{display:flex;align-items:center;gap:var(--spacing-2, .5rem);background:#ffffff1a;padding:var(--spacing-1, .25rem) var(--spacing-3, .75rem);border-radius:var(--border-radius-lg, .5rem);border:1px solid rgba(255,255,255,.2);cursor:pointer;transition:all .2s ease;color:inherit;font-family:inherit}.user-profile .user-info-trigger:hover{background:#ffffff26}.user-profile .user-avatar{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--color-white, #ffffff) 0%,#e2e8f0 100%);display:flex;align-items:center;justify-content:center;font-weight:var(--font-weight-bold, 700);color:var(--color-primary, #313945);font-size:var(--font-size-sm, .875rem);box-shadow:0 2px 6px #0000001a}.user-profile .user-details{display:flex;flex-direction:column}.user-profile .user-details .user-name{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-medium, 500);color:var(--color-white, #ffffff)}.user-profile .dropdown-icon{color:#fffc;font-size:18px;transition:transform .2s ease}.user-panel{position:absolute;top:calc(100% + var(--spacing-2, .5rem));right:0;min-width:320px;background:var(--color-white, #ffffff);border-radius:var(--border-radius-lg, .5rem);box-shadow:0 10px 40px #0003;border:1px solid var(--color-gray-200, #e9ecef);z-index:1100;overflow:hidden;animation:slideDown .2s ease}@media(max-width:768px){.user-panel{position:absolute;top:calc(100% + var(--spacing-1, .25rem));right:0;left:auto;min-width:280px;max-width:calc(100vw - 1rem);max-height:80vh;overflow-y:auto}}.user-panel-header{background:linear-gradient(135deg,#3c4557,#2a3142);padding:var(--spacing-3, .75rem) var(--spacing-4, 1rem)}.user-panel-header .panel-title{font-size:var(--font-size-md, 1rem);font-weight:var(--font-weight-bold, 700);color:var(--color-white, #ffffff)}.user-panel-content{padding:var(--spacing-3, .75rem)}.user-info-item{display:flex;align-items:flex-start;gap:var(--spacing-3, .75rem);padding:var(--spacing-2, .5rem) var(--spacing-3, .75rem);border-bottom:1px solid var(--color-gray-100, #f1f3f4)}.user-info-item:last-child{border-bottom:none}.user-info-item .info-icon{font-size:var(--font-size-lg, 1.125rem);flex-shrink:0;color:var(--color-primary, #313945)}.user-info-item .info-content{display:flex;flex-direction:column;gap:2px;flex:1}.user-info-item .info-content .info-label{font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-gray-600, #5a6268);text-transform:uppercase;letter-spacing:.5px}.user-info-item .info-content .info-value{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-medium, 500);color:var(--color-gray-900, #212529);word-break:break-word}@media(max-width:768px){.mobile-hidden{display:none!important}}.user-panel-footer{padding:var(--spacing-3, .75rem);background:var(--color-gray-50, #f8f9fa);border-top:1px solid var(--color-gray-200, #e9ecef)}.user-panel-footer .logout-btn-panel{width:100%;background:linear-gradient(135deg,#dc2626,#b91c1c);color:var(--color-white, #ffffff);border:none;padding:var(--spacing-3, .75rem);border-radius:var(--border-radius-md, .375rem);font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-bold, 700);cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:var(--spacing-2, .5rem);text-transform:uppercase}.user-panel-footer .logout-btn-panel:hover{background:linear-gradient(135deg,#b91c1c,#991b1b)}.nav-overlay{display:none}@media(max-width:768px){.nav-overlay{display:block;position:fixed;inset:0;background:#00000080;z-index:1001;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}}.dashboard-nav{background-color:#3c4557;box-shadow:0 2px 8px #0000001a}@media(max-width:768px){.dashboard-nav{position:fixed;top:0;left:0;bottom:0;width:280px;max-width:85%;transform:translate(-100%);z-index:1002;overflow-y:auto;box-shadow:2px 0 10px #0000004d;transition:transform .3s ease}.dashboard-nav.mobile-open{transform:translate(0)}}.dashboard-nav .nav-mobile-header{display:none}@media(max-width:768px){.dashboard-nav .nav-mobile-header{display:flex;justify-content:space-between;align-items:center;padding:var(--spacing-3, .75rem);background:var(--color-primary, #313945);border-bottom:1px solid rgba(255,255,255,.1);position:sticky;top:0;z-index:10}}.dashboard-nav .nav-mobile-logo{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.dashboard-nav .nav-mobile-logo img{width:36px;height:auto;filter:brightness(0) invert(1)}.dashboard-nav .nav-mobile-logo .nav-mobile-title{color:var(--color-white, #ffffff);font-size:var(--font-size-md, 1rem);font-weight:var(--font-weight-semibold, 600)}.dashboard-nav .nav-close-btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#ffffff1a;border:none;border-radius:var(--border-radius-md, .375rem);cursor:pointer;color:var(--color-white, #ffffff)}.dashboard-nav .nav-close-btn:hover{background:#fff3}.nav-container{display:flex;flex-wrap:wrap;align-items:center;gap:var(--spacing-1, .25rem);padding:var(--spacing-2, .5rem) var(--spacing-4, 1rem)}@media(max-width:768px){.nav-container{flex-direction:column;align-items:stretch;padding:var(--spacing-3, .75rem);gap:var(--spacing-1, .25rem)}}.nav-item{display:flex;align-items:center;gap:var(--spacing-2, .5rem);padding:var(--spacing-2, .5rem) var(--spacing-3, .75rem);color:var(--color-white, #ffffff);text-decoration:none;border-radius:var(--border-radius-md, .375rem);font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-medium, 500);transition:all .2s ease;cursor:pointer;background:transparent;border:none;font-family:inherit;white-space:nowrap}.nav-item:hover{background:#ffffff1a}.nav-item.active{background:#fff3}.nav-item .nav-icon{font-size:18px;flex-shrink:0}.nav-item .nav-text{flex:1;text-align:left}.nav-item .expand-icon{font-size:16px;flex-shrink:0;margin-left:var(--spacing-1, .25rem)}@media(max-width:768px){.nav-item{width:100%;padding:var(--spacing-3, .75rem)}}.nav-separator{width:1px;height:20px;background:#fff3;margin:0 var(--spacing-1, .25rem);align-self:center}@media(max-width:768px){.nav-separator{width:100%;height:1px;margin:var(--spacing-2, .5rem) 0}}.dynamic-menu{display:flex;flex-wrap:wrap;gap:var(--spacing-1, .25rem);align-items:flex-start}@media(max-width:768px){.dynamic-menu{flex-direction:column;width:100%}}.menu-item-container{position:relative}@media(max-width:768px){.menu-item-container{width:100%}}@media(min-width:769px){.menu-item-container>.submenu{position:absolute;top:100%;left:0;min-width:220px;background:#3c4557;border-radius:var(--border-radius-md, .375rem);box-shadow:0 4px 12px #0000004d;padding:var(--spacing-2, .5rem) 0;z-index:1000;margin-top:var(--spacing-1, .25rem)}}@media(max-width:768px){.menu-item-container>.submenu{padding-left:var(--spacing-4, 1rem)}}@media(min-width:769px){.submenu{min-width:200px}}@media(max-width:768px){.submenu{width:100%}}.submenu .menu-item-container{width:100%}@media(min-width:769px){.submenu .menu-item-container>.submenu{position:absolute;top:0;left:100%;min-width:200px;background:#3c4557;border-radius:var(--border-radius-md, .375rem);box-shadow:0 4px 12px #0000004d;padding:var(--spacing-2, .5rem) 0;z-index:1001;margin-left:2px}}@media(max-width:768px){.submenu .menu-item-container>.submenu{padding-left:var(--spacing-4, 1rem)}}.submenu .submenu-item{padding:var(--spacing-2, .5rem) var(--spacing-4, 1rem);font-size:var(--font-size-sm, .875rem);width:100%;box-sizing:border-box}.submenu .submenu-item:hover{background:#ffffff1a}.menu-loading{display:flex;align-items:center;gap:var(--spacing-2, .5rem);color:#ffffffb3;padding:var(--spacing-2, .5rem) var(--spacing-3, .75rem);font-size:var(--font-size-sm, .875rem)}.dashboard-content{flex:1;background:var(--color-white, #ffffff)}.dashboard-content .content-wrapper{padding:var(--spacing-4, 1rem) var(--spacing-6, 1.5rem);max-width:1400px;margin:0 auto}@media(max-width:768px){.dashboard-content .content-wrapper{padding:var(--spacing-3, .75rem)}}.dashboard-footer{background:#3c4557;color:#c5c8cf;padding:var(--spacing-3, .75rem) var(--spacing-4, 1rem);border-top:1px solid rgba(255,255,255,.1)}.dashboard-footer .footer-content{text-align:center}.dashboard-footer .footer-text{font-size:var(--font-size-xs, .75rem)}@media(min-width:769px){.mobile-user{display:none}}@media(max-width:768px){.desktop-user{display:none}}@keyframes slideDown{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i4.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: i4.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i4.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }] });
1408
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.16", type: DashboardComponent, isStandalone: true, selector: "lib-dashboard", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, ngImport: i0, template: "<div class=\"dashboard\">\r\n <!-- Header -->\r\n <header class=\"dashboard-header\">\r\n <!-- Mobile Header Top -->\r\n <div class=\"header-mobile-top\">\r\n <div class=\"ministry-logo-mobile\">\r\n <img [src]=\"getConfig('logoUrl')\" [alt]=\"getConfig('logoAlt')\" />\r\n </div>\r\n <h1 class=\"mobile-title\">{{ getConfig('title') }}</h1>\r\n </div>\r\n\r\n <!-- Mobile Header Bottom -->\r\n <div class=\"header-mobile-bottom\">\r\n <button class=\"hamburger-btn\" \r\n (click)=\"toggleMobileMenu()\"\r\n [class.active]=\"isMobileMenuOpen\"\r\n aria-label=\"Toggle menu\">\r\n <span class=\"hamburger-line\"></span>\r\n <span class=\"hamburger-line\"></span>\r\n <span class=\"hamburger-line\"></span>\r\n </button>\r\n\r\n <div class=\"user-profile mobile-user\" *ngIf=\"userInfo\">\r\n <button class=\"user-info-trigger\" \r\n (click)=\"toggleUserPanel($event)\"\r\n [attr.aria-expanded]=\"isUserPanelOpen\"\r\n aria-label=\"Abrir men\u00FA de usuario\">\r\n <div class=\"user-avatar\">\r\n <span>{{ getUserInitials() }}</span>\r\n </div>\r\n <div class=\"user-details\">\r\n <span class=\"user-name\">{{ getUsername() }}</span>\r\n </div>\r\n <span class=\"material-symbols-outlined dropdown-icon\">\r\n {{ isUserPanelOpen ? 'expand_less' : 'expand_more' }}\r\n </span>\r\n </button>\r\n \r\n <!-- User Panel -->\r\n <ng-container *ngTemplateOutlet=\"userPanelTemplate\"></ng-container>\r\n </div>\r\n </div>\r\n\r\n <!-- Desktop Header -->\r\n <div class=\"header-desktop-layout\">\r\n <div class=\"header-left\">\r\n <div class=\"ministry-logo\">\r\n <div class=\"logo-icon\">\r\n <img [src]=\"getConfig('logoUrl')\" [alt]=\"getConfig('logoAlt')\" />\r\n </div>\r\n <div class=\"ministry-info\">\r\n <h1>{{ getConfig('title') }}</h1>\r\n <span class=\"subtitle\" *ngIf=\"getConfig('subtitle')\">{{ getConfig('subtitle') }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-right\">\r\n <div class=\"user-profile desktop-user\" *ngIf=\"userInfo\">\r\n <button class=\"user-info-trigger\" \r\n (click)=\"toggleUserPanel($event)\"\r\n [attr.aria-expanded]=\"isUserPanelOpen\"\r\n aria-label=\"Abrir men\u00FA de usuario\">\r\n <div class=\"user-avatar\">\r\n <span>{{ getUserInitials() }}</span>\r\n </div>\r\n <div class=\"user-details\">\r\n <span class=\"user-name\">{{ getUsername() }}</span>\r\n </div>\r\n <span class=\"material-symbols-outlined dropdown-icon\">\r\n {{ isUserPanelOpen ? 'expand_less' : 'expand_more' }}\r\n </span>\r\n </button>\r\n \r\n <!-- User Panel -->\r\n <ng-container *ngTemplateOutlet=\"userPanelTemplate\"></ng-container>\r\n </div>\r\n </div>\r\n </div>\r\n </header>\r\n\r\n <!-- User Panel Template -->\r\n <ng-template #userPanelTemplate>\r\n <div class=\"user-panel\" *ngIf=\"isUserPanelOpen\">\r\n <div class=\"user-panel-header\">\r\n <span class=\"panel-title\">Perfil de Usuario</span>\r\n </div>\r\n <div class=\"user-panel-content\">\r\n <div class=\"user-info-item\">\r\n <span class=\"material-symbols-outlined info-icon\">person</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">C\u00F3digo de Usuario:</span>\r\n <span class=\"info-value\">{{ getUsername() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item\">\r\n <span class=\"material-symbols-outlined info-icon\">badge</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Nombre de Usuario:</span>\r\n <span class=\"info-value\">{{ getUserFullName() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item\">\r\n <span class=\"material-symbols-outlined info-icon\">credit_card</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Documento:</span>\r\n <span class=\"info-value\">{{ getDocument() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item mobile-hidden\">\r\n <span class=\"material-symbols-outlined info-icon\">business</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Instituci\u00F3n:</span>\r\n <span class=\"info-value\">{{ getInstitution() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item mobile-hidden\">\r\n <span class=\"material-symbols-outlined info-icon\">apartment</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Dependencia:</span>\r\n <span class=\"info-value\">{{ getDependency() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item mobile-hidden\">\r\n <span class=\"material-symbols-outlined info-icon\">work</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Rol:</span>\r\n <span class=\"info-value\">{{ getRole() }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"user-panel-footer\">\r\n <button class=\"logout-btn-panel\" (click)=\"logoutAndRedirect()\" title=\"Cerrar sesi\u00F3n\">\r\n <span class=\"material-symbols-outlined\">logout</span>\r\n <span class=\"logout-text\">CERRAR SESI\u00D3N</span>\r\n </button>\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Mobile Menu Overlay -->\r\n <div class=\"nav-overlay\" *ngIf=\"isMobileMenuOpen\" (click)=\"closeMobileMenu()\"></div>\r\n\r\n <!-- Navigation (Horizontal Desktop / Sidebar Mobile) -->\r\n <nav class=\"dashboard-nav\" [class.mobile-open]=\"isMobileMenuOpen\">\r\n <div class=\"nav-mobile-header\">\r\n <div class=\"nav-mobile-logo\">\r\n <img [src]=\"getConfig('logoUrl')\" [alt]=\"getConfig('logoAlt')\" />\r\n <span class=\"nav-mobile-title\">Men\u00FA</span>\r\n </div>\r\n <button class=\"nav-close-btn\" (click)=\"closeMobileMenu()\" aria-label=\"Cerrar men\u00FA\">\r\n <span class=\"material-symbols-outlined\">close</span>\r\n </button>\r\n </div>\r\n\r\n <div class=\"nav-container\">\r\n <!-- Static Menu Items -->\r\n <ng-container *ngIf=\"getConfig('showStaticMenu')\">\r\n <a routerLink=\".\" \r\n routerLinkActive=\"active\" \r\n [routerLinkActiveOptions]=\"{exact: true}\" \r\n class=\"nav-item\"\r\n (click)=\"closeMobileMenu()\">\r\n <span class=\"material-symbols-outlined nav-icon\">home</span>\r\n <span class=\"nav-text\">Inicio</span>\r\n </a>\r\n </ng-container>\r\n\r\n <!-- Separator -->\r\n <div class=\"nav-separator\" *ngIf=\"!isLoadingMenu && menuHierarchy.length > 0\"></div>\r\n \r\n <!-- Dynamic Menu -->\r\n <div class=\"dynamic-menu\" *ngIf=\"!isLoadingMenu && menuHierarchy.length > 0\">\r\n <ng-container *ngFor=\"let rootItem of getRootMenuItems()\">\r\n <div class=\"menu-item-container\" [class.has-children]=\"hasChildren(rootItem)\">\r\n <a \r\n [routerLink]=\"hasChildren(rootItem) ? null : rootItem.path\" \r\n routerLinkActive=\"active\" \r\n class=\"nav-item root-item\"\r\n [class.expandable]=\"hasChildren(rootItem)\"\r\n (click)=\"onMenuItemClick($event, rootItem)\">\r\n <span class=\"material-symbols-outlined nav-icon\">{{ getMenuIcon(rootItem) }}</span>\r\n <span class=\"nav-text\">{{ rootItem.name }}</span>\r\n <span class=\"material-symbols-outlined expand-icon\" *ngIf=\"hasChildren(rootItem)\">\r\n {{ isMenuItemOpen(rootItem.code) ? 'expand_more' : 'chevron_right' }}\r\n </span>\r\n </a>\r\n \r\n <!-- Submenu -->\r\n <div class=\"submenu\" *ngIf=\"hasChildren(rootItem) && isMenuItemOpen(rootItem.code)\">\r\n <ng-container *ngTemplateOutlet=\"menuTemplate; context: { items: rootItem.children, level: 2 }\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </div>\r\n \r\n <!-- Loading Indicator -->\r\n <div class=\"menu-loading\" *ngIf=\"isLoadingMenu\">\r\n <span class=\"material-symbols-outlined nav-icon\">hourglass_empty</span>\r\n <span class=\"nav-text\">Cargando men\u00FA...</span>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <!-- Recursive Menu Template -->\r\n <ng-template #menuTemplate let-items=\"items\" let-level=\"level\">\r\n <ng-container *ngFor=\"let item of items\">\r\n <div class=\"menu-item-container\" [class.has-children]=\"hasChildren(item)\" [attr.data-level]=\"level\">\r\n <a \r\n [routerLink]=\"hasChildren(item) ? null : item.path\" \r\n routerLinkActive=\"active\" \r\n class=\"nav-item submenu-item\"\r\n [class.expandable]=\"hasChildren(item)\"\r\n (click)=\"onMenuItemClick($event, item)\">\r\n <span class=\"material-symbols-outlined nav-icon\">{{ getMenuIcon(item) }}</span>\r\n <span class=\"nav-text\">{{ item.name }}</span>\r\n <span class=\"material-symbols-outlined expand-icon\" *ngIf=\"hasChildren(item)\">\r\n {{ isMenuItemOpen(item.code) ? 'expand_more' : 'chevron_right' }}\r\n </span>\r\n </a>\r\n\r\n <!-- Recursive Submenu (max 5 levels) -->\r\n <div class=\"submenu\" *ngIf=\"hasChildren(item) && isMenuItemOpen(item.code) && level < 5\">\r\n <ng-container *ngTemplateOutlet=\"menuTemplate; context: { items: item.children, level: level + 1 }\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </ng-template>\r\n\r\n <!-- Main Content -->\r\n <main class=\"dashboard-content\">\r\n <div class=\"content-wrapper\">\r\n <router-outlet></router-outlet>\r\n </div>\r\n </main>\r\n\r\n <!-- Footer -->\r\n <footer class=\"dashboard-footer\">\r\n <div class=\"footer-content\">\r\n <span class=\"footer-text\">\r\n {{ getConfig('footerText') }}\r\n <ng-container *ngIf=\"getConfig('showVersion')\">\r\n Versi\u00F3n {{ appVersion }}.\r\n </ng-container>\r\n Todos los derechos reservados.\r\n </span>\r\n </div>\r\n </footer>\r\n</div>\r\n", styles: [":host{display:block;width:100%;min-height:100vh}.dashboard{min-height:100vh;display:flex;flex-direction:column;background:var(--color-primary, #313945);font-family:var(--font-family-primary, \"Museo Sans\", sans-serif)}.dashboard-header{background:var(--color-primary, #313945);color:var(--color-white, #ffffff);padding:var(--spacing-2, .5rem) var(--spacing-4, 1rem);border-bottom:1px solid rgba(255,255,255,.1);display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:1000}@media(max-width:768px){.dashboard-header{flex-direction:column;gap:var(--spacing-2, .5rem);padding:var(--spacing-2, .5rem);align-items:stretch}}.header-mobile-top{display:none}@media(max-width:768px){.header-mobile-top{display:flex;justify-content:space-between;align-items:center;width:100%;gap:var(--spacing-2, .5rem)}}.ministry-logo-mobile{display:none}@media(max-width:768px){.ministry-logo-mobile{display:flex;align-items:center}.ministry-logo-mobile img{height:50px;width:auto;filter:brightness(0) invert(1)}}.mobile-title{display:none}@media(max-width:768px){.mobile-title{display:block;font-size:var(--font-size-md, 1rem);font-weight:var(--font-weight-bold, 700);color:var(--color-white, #ffffff);margin:0;text-align:right;flex:1}}.header-mobile-bottom{display:none}@media(max-width:768px){.header-mobile-bottom{display:flex;justify-content:space-between;align-items:center;width:100%;gap:var(--spacing-2, .5rem);padding-top:var(--spacing-1, .25rem)}}.header-desktop-layout{display:flex;justify-content:space-between;align-items:center;width:100%}@media(max-width:768px){.header-desktop-layout{display:none}}.header-left{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.header-right{display:flex;align-items:center}.ministry-logo{display:flex;align-items:center;gap:var(--spacing-3, .75rem)}.ministry-logo .logo-icon{display:flex;width:80px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.2))}.ministry-logo .logo-icon img{width:100%;height:auto}.ministry-logo .ministry-info h1{font-family:var(--font-family-primary, sans-serif);font-size:var(--font-size-xl, 1.25rem);font-weight:var(--font-weight-bold, 700);margin:0;color:var(--color-white, #ffffff);line-height:1.2}.ministry-logo .ministry-info .subtitle{font-size:var(--font-size-xs, .75rem);color:#ffffffd9;font-weight:var(--font-weight-medium, 500)}.hamburger-btn{display:none;flex-direction:column;justify-content:space-around;width:36px;height:36px;background:#ffffff1a;border:none;border-radius:var(--border-radius-md, .375rem);cursor:pointer;padding:6px;z-index:1003;transition:all .3s ease;flex-shrink:0}@media(max-width:768px){.hamburger-btn{display:flex}}.hamburger-btn .hamburger-line{width:100%;height:2px;background:var(--color-white, #ffffff);border-radius:2px;transition:all .3s ease;transform-origin:center}.hamburger-btn.active{background:#fff3}.hamburger-btn.active .hamburger-line:nth-child(1){transform:translateY(7px) rotate(45deg)}.hamburger-btn.active .hamburger-line:nth-child(2){opacity:0}.hamburger-btn.active .hamburger-line:nth-child(3){transform:translateY(-7px) rotate(-45deg)}.hamburger-btn:hover{background:#fff3}.user-profile{position:relative;display:flex;align-items:center}.user-profile .user-info-trigger{display:flex;align-items:center;gap:var(--spacing-2, .5rem);background:#ffffff1a;padding:var(--spacing-1, .25rem) var(--spacing-3, .75rem);border-radius:var(--border-radius-lg, .5rem);border:1px solid rgba(255,255,255,.2);cursor:pointer;transition:all .2s ease;color:inherit;font-family:inherit}.user-profile .user-info-trigger:hover{background:#ffffff26}.user-profile .user-avatar{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--color-white, #ffffff) 0%,#e2e8f0 100%);display:flex;align-items:center;justify-content:center;font-weight:var(--font-weight-bold, 700);color:var(--color-primary, #313945);font-size:var(--font-size-sm, .875rem);box-shadow:0 2px 6px #0000001a}.user-profile .user-details{display:flex;flex-direction:column}.user-profile .user-details .user-name{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-medium, 500);color:var(--color-white, #ffffff)}.user-profile .dropdown-icon{color:#fffc;font-size:18px;transition:transform .2s ease}.user-panel{position:absolute;top:calc(100% + var(--spacing-2, .5rem));right:0;min-width:320px;background:var(--color-white, #ffffff);border-radius:var(--border-radius-lg, .5rem);box-shadow:0 10px 40px #0003;border:1px solid var(--color-gray-200, #e9ecef);z-index:1100;overflow:hidden;animation:slideDown .2s ease}@media(max-width:768px){.user-panel{position:absolute;top:calc(100% + var(--spacing-1, .25rem));right:0;left:auto;min-width:280px;max-width:calc(100vw - 1rem);max-height:80vh;overflow-y:auto}}.user-panel-header{background:linear-gradient(135deg,#3c4557,#2a3142);padding:var(--spacing-3, .75rem) var(--spacing-4, 1rem)}.user-panel-header .panel-title{font-size:var(--font-size-md, 1rem);font-weight:var(--font-weight-bold, 700);color:var(--color-white, #ffffff)}.user-panel-content{padding:var(--spacing-3, .75rem)}.user-info-item{display:flex;align-items:flex-start;gap:var(--spacing-3, .75rem);padding:var(--spacing-2, .5rem) var(--spacing-3, .75rem);border-bottom:1px solid var(--color-gray-100, #f1f3f4)}.user-info-item:last-child{border-bottom:none}.user-info-item .info-icon{font-size:var(--font-size-lg, 1.125rem);flex-shrink:0;color:var(--color-primary, #313945)}.user-info-item .info-content{display:flex;flex-direction:column;gap:2px;flex:1}.user-info-item .info-content .info-label{font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-gray-600, #5a6268);text-transform:uppercase;letter-spacing:.5px}.user-info-item .info-content .info-value{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-medium, 500);color:var(--color-gray-900, #212529);word-break:break-word}@media(max-width:768px){.mobile-hidden{display:none!important}}.user-panel-footer{padding:var(--spacing-3, .75rem);background:var(--color-gray-50, #f8f9fa);border-top:1px solid var(--color-gray-200, #e9ecef)}.user-panel-footer .logout-btn-panel{width:100%;background:linear-gradient(135deg,#dc2626,#b91c1c);color:var(--color-white, #ffffff);border:none;padding:var(--spacing-3, .75rem);border-radius:var(--border-radius-md, .375rem);font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-bold, 700);cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:var(--spacing-2, .5rem);text-transform:uppercase}.user-panel-footer .logout-btn-panel:hover{background:linear-gradient(135deg,#b91c1c,#991b1b)}.nav-overlay{display:none}@media(max-width:768px){.nav-overlay{display:block;position:fixed;inset:0;background:#00000080;z-index:1001;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}}.dashboard-nav{background-color:#3c4557;box-shadow:0 2px 8px #0000001a}@media(max-width:768px){.dashboard-nav{position:fixed;top:0;left:0;bottom:0;width:280px;max-width:85%;transform:translate(-100%);z-index:1002;overflow-y:auto;box-shadow:2px 0 10px #0000004d;transition:transform .3s ease}.dashboard-nav.mobile-open{transform:translate(0)}}.dashboard-nav .nav-mobile-header{display:none}@media(max-width:768px){.dashboard-nav .nav-mobile-header{display:flex;justify-content:space-between;align-items:center;padding:var(--spacing-3, .75rem);background:var(--color-primary, #313945);border-bottom:1px solid rgba(255,255,255,.1);position:sticky;top:0;z-index:10}}.dashboard-nav .nav-mobile-logo{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.dashboard-nav .nav-mobile-logo img{width:36px;height:auto;filter:brightness(0) invert(1)}.dashboard-nav .nav-mobile-logo .nav-mobile-title{color:var(--color-white, #ffffff);font-size:var(--font-size-md, 1rem);font-weight:var(--font-weight-semibold, 600)}.dashboard-nav .nav-close-btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#ffffff1a;border:none;border-radius:var(--border-radius-md, .375rem);cursor:pointer;color:var(--color-white, #ffffff)}.dashboard-nav .nav-close-btn:hover{background:#fff3}.nav-container{display:flex;flex-wrap:wrap;align-items:center;gap:var(--spacing-1, .25rem);padding:var(--spacing-2, .5rem) var(--spacing-4, 1rem)}@media(max-width:768px){.nav-container{flex-direction:column;align-items:stretch;padding:var(--spacing-3, .75rem);gap:var(--spacing-1, .25rem)}}.nav-item{display:flex;align-items:center;gap:var(--spacing-2, .5rem);padding:var(--spacing-2, .5rem) var(--spacing-3, .75rem);color:var(--color-white, #ffffff);text-decoration:none;border-radius:var(--border-radius-md, .375rem);font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-medium, 500);transition:all .2s ease;cursor:pointer;background:transparent;border:none;font-family:inherit;white-space:nowrap}.nav-item:hover{background:#ffffff1a}.nav-item.active{background:#fff3}.nav-item .nav-icon{font-size:18px;flex-shrink:0}.nav-item .nav-text{flex:1;text-align:left}.nav-item .expand-icon{font-size:16px;flex-shrink:0;margin-left:var(--spacing-1, .25rem)}@media(max-width:768px){.nav-item{width:100%;padding:var(--spacing-3, .75rem)}}.nav-separator{width:1px;height:20px;background:#fff3;margin:0 var(--spacing-1, .25rem);align-self:center}@media(max-width:768px){.nav-separator{width:100%;height:1px;margin:var(--spacing-2, .5rem) 0}}.dynamic-menu{display:flex;flex-wrap:wrap;gap:var(--spacing-1, .25rem);align-items:flex-start}@media(max-width:768px){.dynamic-menu{flex-direction:column;width:100%}}.menu-item-container{position:relative}@media(max-width:768px){.menu-item-container{width:100%}}@media(min-width:769px){.menu-item-container>.submenu{position:absolute;top:100%;left:0;min-width:220px;background:#3c4557;border-radius:var(--border-radius-md, .375rem);box-shadow:0 4px 12px #0000004d;padding:var(--spacing-2, .5rem) 0;z-index:1000;margin-top:var(--spacing-1, .25rem)}}@media(max-width:768px){.menu-item-container>.submenu{padding-left:var(--spacing-4, 1rem)}}@media(min-width:769px){.submenu{min-width:200px}}@media(max-width:768px){.submenu{width:100%}}.submenu .menu-item-container{width:100%}@media(min-width:769px){.submenu .menu-item-container>.submenu{position:absolute;top:0;left:100%;min-width:200px;background:#3c4557;border-radius:var(--border-radius-md, .375rem);box-shadow:0 4px 12px #0000004d;padding:var(--spacing-2, .5rem) 0;z-index:1001;margin-left:2px}}@media(max-width:768px){.submenu .menu-item-container>.submenu{padding-left:var(--spacing-4, 1rem)}}.submenu .submenu-item{padding:var(--spacing-2, .5rem) var(--spacing-4, 1rem);font-size:var(--font-size-sm, .875rem);width:100%;box-sizing:border-box}.submenu .submenu-item:hover{background:#ffffff1a}.menu-loading{display:flex;align-items:center;gap:var(--spacing-2, .5rem);color:#ffffffb3;padding:var(--spacing-2, .5rem) var(--spacing-3, .75rem);font-size:var(--font-size-sm, .875rem)}.dashboard-content{flex:1;background:var(--color-white, #ffffff)}.dashboard-content .content-wrapper{padding:var(--spacing-4, 1rem) var(--spacing-6, 1.5rem);max-width:1400px;margin:0 auto}@media(max-width:768px){.dashboard-content .content-wrapper{padding:var(--spacing-3, .75rem)}}.dashboard-footer{background:#3c4557;color:#c5c8cf;padding:var(--spacing-3, .75rem) var(--spacing-4, 1rem);border-top:1px solid rgba(255,255,255,.1)}.dashboard-footer .footer-content{text-align:center}.dashboard-footer .footer-text{font-size:var(--font-size-xs, .75rem)}@media(min-width:769px){.mobile-user{display:none}}@media(max-width:768px){.desktop-user{display:none}}@keyframes slideDown{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i4.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: i4.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i4.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }] });
1546
1409
  }
1547
1410
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DashboardComponent, decorators: [{
1548
1411
  type: Component,
1549
1412
  args: [{ selector: 'lib-dashboard', standalone: true, imports: [CommonModule, RouterModule], template: "<div class=\"dashboard\">\r\n <!-- Header -->\r\n <header class=\"dashboard-header\">\r\n <!-- Mobile Header Top -->\r\n <div class=\"header-mobile-top\">\r\n <div class=\"ministry-logo-mobile\">\r\n <img [src]=\"getConfig('logoUrl')\" [alt]=\"getConfig('logoAlt')\" />\r\n </div>\r\n <h1 class=\"mobile-title\">{{ getConfig('title') }}</h1>\r\n </div>\r\n\r\n <!-- Mobile Header Bottom -->\r\n <div class=\"header-mobile-bottom\">\r\n <button class=\"hamburger-btn\" \r\n (click)=\"toggleMobileMenu()\"\r\n [class.active]=\"isMobileMenuOpen\"\r\n aria-label=\"Toggle menu\">\r\n <span class=\"hamburger-line\"></span>\r\n <span class=\"hamburger-line\"></span>\r\n <span class=\"hamburger-line\"></span>\r\n </button>\r\n\r\n <div class=\"user-profile mobile-user\" *ngIf=\"userInfo\">\r\n <button class=\"user-info-trigger\" \r\n (click)=\"toggleUserPanel($event)\"\r\n [attr.aria-expanded]=\"isUserPanelOpen\"\r\n aria-label=\"Abrir men\u00FA de usuario\">\r\n <div class=\"user-avatar\">\r\n <span>{{ getUserInitials() }}</span>\r\n </div>\r\n <div class=\"user-details\">\r\n <span class=\"user-name\">{{ getUsername() }}</span>\r\n </div>\r\n <span class=\"material-symbols-outlined dropdown-icon\">\r\n {{ isUserPanelOpen ? 'expand_less' : 'expand_more' }}\r\n </span>\r\n </button>\r\n \r\n <!-- User Panel -->\r\n <ng-container *ngTemplateOutlet=\"userPanelTemplate\"></ng-container>\r\n </div>\r\n </div>\r\n\r\n <!-- Desktop Header -->\r\n <div class=\"header-desktop-layout\">\r\n <div class=\"header-left\">\r\n <div class=\"ministry-logo\">\r\n <div class=\"logo-icon\">\r\n <img [src]=\"getConfig('logoUrl')\" [alt]=\"getConfig('logoAlt')\" />\r\n </div>\r\n <div class=\"ministry-info\">\r\n <h1>{{ getConfig('title') }}</h1>\r\n <span class=\"subtitle\" *ngIf=\"getConfig('subtitle')\">{{ getConfig('subtitle') }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-right\">\r\n <div class=\"user-profile desktop-user\" *ngIf=\"userInfo\">\r\n <button class=\"user-info-trigger\" \r\n (click)=\"toggleUserPanel($event)\"\r\n [attr.aria-expanded]=\"isUserPanelOpen\"\r\n aria-label=\"Abrir men\u00FA de usuario\">\r\n <div class=\"user-avatar\">\r\n <span>{{ getUserInitials() }}</span>\r\n </div>\r\n <div class=\"user-details\">\r\n <span class=\"user-name\">{{ getUsername() }}</span>\r\n </div>\r\n <span class=\"material-symbols-outlined dropdown-icon\">\r\n {{ isUserPanelOpen ? 'expand_less' : 'expand_more' }}\r\n </span>\r\n </button>\r\n \r\n <!-- User Panel -->\r\n <ng-container *ngTemplateOutlet=\"userPanelTemplate\"></ng-container>\r\n </div>\r\n </div>\r\n </div>\r\n </header>\r\n\r\n <!-- User Panel Template -->\r\n <ng-template #userPanelTemplate>\r\n <div class=\"user-panel\" *ngIf=\"isUserPanelOpen\">\r\n <div class=\"user-panel-header\">\r\n <span class=\"panel-title\">Perfil de Usuario</span>\r\n </div>\r\n <div class=\"user-panel-content\">\r\n <div class=\"user-info-item\">\r\n <span class=\"material-symbols-outlined info-icon\">person</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">C\u00F3digo de Usuario:</span>\r\n <span class=\"info-value\">{{ getUsername() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item\">\r\n <span class=\"material-symbols-outlined info-icon\">badge</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Nombre de Usuario:</span>\r\n <span class=\"info-value\">{{ getUserFullName() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item\">\r\n <span class=\"material-symbols-outlined info-icon\">credit_card</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Documento:</span>\r\n <span class=\"info-value\">{{ getDocument() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item mobile-hidden\">\r\n <span class=\"material-symbols-outlined info-icon\">business</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Instituci\u00F3n:</span>\r\n <span class=\"info-value\">{{ getInstitution() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item mobile-hidden\">\r\n <span class=\"material-symbols-outlined info-icon\">apartment</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Dependencia:</span>\r\n <span class=\"info-value\">{{ getDependency() }}</span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"user-info-item mobile-hidden\">\r\n <span class=\"material-symbols-outlined info-icon\">work</span>\r\n <div class=\"info-content\">\r\n <span class=\"info-label\">Rol:</span>\r\n <span class=\"info-value\">{{ getRole() }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"user-panel-footer\">\r\n <button class=\"logout-btn-panel\" (click)=\"logoutAndRedirect()\" title=\"Cerrar sesi\u00F3n\">\r\n <span class=\"material-symbols-outlined\">logout</span>\r\n <span class=\"logout-text\">CERRAR SESI\u00D3N</span>\r\n </button>\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Mobile Menu Overlay -->\r\n <div class=\"nav-overlay\" *ngIf=\"isMobileMenuOpen\" (click)=\"closeMobileMenu()\"></div>\r\n\r\n <!-- Navigation (Horizontal Desktop / Sidebar Mobile) -->\r\n <nav class=\"dashboard-nav\" [class.mobile-open]=\"isMobileMenuOpen\">\r\n <div class=\"nav-mobile-header\">\r\n <div class=\"nav-mobile-logo\">\r\n <img [src]=\"getConfig('logoUrl')\" [alt]=\"getConfig('logoAlt')\" />\r\n <span class=\"nav-mobile-title\">Men\u00FA</span>\r\n </div>\r\n <button class=\"nav-close-btn\" (click)=\"closeMobileMenu()\" aria-label=\"Cerrar men\u00FA\">\r\n <span class=\"material-symbols-outlined\">close</span>\r\n </button>\r\n </div>\r\n\r\n <div class=\"nav-container\">\r\n <!-- Static Menu Items -->\r\n <ng-container *ngIf=\"getConfig('showStaticMenu')\">\r\n <a routerLink=\".\" \r\n routerLinkActive=\"active\" \r\n [routerLinkActiveOptions]=\"{exact: true}\" \r\n class=\"nav-item\"\r\n (click)=\"closeMobileMenu()\">\r\n <span class=\"material-symbols-outlined nav-icon\">home</span>\r\n <span class=\"nav-text\">Inicio</span>\r\n </a>\r\n </ng-container>\r\n\r\n <!-- Separator -->\r\n <div class=\"nav-separator\" *ngIf=\"!isLoadingMenu && menuHierarchy.length > 0\"></div>\r\n \r\n <!-- Dynamic Menu -->\r\n <div class=\"dynamic-menu\" *ngIf=\"!isLoadingMenu && menuHierarchy.length > 0\">\r\n <ng-container *ngFor=\"let rootItem of getRootMenuItems()\">\r\n <div class=\"menu-item-container\" [class.has-children]=\"hasChildren(rootItem)\">\r\n <a \r\n [routerLink]=\"hasChildren(rootItem) ? null : rootItem.path\" \r\n routerLinkActive=\"active\" \r\n class=\"nav-item root-item\"\r\n [class.expandable]=\"hasChildren(rootItem)\"\r\n (click)=\"onMenuItemClick($event, rootItem)\">\r\n <span class=\"material-symbols-outlined nav-icon\">{{ getMenuIcon(rootItem) }}</span>\r\n <span class=\"nav-text\">{{ rootItem.name }}</span>\r\n <span class=\"material-symbols-outlined expand-icon\" *ngIf=\"hasChildren(rootItem)\">\r\n {{ isMenuItemOpen(rootItem.code) ? 'expand_more' : 'chevron_right' }}\r\n </span>\r\n </a>\r\n \r\n <!-- Submenu -->\r\n <div class=\"submenu\" *ngIf=\"hasChildren(rootItem) && isMenuItemOpen(rootItem.code)\">\r\n <ng-container *ngTemplateOutlet=\"menuTemplate; context: { items: rootItem.children, level: 2 }\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </div>\r\n \r\n <!-- Loading Indicator -->\r\n <div class=\"menu-loading\" *ngIf=\"isLoadingMenu\">\r\n <span class=\"material-symbols-outlined nav-icon\">hourglass_empty</span>\r\n <span class=\"nav-text\">Cargando men\u00FA...</span>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <!-- Recursive Menu Template -->\r\n <ng-template #menuTemplate let-items=\"items\" let-level=\"level\">\r\n <ng-container *ngFor=\"let item of items\">\r\n <div class=\"menu-item-container\" [class.has-children]=\"hasChildren(item)\" [attr.data-level]=\"level\">\r\n <a \r\n [routerLink]=\"hasChildren(item) ? null : item.path\" \r\n routerLinkActive=\"active\" \r\n class=\"nav-item submenu-item\"\r\n [class.expandable]=\"hasChildren(item)\"\r\n (click)=\"onMenuItemClick($event, item)\">\r\n <span class=\"material-symbols-outlined nav-icon\">{{ getMenuIcon(item) }}</span>\r\n <span class=\"nav-text\">{{ item.name }}</span>\r\n <span class=\"material-symbols-outlined expand-icon\" *ngIf=\"hasChildren(item)\">\r\n {{ isMenuItemOpen(item.code) ? 'expand_more' : 'chevron_right' }}\r\n </span>\r\n </a>\r\n\r\n <!-- Recursive Submenu (max 5 levels) -->\r\n <div class=\"submenu\" *ngIf=\"hasChildren(item) && isMenuItemOpen(item.code) && level < 5\">\r\n <ng-container *ngTemplateOutlet=\"menuTemplate; context: { items: item.children, level: level + 1 }\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </ng-template>\r\n\r\n <!-- Main Content -->\r\n <main class=\"dashboard-content\">\r\n <div class=\"content-wrapper\">\r\n <router-outlet></router-outlet>\r\n </div>\r\n </main>\r\n\r\n <!-- Footer -->\r\n <footer class=\"dashboard-footer\">\r\n <div class=\"footer-content\">\r\n <span class=\"footer-text\">\r\n {{ getConfig('footerText') }}\r\n <ng-container *ngIf=\"getConfig('showVersion')\">\r\n Versi\u00F3n {{ appVersion }}.\r\n </ng-container>\r\n Todos los derechos reservados.\r\n </span>\r\n </div>\r\n </footer>\r\n</div>\r\n", styles: [":host{display:block;width:100%;min-height:100vh}.dashboard{min-height:100vh;display:flex;flex-direction:column;background:var(--color-primary, #313945);font-family:var(--font-family-primary, \"Museo Sans\", sans-serif)}.dashboard-header{background:var(--color-primary, #313945);color:var(--color-white, #ffffff);padding:var(--spacing-2, .5rem) var(--spacing-4, 1rem);border-bottom:1px solid rgba(255,255,255,.1);display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:1000}@media(max-width:768px){.dashboard-header{flex-direction:column;gap:var(--spacing-2, .5rem);padding:var(--spacing-2, .5rem);align-items:stretch}}.header-mobile-top{display:none}@media(max-width:768px){.header-mobile-top{display:flex;justify-content:space-between;align-items:center;width:100%;gap:var(--spacing-2, .5rem)}}.ministry-logo-mobile{display:none}@media(max-width:768px){.ministry-logo-mobile{display:flex;align-items:center}.ministry-logo-mobile img{height:50px;width:auto;filter:brightness(0) invert(1)}}.mobile-title{display:none}@media(max-width:768px){.mobile-title{display:block;font-size:var(--font-size-md, 1rem);font-weight:var(--font-weight-bold, 700);color:var(--color-white, #ffffff);margin:0;text-align:right;flex:1}}.header-mobile-bottom{display:none}@media(max-width:768px){.header-mobile-bottom{display:flex;justify-content:space-between;align-items:center;width:100%;gap:var(--spacing-2, .5rem);padding-top:var(--spacing-1, .25rem)}}.header-desktop-layout{display:flex;justify-content:space-between;align-items:center;width:100%}@media(max-width:768px){.header-desktop-layout{display:none}}.header-left{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.header-right{display:flex;align-items:center}.ministry-logo{display:flex;align-items:center;gap:var(--spacing-3, .75rem)}.ministry-logo .logo-icon{display:flex;width:80px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.2))}.ministry-logo .logo-icon img{width:100%;height:auto}.ministry-logo .ministry-info h1{font-family:var(--font-family-primary, sans-serif);font-size:var(--font-size-xl, 1.25rem);font-weight:var(--font-weight-bold, 700);margin:0;color:var(--color-white, #ffffff);line-height:1.2}.ministry-logo .ministry-info .subtitle{font-size:var(--font-size-xs, .75rem);color:#ffffffd9;font-weight:var(--font-weight-medium, 500)}.hamburger-btn{display:none;flex-direction:column;justify-content:space-around;width:36px;height:36px;background:#ffffff1a;border:none;border-radius:var(--border-radius-md, .375rem);cursor:pointer;padding:6px;z-index:1003;transition:all .3s ease;flex-shrink:0}@media(max-width:768px){.hamburger-btn{display:flex}}.hamburger-btn .hamburger-line{width:100%;height:2px;background:var(--color-white, #ffffff);border-radius:2px;transition:all .3s ease;transform-origin:center}.hamburger-btn.active{background:#fff3}.hamburger-btn.active .hamburger-line:nth-child(1){transform:translateY(7px) rotate(45deg)}.hamburger-btn.active .hamburger-line:nth-child(2){opacity:0}.hamburger-btn.active .hamburger-line:nth-child(3){transform:translateY(-7px) rotate(-45deg)}.hamburger-btn:hover{background:#fff3}.user-profile{position:relative;display:flex;align-items:center}.user-profile .user-info-trigger{display:flex;align-items:center;gap:var(--spacing-2, .5rem);background:#ffffff1a;padding:var(--spacing-1, .25rem) var(--spacing-3, .75rem);border-radius:var(--border-radius-lg, .5rem);border:1px solid rgba(255,255,255,.2);cursor:pointer;transition:all .2s ease;color:inherit;font-family:inherit}.user-profile .user-info-trigger:hover{background:#ffffff26}.user-profile .user-avatar{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--color-white, #ffffff) 0%,#e2e8f0 100%);display:flex;align-items:center;justify-content:center;font-weight:var(--font-weight-bold, 700);color:var(--color-primary, #313945);font-size:var(--font-size-sm, .875rem);box-shadow:0 2px 6px #0000001a}.user-profile .user-details{display:flex;flex-direction:column}.user-profile .user-details .user-name{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-medium, 500);color:var(--color-white, #ffffff)}.user-profile .dropdown-icon{color:#fffc;font-size:18px;transition:transform .2s ease}.user-panel{position:absolute;top:calc(100% + var(--spacing-2, .5rem));right:0;min-width:320px;background:var(--color-white, #ffffff);border-radius:var(--border-radius-lg, .5rem);box-shadow:0 10px 40px #0003;border:1px solid var(--color-gray-200, #e9ecef);z-index:1100;overflow:hidden;animation:slideDown .2s ease}@media(max-width:768px){.user-panel{position:absolute;top:calc(100% + var(--spacing-1, .25rem));right:0;left:auto;min-width:280px;max-width:calc(100vw - 1rem);max-height:80vh;overflow-y:auto}}.user-panel-header{background:linear-gradient(135deg,#3c4557,#2a3142);padding:var(--spacing-3, .75rem) var(--spacing-4, 1rem)}.user-panel-header .panel-title{font-size:var(--font-size-md, 1rem);font-weight:var(--font-weight-bold, 700);color:var(--color-white, #ffffff)}.user-panel-content{padding:var(--spacing-3, .75rem)}.user-info-item{display:flex;align-items:flex-start;gap:var(--spacing-3, .75rem);padding:var(--spacing-2, .5rem) var(--spacing-3, .75rem);border-bottom:1px solid var(--color-gray-100, #f1f3f4)}.user-info-item:last-child{border-bottom:none}.user-info-item .info-icon{font-size:var(--font-size-lg, 1.125rem);flex-shrink:0;color:var(--color-primary, #313945)}.user-info-item .info-content{display:flex;flex-direction:column;gap:2px;flex:1}.user-info-item .info-content .info-label{font-size:var(--font-size-xs, .75rem);font-weight:var(--font-weight-semibold, 600);color:var(--color-gray-600, #5a6268);text-transform:uppercase;letter-spacing:.5px}.user-info-item .info-content .info-value{font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-medium, 500);color:var(--color-gray-900, #212529);word-break:break-word}@media(max-width:768px){.mobile-hidden{display:none!important}}.user-panel-footer{padding:var(--spacing-3, .75rem);background:var(--color-gray-50, #f8f9fa);border-top:1px solid var(--color-gray-200, #e9ecef)}.user-panel-footer .logout-btn-panel{width:100%;background:linear-gradient(135deg,#dc2626,#b91c1c);color:var(--color-white, #ffffff);border:none;padding:var(--spacing-3, .75rem);border-radius:var(--border-radius-md, .375rem);font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-bold, 700);cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:var(--spacing-2, .5rem);text-transform:uppercase}.user-panel-footer .logout-btn-panel:hover{background:linear-gradient(135deg,#b91c1c,#991b1b)}.nav-overlay{display:none}@media(max-width:768px){.nav-overlay{display:block;position:fixed;inset:0;background:#00000080;z-index:1001;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}}.dashboard-nav{background-color:#3c4557;box-shadow:0 2px 8px #0000001a}@media(max-width:768px){.dashboard-nav{position:fixed;top:0;left:0;bottom:0;width:280px;max-width:85%;transform:translate(-100%);z-index:1002;overflow-y:auto;box-shadow:2px 0 10px #0000004d;transition:transform .3s ease}.dashboard-nav.mobile-open{transform:translate(0)}}.dashboard-nav .nav-mobile-header{display:none}@media(max-width:768px){.dashboard-nav .nav-mobile-header{display:flex;justify-content:space-between;align-items:center;padding:var(--spacing-3, .75rem);background:var(--color-primary, #313945);border-bottom:1px solid rgba(255,255,255,.1);position:sticky;top:0;z-index:10}}.dashboard-nav .nav-mobile-logo{display:flex;align-items:center;gap:var(--spacing-2, .5rem)}.dashboard-nav .nav-mobile-logo img{width:36px;height:auto;filter:brightness(0) invert(1)}.dashboard-nav .nav-mobile-logo .nav-mobile-title{color:var(--color-white, #ffffff);font-size:var(--font-size-md, 1rem);font-weight:var(--font-weight-semibold, 600)}.dashboard-nav .nav-close-btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#ffffff1a;border:none;border-radius:var(--border-radius-md, .375rem);cursor:pointer;color:var(--color-white, #ffffff)}.dashboard-nav .nav-close-btn:hover{background:#fff3}.nav-container{display:flex;flex-wrap:wrap;align-items:center;gap:var(--spacing-1, .25rem);padding:var(--spacing-2, .5rem) var(--spacing-4, 1rem)}@media(max-width:768px){.nav-container{flex-direction:column;align-items:stretch;padding:var(--spacing-3, .75rem);gap:var(--spacing-1, .25rem)}}.nav-item{display:flex;align-items:center;gap:var(--spacing-2, .5rem);padding:var(--spacing-2, .5rem) var(--spacing-3, .75rem);color:var(--color-white, #ffffff);text-decoration:none;border-radius:var(--border-radius-md, .375rem);font-size:var(--font-size-sm, .875rem);font-weight:var(--font-weight-medium, 500);transition:all .2s ease;cursor:pointer;background:transparent;border:none;font-family:inherit;white-space:nowrap}.nav-item:hover{background:#ffffff1a}.nav-item.active{background:#fff3}.nav-item .nav-icon{font-size:18px;flex-shrink:0}.nav-item .nav-text{flex:1;text-align:left}.nav-item .expand-icon{font-size:16px;flex-shrink:0;margin-left:var(--spacing-1, .25rem)}@media(max-width:768px){.nav-item{width:100%;padding:var(--spacing-3, .75rem)}}.nav-separator{width:1px;height:20px;background:#fff3;margin:0 var(--spacing-1, .25rem);align-self:center}@media(max-width:768px){.nav-separator{width:100%;height:1px;margin:var(--spacing-2, .5rem) 0}}.dynamic-menu{display:flex;flex-wrap:wrap;gap:var(--spacing-1, .25rem);align-items:flex-start}@media(max-width:768px){.dynamic-menu{flex-direction:column;width:100%}}.menu-item-container{position:relative}@media(max-width:768px){.menu-item-container{width:100%}}@media(min-width:769px){.menu-item-container>.submenu{position:absolute;top:100%;left:0;min-width:220px;background:#3c4557;border-radius:var(--border-radius-md, .375rem);box-shadow:0 4px 12px #0000004d;padding:var(--spacing-2, .5rem) 0;z-index:1000;margin-top:var(--spacing-1, .25rem)}}@media(max-width:768px){.menu-item-container>.submenu{padding-left:var(--spacing-4, 1rem)}}@media(min-width:769px){.submenu{min-width:200px}}@media(max-width:768px){.submenu{width:100%}}.submenu .menu-item-container{width:100%}@media(min-width:769px){.submenu .menu-item-container>.submenu{position:absolute;top:0;left:100%;min-width:200px;background:#3c4557;border-radius:var(--border-radius-md, .375rem);box-shadow:0 4px 12px #0000004d;padding:var(--spacing-2, .5rem) 0;z-index:1001;margin-left:2px}}@media(max-width:768px){.submenu .menu-item-container>.submenu{padding-left:var(--spacing-4, 1rem)}}.submenu .submenu-item{padding:var(--spacing-2, .5rem) var(--spacing-4, 1rem);font-size:var(--font-size-sm, .875rem);width:100%;box-sizing:border-box}.submenu .submenu-item:hover{background:#ffffff1a}.menu-loading{display:flex;align-items:center;gap:var(--spacing-2, .5rem);color:#ffffffb3;padding:var(--spacing-2, .5rem) var(--spacing-3, .75rem);font-size:var(--font-size-sm, .875rem)}.dashboard-content{flex:1;background:var(--color-white, #ffffff)}.dashboard-content .content-wrapper{padding:var(--spacing-4, 1rem) var(--spacing-6, 1.5rem);max-width:1400px;margin:0 auto}@media(max-width:768px){.dashboard-content .content-wrapper{padding:var(--spacing-3, .75rem)}}.dashboard-footer{background:#3c4557;color:#c5c8cf;padding:var(--spacing-3, .75rem) var(--spacing-4, 1rem);border-top:1px solid rgba(255,255,255,.1)}.dashboard-footer .footer-content{text-align:center}.dashboard-footer .footer-text{font-size:var(--font-size-xs, .75rem)}@media(min-width:769px){.mobile-user{display:none}}@media(max-width:768px){.desktop-user{display:none}}@keyframes slideDown{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}\n"] }]
1550
- }], ctorParameters: () => [{ type: AuthService }, { type: AuthorizationService }], propDecorators: { config: [{
1551
- type: Input
1552
- }], onDocumentClick: [{
1413
+ }], ctorParameters: () => [{ type: AuthService }, { type: AuthorizationService }], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], onDocumentClick: [{
1553
1414
  type: HostListener,
1554
1415
  args: ['document:click', ['$event']]
1555
1416
  }] } });
@@ -1564,5 +1425,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1564
1425
  * Generated bundle index. Do not edit.
1565
1426
  */
1566
1427
 
1567
- export { AuthService, AuthorizationService, ConfigService, DEFAULT_DASHBOARD_CONFIG, DEFAULT_LIBRARY_CONFIG, DEFAULT_LOGIN_CONFIG, DashboardComponent, FRMK_LIBRARY_CONFIG, LoginComponent, VERSION, VERSION_INFO, authGuard, getAppConfig, getAppConfigWithServerData, getAuthConfig, getAuthConfigEnv, getCurrentAppConfig, getDashboardConfig, getDefaultAppConfig, getLibraryConfig, getLoginConfig, initializeLibraryConfig, isLibraryConfigured, mapConfigServerResponse, provideFrmkConfig, setCurrentAppConfig };
1428
+ export { AuthService, AuthorizationService, ConfigService, DEFAULT_DASHBOARD_CONFIG, DEFAULT_LIBRARY_CONFIG, DEFAULT_LOGIN_CONFIG, DashboardComponent, FrmkConfigStore, LoginComponent, VERSION, VERSION_INFO, authGuard };
1568
1429
  //# sourceMappingURL=shared-lib-angular.mjs.map