shared-lib-angular 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/shared-lib-angular.mjs +169 -85
- package/fesm2022/shared-lib-angular.mjs.map +1 -1
- package/index.d.ts +64 -15
- package/package.json +1 -1
|
@@ -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 = '2.0.
|
|
23
|
+
const VERSION = '2.0.4';
|
|
24
24
|
/**
|
|
25
25
|
* Información completa de la versión
|
|
26
26
|
*/
|
|
27
27
|
const VERSION_INFO = {
|
|
28
|
-
version: '2.0.
|
|
28
|
+
version: '2.0.4',
|
|
29
29
|
name: 'shared-lib-angular',
|
|
30
|
-
buildDate: '2026-02-
|
|
30
|
+
buildDate: '2026-02-09T22:43:40.953Z',
|
|
31
31
|
angular: '^20.0.0'
|
|
32
32
|
};
|
|
33
33
|
|
|
@@ -340,19 +340,160 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
340
340
|
args: [{ providedIn: 'root' }]
|
|
341
341
|
}] });
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Keys de tokens OAuth/OIDC manejados por angular-oauth2-oidc y Keycloak.
|
|
345
|
+
* Única fuente de verdad — elimina listas duplicadas e inconsistentes.
|
|
346
|
+
*/
|
|
347
|
+
const TOKEN_KEYS = [
|
|
348
|
+
'access_token',
|
|
349
|
+
'refresh_token',
|
|
350
|
+
'id_token',
|
|
351
|
+
'token_type',
|
|
352
|
+
'expires_at',
|
|
353
|
+
'nonce',
|
|
354
|
+
'PKCE_verifier',
|
|
355
|
+
'code_verifier',
|
|
356
|
+
'session_state',
|
|
357
|
+
'id_token_claims_obj',
|
|
358
|
+
'id_token_expires_at',
|
|
359
|
+
'id_token_stored_at',
|
|
360
|
+
'access_token_stored_at'
|
|
361
|
+
];
|
|
362
|
+
/**
|
|
363
|
+
* Keys de estado OAuth que deben preservarse durante un callback
|
|
364
|
+
* (necesarias para validar code + state en el intercambio PKCE).
|
|
365
|
+
*/
|
|
366
|
+
const STATE_KEYS = new Set([
|
|
367
|
+
'nonce',
|
|
368
|
+
'PKCE_verifier',
|
|
369
|
+
'code_verifier',
|
|
370
|
+
'session_state',
|
|
371
|
+
'state'
|
|
372
|
+
]);
|
|
373
|
+
/**
|
|
374
|
+
* Servicio centralizado para gestión de tokens en localStorage / sessionStorage.
|
|
375
|
+
*
|
|
376
|
+
* - Elimina las 4+ listas de keys duplicadas e inconsistentes que existían en AuthService.
|
|
377
|
+
* - Opera simétricamente sobre ambos storages.
|
|
378
|
+
* - Soporta la key dinámica `refresh_token_{clientId}` que usa angular-oauth2-oidc.
|
|
379
|
+
* - Expone métodos públicos para que la plantilla pueda leer tokens sin acceder a storage directamente.
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* // Desde la librería (AuthService):
|
|
383
|
+
* this.tokenStorage.clearAll();
|
|
384
|
+
*
|
|
385
|
+
* // Desde la plantilla (skeleton):
|
|
386
|
+
* const expired = this.tokenStorage.isTokenExpired();
|
|
387
|
+
*/
|
|
388
|
+
class TokenStorageService {
|
|
389
|
+
// ── Limpieza ──────────────────────────────────────────────────────
|
|
390
|
+
/**
|
|
391
|
+
* Limpia TODOS los tokens y estado OAuth de ambos storages.
|
|
392
|
+
* Incluye la key dinámica `refresh_token_{clientId}` si se proporciona.
|
|
393
|
+
*
|
|
394
|
+
* @param clientId — clientId de OAuth para limpiar `refresh_token_{clientId}` (opcional)
|
|
395
|
+
*/
|
|
396
|
+
clearAll(clientId) {
|
|
397
|
+
TOKEN_KEYS.forEach(key => {
|
|
398
|
+
localStorage.removeItem(key);
|
|
399
|
+
sessionStorage.removeItem(key);
|
|
400
|
+
});
|
|
401
|
+
// Key dinámica de angular-oauth2-oidc
|
|
402
|
+
if (clientId) {
|
|
403
|
+
localStorage.removeItem(`refresh_token_${clientId}`);
|
|
404
|
+
sessionStorage.removeItem(`refresh_token_${clientId}`);
|
|
405
|
+
}
|
|
406
|
+
// También limpiar 'state' que no está en TOKEN_KEYS pero puede persistir
|
|
407
|
+
localStorage.removeItem('state');
|
|
408
|
+
sessionStorage.removeItem('state');
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Limpia tokens pero MANTIENE las keys de estado OAuth necesarias para
|
|
412
|
+
* validar el callback (nonce, PKCE_verifier, code_verifier, session_state, state).
|
|
413
|
+
*
|
|
414
|
+
* Usar durante un callback OAuth antes de procesar el authorization code.
|
|
415
|
+
*/
|
|
416
|
+
clearTokensKeepState() {
|
|
417
|
+
TOKEN_KEYS.forEach(key => {
|
|
418
|
+
if (!STATE_KEYS.has(key)) {
|
|
419
|
+
localStorage.removeItem(key);
|
|
420
|
+
sessionStorage.removeItem(key);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
console.log('[TokenStorageService] Cleared tokens, kept state/nonce/PKCE for callback validation');
|
|
424
|
+
}
|
|
425
|
+
// ── Lectura ───────────────────────────────────────────────────────
|
|
426
|
+
/**
|
|
427
|
+
* Lee un valor de localStorage o sessionStorage (en ese orden de prioridad).
|
|
428
|
+
*/
|
|
429
|
+
getItem(key) {
|
|
430
|
+
return localStorage.getItem(key) ?? sessionStorage.getItem(key);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Obtiene el refresh_token, incluyendo la variante dinámica `refresh_token_{clientId}`.
|
|
434
|
+
*
|
|
435
|
+
* @param clientId — clientId de OAuth (opcional)
|
|
436
|
+
*/
|
|
437
|
+
getRefreshToken(clientId) {
|
|
438
|
+
const token = this.getItem('refresh_token');
|
|
439
|
+
if (token)
|
|
440
|
+
return token;
|
|
441
|
+
if (clientId) {
|
|
442
|
+
return localStorage.getItem(`refresh_token_${clientId}`)
|
|
443
|
+
?? sessionStorage.getItem(`refresh_token_${clientId}`)
|
|
444
|
+
?? null;
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Obtiene el access_token de cualquier storage.
|
|
450
|
+
*/
|
|
451
|
+
getAccessToken() {
|
|
452
|
+
return this.getItem('access_token');
|
|
453
|
+
}
|
|
454
|
+
// ── Estado ────────────────────────────────────────────────────────
|
|
455
|
+
/**
|
|
456
|
+
* Verifica si hay un token almacenado y está expirado.
|
|
457
|
+
* Retorna `false` si no existe `expires_at` (no hay token, no está "expirado").
|
|
458
|
+
*/
|
|
459
|
+
isTokenExpired() {
|
|
460
|
+
const expiresAt = this.getItem('expires_at');
|
|
461
|
+
if (!expiresAt) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
return parseInt(expiresAt, 10) < Date.now();
|
|
465
|
+
}
|
|
466
|
+
// ── Utilidades ────────────────────────────────────────────────────
|
|
467
|
+
/**
|
|
468
|
+
* Elimina una key específica de ambos storages.
|
|
469
|
+
*/
|
|
470
|
+
removeItem(key) {
|
|
471
|
+
localStorage.removeItem(key);
|
|
472
|
+
sessionStorage.removeItem(key);
|
|
473
|
+
}
|
|
474
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: TokenStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
475
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: TokenStorageService, providedIn: 'root' });
|
|
476
|
+
}
|
|
477
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: TokenStorageService, decorators: [{
|
|
478
|
+
type: Injectable,
|
|
479
|
+
args: [{ providedIn: 'root' }]
|
|
480
|
+
}] });
|
|
481
|
+
|
|
343
482
|
class AuthService {
|
|
344
483
|
oauthService;
|
|
345
484
|
router;
|
|
346
485
|
store;
|
|
486
|
+
tokenStorage;
|
|
347
487
|
isAuthenticatedSubject$ = new BehaviorSubject(false);
|
|
348
488
|
isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();
|
|
349
489
|
isDoneLoadingSubject$ = new BehaviorSubject(false);
|
|
350
490
|
isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();
|
|
351
491
|
isInitialLogin = true;
|
|
352
|
-
constructor(oauthService, router, store) {
|
|
492
|
+
constructor(oauthService, router, store, tokenStorage) {
|
|
353
493
|
this.oauthService = oauthService;
|
|
354
494
|
this.router = router;
|
|
355
495
|
this.store = store;
|
|
496
|
+
this.tokenStorage = tokenStorage;
|
|
356
497
|
// IMPORTANTE: Limpiar tokens expirados ANTES de cualquier otra operación
|
|
357
498
|
this.clearExpiredTokensOnStartup();
|
|
358
499
|
this.oauthService.events
|
|
@@ -392,48 +533,11 @@ class AuthService {
|
|
|
392
533
|
return;
|
|
393
534
|
}
|
|
394
535
|
// Verificar si hay un token y si está expirado
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const now = Date.now();
|
|
399
|
-
if (expirationTime < now) {
|
|
400
|
-
console.log('[AuthService] Expired token detected, clearing storage');
|
|
401
|
-
this.clearAllTokens();
|
|
402
|
-
}
|
|
536
|
+
if (this.tokenStorage.isTokenExpired()) {
|
|
537
|
+
console.log('[AuthService] Expired token detected, clearing storage');
|
|
538
|
+
this.tokenStorage.clearAll();
|
|
403
539
|
}
|
|
404
540
|
}
|
|
405
|
-
/**
|
|
406
|
-
* Limpia todos los tokens del storage
|
|
407
|
-
*/
|
|
408
|
-
clearAllTokens() {
|
|
409
|
-
const keysToRemove = [
|
|
410
|
-
'access_token', 'refresh_token', 'id_token', 'token_type',
|
|
411
|
-
'expires_at', 'nonce', 'PKCE_verifier', 'session_state',
|
|
412
|
-
'id_token_claims_obj', 'id_token_expires_at', 'id_token_stored_at',
|
|
413
|
-
'access_token_stored_at'
|
|
414
|
-
];
|
|
415
|
-
keysToRemove.forEach(key => {
|
|
416
|
-
localStorage.removeItem(key);
|
|
417
|
-
sessionStorage.removeItem(key);
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Limpia tokens antiguos pero MANTIENE state, nonce y PKCE_verifier
|
|
422
|
-
* que son necesarios para validar el callback OAuth
|
|
423
|
-
*/
|
|
424
|
-
clearOldTokensKeepState() {
|
|
425
|
-
// Solo limpiar tokens, NO state/nonce/PKCE_verifier
|
|
426
|
-
const keysToRemove = [
|
|
427
|
-
'access_token', 'refresh_token', 'id_token', 'token_type',
|
|
428
|
-
'expires_at', 'id_token_claims_obj', 'id_token_expires_at',
|
|
429
|
-
'id_token_stored_at', 'access_token_stored_at'
|
|
430
|
-
];
|
|
431
|
-
keysToRemove.forEach(key => {
|
|
432
|
-
localStorage.removeItem(key);
|
|
433
|
-
sessionStorage.removeItem(key);
|
|
434
|
-
});
|
|
435
|
-
console.log('[AuthService] Cleared old tokens, kept state/nonce/PKCE for callback validation');
|
|
436
|
-
}
|
|
437
541
|
handleSuccessfulAuthentication() {
|
|
438
542
|
if (this.oauthService.hasValidAccessToken()) {
|
|
439
543
|
this.isAuthenticatedSubject$.next(true);
|
|
@@ -484,28 +588,23 @@ class AuthService {
|
|
|
484
588
|
this.isDoneLoadingSubject$.next(true);
|
|
485
589
|
}
|
|
486
590
|
/**
|
|
487
|
-
* Limpia todos los tokens del storage
|
|
591
|
+
* Limpia todos los tokens del storage y resetea OAuthService
|
|
488
592
|
*/
|
|
489
593
|
clearTokenStorage() {
|
|
490
594
|
this.oauthService.logOut(true); // true = no redirect, solo limpiar
|
|
491
|
-
|
|
492
|
-
localStorage.removeItem('refresh_token');
|
|
493
|
-
localStorage.removeItem('id_token');
|
|
494
|
-
localStorage.removeItem('nonce');
|
|
495
|
-
localStorage.removeItem('expires_at');
|
|
496
|
-
sessionStorage.removeItem('access_token');
|
|
497
|
-
sessionStorage.removeItem('refresh_token');
|
|
498
|
-
sessionStorage.removeItem('id_token');
|
|
595
|
+
this.tokenStorage.clearAll();
|
|
499
596
|
}
|
|
500
597
|
/**
|
|
501
598
|
* Verifica si el token actual está expirado
|
|
502
599
|
*/
|
|
503
600
|
isTokenExpired() {
|
|
601
|
+
// Primero verificar vía OAuthService (tokens en memoria)
|
|
504
602
|
const expiresAt = this.oauthService.getAccessTokenExpiration();
|
|
505
|
-
if (
|
|
506
|
-
return
|
|
603
|
+
if (expiresAt) {
|
|
604
|
+
return expiresAt < Date.now();
|
|
507
605
|
}
|
|
508
|
-
|
|
606
|
+
// Fallback: verificar en storage
|
|
607
|
+
return this.tokenStorage.isTokenExpired();
|
|
509
608
|
}
|
|
510
609
|
async runInitialLoginSequence() {
|
|
511
610
|
try {
|
|
@@ -529,7 +628,7 @@ class AuthService {
|
|
|
529
628
|
if (isOAuthCallback) {
|
|
530
629
|
console.log('[AuthService] OAuth callback detected, clearing old tokens before processing');
|
|
531
630
|
// Limpiar solo tokens, NO el state/nonce que necesitamos para validar el callback
|
|
532
|
-
this.
|
|
631
|
+
this.tokenStorage.clearTokensKeepState();
|
|
533
632
|
this.isInitialLogin = true;
|
|
534
633
|
await this.oauthService.tryLoginCodeFlow();
|
|
535
634
|
}
|
|
@@ -571,18 +670,16 @@ class AuthService {
|
|
|
571
670
|
if (hasCode) {
|
|
572
671
|
console.log('[AuthService] OAuth callback detected: resetting OAuthService state');
|
|
573
672
|
// Limpiar storage EXCEPTO state/nonce/PKCE
|
|
574
|
-
this.
|
|
673
|
+
this.tokenStorage.clearTokensKeepState();
|
|
575
674
|
// CRÍTICO: Resetear propiedades internas de OAuthService
|
|
576
675
|
// para que no valide tokens viejos en memoria
|
|
577
676
|
this.resetOAuthServiceInternalState();
|
|
578
677
|
}
|
|
579
678
|
else {
|
|
580
679
|
// No es callback - verificar si hay tokens expirados
|
|
581
|
-
|
|
582
|
-
const isExpired = expiresAt && parseInt(expiresAt, 10) < Date.now();
|
|
583
|
-
if (isExpired) {
|
|
680
|
+
if (this.tokenStorage.isTokenExpired()) {
|
|
584
681
|
console.log('[AuthService] Clearing all expired tokens before configure');
|
|
585
|
-
this.
|
|
682
|
+
this.tokenStorage.clearAll();
|
|
586
683
|
this.resetOAuthServiceInternalState();
|
|
587
684
|
}
|
|
588
685
|
}
|
|
@@ -640,9 +737,7 @@ class AuthService {
|
|
|
640
737
|
const logoutUrl = authConfig.logoutUrl;
|
|
641
738
|
let refreshToken = this.oauthService.getRefreshToken();
|
|
642
739
|
if (!refreshToken) {
|
|
643
|
-
refreshToken =
|
|
644
|
-
|| localStorage.getItem('refresh_token_' + authConfig.clientId)
|
|
645
|
-
|| sessionStorage.getItem('refresh_token');
|
|
740
|
+
refreshToken = this.tokenStorage.getRefreshToken(authConfig.clientId);
|
|
646
741
|
}
|
|
647
742
|
if (!logoutUrl) {
|
|
648
743
|
console.warn('Logout URL not configured, skipping Keycloak logout');
|
|
@@ -675,20 +770,9 @@ class AuthService {
|
|
|
675
770
|
}
|
|
676
771
|
}
|
|
677
772
|
performLocalLogout() {
|
|
773
|
+
const authConfig = this.store.authConfig();
|
|
678
774
|
this.oauthService.logOut(true);
|
|
679
|
-
|
|
680
|
-
localStorage.removeItem('access_token');
|
|
681
|
-
localStorage.removeItem('id_token');
|
|
682
|
-
localStorage.removeItem('refresh_token');
|
|
683
|
-
localStorage.removeItem('code_verifier');
|
|
684
|
-
localStorage.removeItem('nonce');
|
|
685
|
-
localStorage.removeItem('PKCE_verifier');
|
|
686
|
-
// Limpiar tokens del sessionStorage
|
|
687
|
-
sessionStorage.removeItem('access_token');
|
|
688
|
-
sessionStorage.removeItem('id_token');
|
|
689
|
-
sessionStorage.removeItem('refresh_token');
|
|
690
|
-
sessionStorage.removeItem('nonce');
|
|
691
|
-
sessionStorage.removeItem('PKCE_verifier');
|
|
775
|
+
this.tokenStorage.clearAll(authConfig.clientId);
|
|
692
776
|
this.isAuthenticatedSubject$.next(false);
|
|
693
777
|
this.isDoneLoadingSubject$.next(true);
|
|
694
778
|
const currentUrl = this.router.url;
|
|
@@ -765,7 +849,7 @@ class AuthService {
|
|
|
765
849
|
if (accessToken) {
|
|
766
850
|
try {
|
|
767
851
|
const payload = this.decodeJwtPayload(accessToken);
|
|
768
|
-
console.log('[AuthService] Access token payload:', payload);
|
|
852
|
+
// console.log('[AuthService] Access token payload:', payload);
|
|
769
853
|
// Extract groups from access token
|
|
770
854
|
if (payload.groups) {
|
|
771
855
|
if (Array.isArray(payload.groups)) {
|
|
@@ -794,7 +878,7 @@ class AuthService {
|
|
|
794
878
|
}
|
|
795
879
|
}
|
|
796
880
|
const uniqueGroups = [...new Set(groups)];
|
|
797
|
-
console.log('[AuthService] getUserGroups result:', uniqueGroups);
|
|
881
|
+
// console.log('[AuthService] getUserGroups result:', uniqueGroups);
|
|
798
882
|
return uniqueGroups;
|
|
799
883
|
}
|
|
800
884
|
decodeJwtPayload(token) {
|
|
@@ -812,7 +896,7 @@ class AuthService {
|
|
|
812
896
|
return {};
|
|
813
897
|
}
|
|
814
898
|
}
|
|
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 });
|
|
899
|
+
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 }, { token: TokenStorageService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
816
900
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AuthService, providedIn: 'root' });
|
|
817
901
|
}
|
|
818
902
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AuthService, decorators: [{
|
|
@@ -820,7 +904,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
820
904
|
args: [{
|
|
821
905
|
providedIn: 'root'
|
|
822
906
|
}]
|
|
823
|
-
}], ctorParameters: () => [{ type: i1.OAuthService }, { type: i4.Router }, { type: FrmkConfigStore }] });
|
|
907
|
+
}], ctorParameters: () => [{ type: i1.OAuthService }, { type: i4.Router }, { type: FrmkConfigStore }, { type: TokenStorageService }] });
|
|
824
908
|
|
|
825
909
|
class ConfigService {
|
|
826
910
|
http;
|
|
@@ -1405,11 +1489,11 @@ class DashboardComponent {
|
|
|
1405
1489
|
document.body.style.overflow = '';
|
|
1406
1490
|
}
|
|
1407
1491
|
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 });
|
|
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"] }] });
|
|
1492
|
+
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\" (click)=\"toggleMobileMenu()\" [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\" (click)=\"toggleUserPanel($event)\" [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\" (click)=\"toggleUserPanel($event)\" [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=\".\" routerLinkActive=\"active\" [routerLinkActiveOptions]=\"{exact: true}\" 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 [routerLink]=\"hasChildren(rootItem) ? null : rootItem.path\" routerLinkActive=\"active\"\r\n class=\"nav-item root-item\" [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\r\n *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 [routerLink]=\"hasChildren(item) ? null : item.path\" routerLinkActive=\"active\" class=\"nav-item submenu-item\"\r\n [class.expandable]=\"hasChildren(item)\" (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\r\n *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>", 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"] }] });
|
|
1409
1493
|
}
|
|
1410
1494
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DashboardComponent, decorators: [{
|
|
1411
1495
|
type: Component,
|
|
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"] }]
|
|
1496
|
+
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\" (click)=\"toggleMobileMenu()\" [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\" (click)=\"toggleUserPanel($event)\" [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\" (click)=\"toggleUserPanel($event)\" [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=\".\" routerLinkActive=\"active\" [routerLinkActiveOptions]=\"{exact: true}\" 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 [routerLink]=\"hasChildren(rootItem) ? null : rootItem.path\" routerLinkActive=\"active\"\r\n class=\"nav-item root-item\" [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\r\n *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 [routerLink]=\"hasChildren(item) ? null : item.path\" routerLinkActive=\"active\" class=\"nav-item submenu-item\"\r\n [class.expandable]=\"hasChildren(item)\" (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\r\n *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>", 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"] }]
|
|
1413
1497
|
}], ctorParameters: () => [{ type: AuthService }, { type: AuthorizationService }], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], onDocumentClick: [{
|
|
1414
1498
|
type: HostListener,
|
|
1415
1499
|
args: ['document:click', ['$event']]
|
|
@@ -1425,5 +1509,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
1425
1509
|
* Generated bundle index. Do not edit.
|
|
1426
1510
|
*/
|
|
1427
1511
|
|
|
1428
|
-
export { AuthService, AuthorizationService, ConfigService, DEFAULT_DASHBOARD_CONFIG, DEFAULT_LIBRARY_CONFIG, DEFAULT_LOGIN_CONFIG, DashboardComponent, FrmkConfigStore, LoginComponent, VERSION, VERSION_INFO, authGuard };
|
|
1512
|
+
export { AuthService, AuthorizationService, ConfigService, DEFAULT_DASHBOARD_CONFIG, DEFAULT_LIBRARY_CONFIG, DEFAULT_LOGIN_CONFIG, DashboardComponent, FrmkConfigStore, LoginComponent, TokenStorageService, VERSION, VERSION_INFO, authGuard };
|
|
1429
1513
|
//# sourceMappingURL=shared-lib-angular.mjs.map
|