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