utn-cli 2.1.30 → 2.1.32
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/commands/backend.js +3 -2
- package/commands/frontend.js +37 -2
- package/package.json +1 -1
- package/templates/backend/InformacionDelModulo.json +0 -1
- package/templates/backend/rutas/misc.js +35 -0
- package/templates/backend/servicios/Nucleo/Miscelaneas.js +80 -6
- package/templates/frontend/src/app/Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component.css +44 -0
- package/templates/frontend/src/app/Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component.html +22 -0
- package/templates/frontend/src/app/Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component.ts +21 -0
- package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.ts +7 -3
- package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.css +22 -1
- package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.html +5 -0
- package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.ts +12 -0
- package/templates/frontend/src/app/Componentes/Nucleo/mensaje-institucional/mensaje-institucional.component.css +55 -0
- package/templates/frontend/src/app/Componentes/Nucleo/mensaje-institucional/mensaje-institucional.component.html +23 -0
- package/templates/frontend/src/app/Componentes/Nucleo/mensaje-institucional/mensaje-institucional.component.ts +19 -0
- package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.html +16 -14
- package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.ts +10 -0
- package/templates/frontend/src/app/datos-globales.service.ts +4 -1
- package/templates/frontend/src/index.html +2 -1
package/commands/backend.js
CHANGED
|
@@ -73,7 +73,8 @@ async function inicializarProyectoBackend() {
|
|
|
73
73
|
reemplazarContenidoEnArchivo(rutaDeDB, 'NOMBRE_DEL_REPOSITORIO_DE_BASE_DE_DATOS', val.slice(0, -3));
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
const NOMBRE_DEL_REPOSITORIO_DE_BACKEND = await askAndReplace('NOMBRE_DEL_REPOSITORIO_DE_BACKEND', '¿Cuál es el nombre del repositorio de backend?: '
|
|
76
|
+
const NOMBRE_DEL_REPOSITORIO_DE_BACKEND = await askAndReplace('NOMBRE_DEL_REPOSITORIO_DE_BACKEND', '¿Cuál es el nombre del repositorio de backend?: '
|
|
77
|
+
, null, 'NOMBRE_DEL_REPOSITORIO_DE_BACKEND', (val) => {
|
|
77
78
|
const rutaPackage = path.join(process.cwd(), 'package.json');
|
|
78
79
|
reemplazarContenidoEnArchivo(rutaPackage, 'nombre_del_repositorio_backend', val);
|
|
79
80
|
const rutaDeserver = path.join(process.cwd(), 'server.js');
|
|
@@ -91,7 +92,7 @@ async function inicializarProyectoBackend() {
|
|
|
91
92
|
reemplazarContenidoEnArchivo(path.join(process.cwd(), 'package.json'), 'nombre_de_desarrollador', `Para la Universidad Técnica Nacional por: ${val}`);
|
|
92
93
|
});
|
|
93
94
|
|
|
94
|
-
await askAndReplace('CEDULA_DEL_DESARROLLADOR', '¿Cuál es el número de cédula del desarrollador?: ', rutaDeInformacionDelModulo, 'CEDULA_DEL_DESARROLLADOR');
|
|
95
|
+
// await askAndReplace('CEDULA_DEL_DESARROLLADOR', '¿Cuál es el número de cédula del desarrollador?: ', rutaDeInformacionDelModulo, 'CEDULA_DEL_DESARROLLADOR');
|
|
95
96
|
|
|
96
97
|
const url_del_grupo = await askAndReplace('url_del_grupo', '¿Cuál es la URL del grupo en el Git?: ', null, 'url_del_grupo', (val) => {
|
|
97
98
|
reemplazarContenidoEnArchivo(path.join(process.cwd(), 'package.json'), 'url_del_grupo', val);
|
package/commands/frontend.js
CHANGED
|
@@ -55,14 +55,16 @@ export async function updateFrontend(opciones = { cerrarAlFinalizar: true }) {
|
|
|
55
55
|
// Copiar archivos base
|
|
56
56
|
copiarDirectorios(directoriodePlantillas, directorioDestino, archivosAExcluir);
|
|
57
57
|
|
|
58
|
-
// Copiar AccionesPersonalizadas.ts solo si no existe en el destino
|
|
58
|
+
// Copiar AccionesPersonalizadas.ts solo si no existe en el destino; si existe, asegurar referencias de MatDialog
|
|
59
59
|
const rutaAccionesDestino = path.join(directorioDestino, 'src', 'app', 'Paginas', 'contenedor-principal', 'AccionesPersonalizadas.ts');
|
|
60
|
+
const rutaAccionesTemplate = path.join(directoriodePlantillas, 'src', 'app', 'Paginas', 'contenedor-principal', 'AccionesPersonalizadas.ts');
|
|
60
61
|
if (!fs.existsSync(rutaAccionesDestino)) {
|
|
61
|
-
const rutaAccionesTemplate = path.join(directoriodePlantillas, 'src', 'app', 'Paginas', 'contenedor-principal', 'AccionesPersonalizadas.ts');
|
|
62
62
|
if (fs.existsSync(rutaAccionesTemplate)) {
|
|
63
63
|
fs.copyFileSync(rutaAccionesTemplate, rutaAccionesDestino);
|
|
64
64
|
console.log('AccionesPersonalizadas.ts no encontrado, se copió desde la plantilla.');
|
|
65
65
|
}
|
|
66
|
+
} else {
|
|
67
|
+
mergearAccionesPersonalizadas(rutaAccionesDestino);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
// Merge de app.routes.ts
|
|
@@ -90,6 +92,39 @@ export async function updateFrontend(opciones = { cerrarAlFinalizar: true }) {
|
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
function mergearAccionesPersonalizadas(rutaArchivo) {
|
|
96
|
+
let contenido = fs.readFileSync(rutaArchivo, 'utf-8');
|
|
97
|
+
let modificado = false;
|
|
98
|
+
|
|
99
|
+
const importMatDialog = `import { MatDialog } from '@angular/material/dialog';`;
|
|
100
|
+
if (!contenido.includes(importMatDialog)) {
|
|
101
|
+
// Insertar después del último import existente
|
|
102
|
+
const ultimoImport = contenido.lastIndexOf("import {");
|
|
103
|
+
if (ultimoImport !== -1) {
|
|
104
|
+
const finLinea = contenido.indexOf('\n', ultimoImport);
|
|
105
|
+
contenido = contenido.slice(0, finLinea + 1) + importMatDialog + '\n' + contenido.slice(finLinea + 1);
|
|
106
|
+
} else {
|
|
107
|
+
contenido = importMatDialog + '\n' + contenido;
|
|
108
|
+
}
|
|
109
|
+
modificado = true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const propiedadDialog = ` dialog: MatDialog;`;
|
|
113
|
+
if (!contenido.includes(propiedadDialog)) {
|
|
114
|
+
// Insertar antes del cierre de la interfaz DependenciasDeAccion
|
|
115
|
+
const cierreInterfaz = contenido.indexOf('}', contenido.indexOf('DependenciasDeAccion'));
|
|
116
|
+
if (cierreInterfaz !== -1) {
|
|
117
|
+
contenido = contenido.slice(0, cierreInterfaz) + propiedadDialog + '\n' + contenido.slice(cierreInterfaz);
|
|
118
|
+
modificado = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (modificado) {
|
|
123
|
+
fs.writeFileSync(rutaArchivo, contenido);
|
|
124
|
+
console.log('AccionesPersonalizadas.ts: referencias de MatDialog agregadas.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
93
128
|
function mergeRoutesFrontend(rutaDestino, rutaFuente) {
|
|
94
129
|
let contenidoDestino = fs.readFileSync(rutaDestino, 'utf-8');
|
|
95
130
|
const contenidoFuente = fs.readFileSync(rutaFuente, 'utf-8');
|
package/package.json
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"DESCRIPCION_DEL_PERMISO": "DESCRIPCION_DEL_PERMISO",
|
|
11
11
|
"CORREO_PARA_REPORTES": "CORREO_PARA_REPORTES",
|
|
12
12
|
"nombre_de_desarrollador": "nombre_de_desarrollador",
|
|
13
|
-
"CEDULA_DEL_DESARROLLADOR": "CEDULA_DEL_DESARROLLADOR",
|
|
14
13
|
"url_del_grupo": "url_del_grupo",
|
|
15
14
|
"BACKENDS_QUE_CONSUME_ESTE_MODULO": "BACKENDS_QUE_CONSUME_ESTE_MODULO",
|
|
16
15
|
"VERSION": "VERSION"
|
|
@@ -227,6 +227,41 @@ Router.get('/VistaDelManual', async (solicitud, respuesta, next) => {
|
|
|
227
227
|
}
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
+
Router.get('/VistaDelModulo', async (solicitud, respuesta, next) => {
|
|
231
|
+
try {
|
|
232
|
+
if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
|
|
233
|
+
try {
|
|
234
|
+
await Miscelaneo.registrarVistaDelModulo(solicitud.headers.authorization);
|
|
235
|
+
return respuesta.json({ body: undefined, error: undefined });
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const MensajeDeError = 'No fue posible registrar la vista del módulo';
|
|
238
|
+
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
|
|
239
|
+
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
next(error);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
Router.get('/obtenerVistas', async (solicitud, respuesta, next) => {
|
|
249
|
+
try {
|
|
250
|
+
if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
|
|
251
|
+
try {
|
|
252
|
+
return respuesta.json({ body: await Miscelaneo.obtenerVistas(), error: undefined });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
const MensajeDeError = 'No fue posible obtener las vistas';
|
|
255
|
+
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
|
|
256
|
+
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
next(error);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
230
265
|
Router.get('/obtenerDatosDelUsuario', async (solicitud, respuesta, next) => {
|
|
231
266
|
try {
|
|
232
267
|
if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
|
|
@@ -24,6 +24,7 @@ class Miscelaneo {
|
|
|
24
24
|
this.MenuPadre = InformacionDelModulo.getMenuPadre();
|
|
25
25
|
this.BackEndsQueConsumeEsteModulo = InformacionDelModulo.getBackEndsQueConsumeEsteModulo();
|
|
26
26
|
// this.PerfilGeneral = InformacionDelModulo.getPerfilGeneral();
|
|
27
|
+
this.UsuariosConAccesoInicial = InformacionDelModulo.getUsuariosConAccesoInicial();
|
|
27
28
|
this.IconoDelModulo = this.NombreCanonicoDelModulo + '.svg';
|
|
28
29
|
// this.ColorDelModulo = InformacionDelModulo.getColorDelModulo();
|
|
29
30
|
this.CorreoParaReportes = InformacionDelModulo.getCorreoParaReportes();
|
|
@@ -191,11 +192,15 @@ class Miscelaneo {
|
|
|
191
192
|
, [Permiso, this.NombreCanonicoDelModulo, Descripcion, Permiso, Descripcion]);
|
|
192
193
|
|
|
193
194
|
// Asginación inicial de permisos
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
if (this.UsuariosConAccesoInicial.length > 0) {
|
|
196
|
+
const PermisoExtraIdValor = await this.PermisoExtraId(Permiso);
|
|
197
|
+
this.UsuariosConAccesoInicial.forEach(async (dato) => {
|
|
198
|
+
await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_PermisosExtraPersonasV2` VALUES (?,\
|
|
199
|
+
(SELECT `Identificador` FROM `SIGU`.`SIGU_Personas` WHERE `Identificacion` = ?), NOW(4), USER())\
|
|
197
200
|
ON DUPLICATE KEY UPDATE `LastUpdate` = NOW(4)"
|
|
198
|
-
|
|
201
|
+
, [PermisoExtraIdValor, dato]);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
async PermisoExtraId(Permiso) {
|
|
@@ -883,6 +888,37 @@ class Miscelaneo {
|
|
|
883
888
|
);
|
|
884
889
|
}
|
|
885
890
|
|
|
891
|
+
async registrarVistaDelModulo(Token) {
|
|
892
|
+
const { uid } = await this.obtenerDatosDelUsuario(Token);
|
|
893
|
+
await ejecutarConsulta(
|
|
894
|
+
`INSERT INTO \`DatosMiscelaneos\` (\`DatoMiscelaneo\`, \`Datos\`, \`LastUser\`)
|
|
895
|
+
VALUES ('VistasDelModulo', JSON_OBJECT('Total', 1, 'PorFecha', JSON_OBJECT(CURDATE(), 1)), ?)
|
|
896
|
+
ON DUPLICATE KEY UPDATE
|
|
897
|
+
\`Datos\` = JSON_SET(
|
|
898
|
+
\`Datos\`,
|
|
899
|
+
'$.Total', CAST(JSON_VALUE(\`Datos\`, '$.Total') AS UNSIGNED) + 1,
|
|
900
|
+
'$.PorFecha', JSON_SET(
|
|
901
|
+
COALESCE(JSON_EXTRACT(\`Datos\`, '$.PorFecha'), JSON_OBJECT()),
|
|
902
|
+
CONCAT('$."', CURDATE(), '"'),
|
|
903
|
+
COALESCE(CAST(JSON_EXTRACT(JSON_EXTRACT(\`Datos\`, '$.PorFecha'), CONCAT('$."', CURDATE(), '"')) AS UNSIGNED), 0) + 1
|
|
904
|
+
)
|
|
905
|
+
),
|
|
906
|
+
\`LastUser\` = ?`,
|
|
907
|
+
[uid, uid]
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async obtenerVistas() {
|
|
912
|
+
const [manual, modulo] = await Promise.all([
|
|
913
|
+
ejecutarConsulta("SELECT CAST(JSON_VALUE(`Datos`, '$.Total') AS UNSIGNED) AS Total FROM `DatosMiscelaneos` WHERE `DatoMiscelaneo` = 'VistasDelManual'"),
|
|
914
|
+
ejecutarConsulta("SELECT CAST(JSON_VALUE(`Datos`, '$.Total') AS UNSIGNED) AS Total FROM `DatosMiscelaneos` WHERE `DatoMiscelaneo` = 'VistasDelModulo'")
|
|
915
|
+
]);
|
|
916
|
+
return {
|
|
917
|
+
VistasDelManual: manual[0]?.Total ?? 0,
|
|
918
|
+
VistasDelModulo: modulo[0]?.Total ?? 0
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
886
922
|
async obtenerMensajesModulares() {
|
|
887
923
|
return await ejecutarConsultaSIGU("SELECT `MensajeModularId`, `Titulo`, `Texto`, `FechaYHoraDeInicio`, `FechaYHoraDeFinalizacion` FROM `SIGU`.`SIGU_MensajesModulares` WHERE NOW(4) BETWEEN `FechaYHoraDeInicio` AND `FechaYHoraDeFinalizacion` AND `Modulo` = ?"
|
|
888
924
|
, [this.NombreCanonicoDelModulo]);
|
|
@@ -1096,6 +1132,22 @@ class Miscelaneo {
|
|
|
1096
1132
|
(SELECT `Identificador` FROM `SIGU`.`SIGU_Personas` WHERE `Identificacion` = '111050570'), NOW(4), USER()) ON DUPLICATE KEY UPDATE `LastUser` = USER()"
|
|
1097
1133
|
, [await this.permisoIdV2()]);
|
|
1098
1134
|
|
|
1135
|
+
// Asginación inicial de permisos
|
|
1136
|
+
if (this.UsuariosConAccesoInicial.length > 0) {
|
|
1137
|
+
const permisoId = await this.permisoIdV2();
|
|
1138
|
+
const permisoIdDelPadre = await this.permisoIdDelPadreV2();
|
|
1139
|
+
this.UsuariosConAccesoInicial.forEach(async (dato) => {
|
|
1140
|
+
await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_PermisosPersonasV2` VALUES (?,\
|
|
1141
|
+
(SELECT `Identificador` FROM `SIGU`.`SIGU_Personas` WHERE `Identificacion` = ?), NOW(4), USER())\
|
|
1142
|
+
ON DUPLICATE KEY UPDATE `LastUpdate` = NOW(4)"
|
|
1143
|
+
, [permisoIdDelPadre, dato]);
|
|
1144
|
+
await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_PermisosPersonasV2` VALUES (?,\
|
|
1145
|
+
(SELECT `Identificador` FROM `SIGU`.`SIGU_Personas` WHERE `Identificacion` = ?), NOW(4), USER())\
|
|
1146
|
+
ON DUPLICATE KEY UPDATE `LastUpdate` = NOW(4)"
|
|
1147
|
+
, [permisoId, dato]);
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1099
1151
|
// Validación de que sólo el front especificado pueda consumir este backend
|
|
1100
1152
|
const uuidTemporal = await ejecutarConsultaSIGU("SELECT UUID() AS `UUID`");
|
|
1101
1153
|
await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_Repositorios` VALUES (?, ?) ON DUPLICATE KEY UPDATE `Identificador` = ?"
|
|
@@ -1283,6 +1335,21 @@ class Miscelaneo {
|
|
|
1283
1335
|
});
|
|
1284
1336
|
}
|
|
1285
1337
|
|
|
1338
|
+
// // Asginación inicial de permisos
|
|
1339
|
+
// if (this.UsuariosConAccesoInicial.length > 0) {
|
|
1340
|
+
// const rolPermisoIdDelPadre = await this.rolPermisoIdDelMenuPadre();
|
|
1341
|
+
// this.UsuariosConAccesoInicial.forEach(async (dato) => {
|
|
1342
|
+
// await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_RolesPersonas` VALUES (?, ?,\
|
|
1343
|
+
// (SELECT `Identificador` FROM `SIGU`.`SIGU_Personas` WHERE `Identificacion` = ?), NOW(4), USER())\
|
|
1344
|
+
// ON DUPLICATE KEY UPDATE `LastUpdate` = NOW(4)"
|
|
1345
|
+
// , [this.RolPermisoId, perfilGeneralId, dato]);
|
|
1346
|
+
// await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_RolesPersonas` VALUES (?, ?,\
|
|
1347
|
+
// (SELECT `Identificador` FROM `SIGU`.`SIGU_Personas` WHERE `Identificacion` = ?), NOW(4), USER())\
|
|
1348
|
+
// ON DUPLICATE KEY UPDATE `LastUpdate` = NOW(4)"
|
|
1349
|
+
// , [rolPermisoIdDelPadre, perfilGeneralId, dato]);
|
|
1350
|
+
// });
|
|
1351
|
+
// }
|
|
1352
|
+
|
|
1286
1353
|
// // Impresión de la información de registro
|
|
1287
1354
|
// console.log(new Date());
|
|
1288
1355
|
// console.log(`Módulo registrado correctamente en SIGU. Módulo: ${this.NombreCanonicoDelModulo}`);
|
|
@@ -2511,11 +2578,17 @@ class Miscelaneo {
|
|
|
2511
2578
|
const serviciosDir = path.join(__dirname, '..');
|
|
2512
2579
|
|
|
2513
2580
|
const usuario = await this.obtenerDatosDelUsuario(token);
|
|
2514
|
-
const [tarjetas, configuracion] = await Promise.all([
|
|
2581
|
+
const [tarjetas, configuracion, moduloInfo] = await Promise.all([
|
|
2515
2582
|
this.obtenerTarjetas(),
|
|
2516
|
-
ConfiguracionDeTarjetas.obtener({ Identificador: usuario.uid })
|
|
2583
|
+
ConfiguracionDeTarjetas.obtener({ Identificador: usuario.uid }),
|
|
2584
|
+
ejecutarConsultaSIGU(
|
|
2585
|
+
"SELECT `Color` FROM `SIGU`.`SIGU_ModulosV2` WHERE `Nombre` = (SELECT `Padre` FROM `SIGU`.`SIGU_ModulosV2` WHERE `Nombre` = ?)",
|
|
2586
|
+
[this.NombreCanonicoDelModulo]
|
|
2587
|
+
)
|
|
2517
2588
|
]);
|
|
2518
2589
|
|
|
2590
|
+
const colorDelModulo = moduloInfo[0]?.Color;
|
|
2591
|
+
|
|
2519
2592
|
const datosDeServicio = await Promise.all(
|
|
2520
2593
|
tarjetas
|
|
2521
2594
|
.filter(t => t.Archivo)
|
|
@@ -2550,6 +2623,7 @@ class Miscelaneo {
|
|
|
2550
2623
|
t['Posición'] = config.Posicion;
|
|
2551
2624
|
if (config.ColorDeBorde) t.ColorDeBorde = config.ColorDeBorde;
|
|
2552
2625
|
}
|
|
2626
|
+
if (!t.ColorDeBorde && colorDelModulo) t.ColorDeBorde = colorDelModulo;
|
|
2553
2627
|
const datos = datosPorTitulo[t['Título']];
|
|
2554
2628
|
if (datos?.cantidades) {
|
|
2555
2629
|
t['Cantidad'] = datos.cantidades.cantidad;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.contenedor {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 1rem;
|
|
5
|
+
padding: 0.5rem 0;
|
|
6
|
+
min-width: 260px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.estadistica {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: 1rem;
|
|
13
|
+
padding: 0.75rem 1rem;
|
|
14
|
+
border-radius: 8px;
|
|
15
|
+
background: #f5f8ff;
|
|
16
|
+
border-left: 4px solid #1976d2;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.estadistica-icono {
|
|
20
|
+
color: #1976d2;
|
|
21
|
+
font-size: 1.75rem;
|
|
22
|
+
width: 1.75rem;
|
|
23
|
+
height: 1.75rem;
|
|
24
|
+
flex-shrink: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.estadistica-info {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.estadistica-label {
|
|
33
|
+
font-size: 0.78rem;
|
|
34
|
+
color: #555;
|
|
35
|
+
text-transform: uppercase;
|
|
36
|
+
letter-spacing: 0.05em;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.estadistica-valor {
|
|
40
|
+
font-size: 1.5rem;
|
|
41
|
+
font-weight: 700;
|
|
42
|
+
color: #002f6b;
|
|
43
|
+
line-height: 1.2;
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<h2 mat-dialog-title>Estadísticas del módulo</h2>
|
|
2
|
+
<mat-dialog-content>
|
|
3
|
+
<div class="contenedor">
|
|
4
|
+
<div class="estadistica">
|
|
5
|
+
<mat-icon class="estadistica-icono">visibility</mat-icon>
|
|
6
|
+
<div class="estadistica-info">
|
|
7
|
+
<span class="estadistica-label">Vistas del módulo</span>
|
|
8
|
+
<span class="estadistica-valor">{{ datos.VistasDelModulo | number }}</span>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="estadistica">
|
|
12
|
+
<mat-icon class="estadistica-icono">menu_book</mat-icon>
|
|
13
|
+
<div class="estadistica-info">
|
|
14
|
+
<span class="estadistica-label">Vistas del manual</span>
|
|
15
|
+
<span class="estadistica-valor">{{ datos.VistasDelManual | number }}</span>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</mat-dialog-content>
|
|
20
|
+
<mat-dialog-actions>
|
|
21
|
+
<button mat-button (click)="Cerrar()">Cerrar</button>
|
|
22
|
+
</mat-dialog-actions>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Component, Inject, inject } from '@angular/core';
|
|
2
|
+
import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
|
|
3
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
4
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
5
|
+
import { CommonModule } from '@angular/common';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'app-estadisticas-del-modulo',
|
|
9
|
+
templateUrl: './estadisticas-del-modulo.component.html',
|
|
10
|
+
styleUrl: './estadisticas-del-modulo.component.css',
|
|
11
|
+
imports: [MatDialogContent, MatDialogActions, MatDialogTitle, MatButtonModule, MatIconModule, CommonModule]
|
|
12
|
+
})
|
|
13
|
+
export class EstadisticasDelModuloComponent {
|
|
14
|
+
readonly dialogRef = inject(MatDialogRef<EstadisticasDelModuloComponent>);
|
|
15
|
+
|
|
16
|
+
constructor(@Inject(MAT_DIALOG_DATA) public datos: { VistasDelModulo: number, VistasDelManual: number }) { }
|
|
17
|
+
|
|
18
|
+
Cerrar(): void {
|
|
19
|
+
this.dialogRef.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -58,7 +58,7 @@ export class GestionActividadComponent implements OnInit, OnDestroy {
|
|
|
58
58
|
onAccept: () => {
|
|
59
59
|
this.http.delete(`${this.datosGlobalesService.ObtenerURL()}misc/cerrarSesionPorToken/${token}`).subscribe({
|
|
60
60
|
next: () => {
|
|
61
|
-
if (token
|
|
61
|
+
if (this.tokenesIguales(token, this.tokenActual)) {
|
|
62
62
|
this.datosGlobalesService.RedirigirALogin();
|
|
63
63
|
} else {
|
|
64
64
|
this.obtenerActividad();
|
|
@@ -77,9 +77,13 @@ export class GestionActividadComponent implements OnInit, OnDestroy {
|
|
|
77
77
|
this._destroy$.complete();
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
private tokenesIguales(a: string, b: string): boolean {
|
|
81
|
+
if (a.length !== b.length) return false;
|
|
82
|
+
return [...a].reduce((acc: number, char, i) => acc | (char.charCodeAt(0) ^ b.charCodeAt(i)), 0) === 0;
|
|
83
|
+
}
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
esSesionActual(token: string): boolean {
|
|
86
|
+
return this.tokenesIguales(token, this.tokenActual);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
getIcon(metodo: string): string {
|
|
@@ -94,6 +94,27 @@
|
|
|
94
94
|
font-size: 0.78rem;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
.toc-descargar {
|
|
98
|
+
display: block;
|
|
99
|
+
width: calc(100% - 1rem);
|
|
100
|
+
margin: 1rem 0 0 0;
|
|
101
|
+
padding: 0.45rem 0.75rem;
|
|
102
|
+
background: none;
|
|
103
|
+
border: 1px solid #b0c4de;
|
|
104
|
+
border-radius: 6px;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
text-align: left;
|
|
107
|
+
font-family: inherit;
|
|
108
|
+
font-size: 0.78rem;
|
|
109
|
+
color: #002f6b;
|
|
110
|
+
transition: background 0.15s, border-color 0.15s;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.toc-descargar:hover {
|
|
114
|
+
background: #dce9ff;
|
|
115
|
+
border-color: #1976d2;
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
/* ═══════════════════════════════════════════════════════════
|
|
98
119
|
Área principal del artículo
|
|
99
120
|
══════════════════════════════════════════════════════════ */
|
|
@@ -315,4 +336,4 @@
|
|
|
315
336
|
.manual-main {
|
|
316
337
|
padding: 1rem 1rem 3rem;
|
|
317
338
|
}
|
|
318
|
-
}
|
|
339
|
+
}
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
}
|
|
15
15
|
</ul>
|
|
16
16
|
</nav>
|
|
17
|
+
@if (!cargando && !error) {
|
|
18
|
+
<button class="toc-descargar" (click)="descargar()" title="Descargar manual en formato Markdown">
|
|
19
|
+
⬇ Descargar manual
|
|
20
|
+
</button>
|
|
21
|
+
}
|
|
17
22
|
</aside>
|
|
18
23
|
|
|
19
24
|
<!-- ── Artículo principal ─────────────────────────────── -->
|
|
@@ -27,6 +27,7 @@ export class ManualComponent implements OnInit, OnDestroy {
|
|
|
27
27
|
public toc: EntradaToc[] = [];
|
|
28
28
|
public cargando: boolean = true;
|
|
29
29
|
public error: boolean = false;
|
|
30
|
+
private markdownRaw: string = '';
|
|
30
31
|
private _destroy$ = new Subject<void>();
|
|
31
32
|
@ViewChild('contenidoManual') contenidoManualRef!: ElementRef;
|
|
32
33
|
|
|
@@ -37,6 +38,7 @@ export class ManualComponent implements OnInit, OnDestroy {
|
|
|
37
38
|
|
|
38
39
|
this.http.get('/Manual.md', { responseType: 'text' }).pipe(takeUntil(this._destroy$)).subscribe({
|
|
39
40
|
next: (markdown) => {
|
|
41
|
+
this.markdownRaw = markdown;
|
|
40
42
|
const html = marked.parse(markdown) as string;
|
|
41
43
|
|
|
42
44
|
const parser = new DOMParser();
|
|
@@ -75,6 +77,16 @@ export class ManualComponent implements OnInit, OnDestroy {
|
|
|
75
77
|
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
descargar(): void {
|
|
81
|
+
const blob = new Blob([this.markdownRaw], { type: 'text/markdown' });
|
|
82
|
+
const url = URL.createObjectURL(blob);
|
|
83
|
+
const a = document.createElement('a');
|
|
84
|
+
a.href = url;
|
|
85
|
+
a.download = 'Manual.md';
|
|
86
|
+
a.click();
|
|
87
|
+
URL.revokeObjectURL(url);
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
manejarClicEnContenido(event: MouseEvent): void {
|
|
79
91
|
const anchor = (event.target as Element).closest('a');
|
|
80
92
|
if (!anchor) return;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* ── Banner full-width (fuera del padding de mat-dialog-content) ─── */
|
|
2
|
+
.mensaje-banner {
|
|
3
|
+
margin: -8px -8px 0;
|
|
4
|
+
line-height: 0;
|
|
5
|
+
border-bottom: 3px solid #002f6b;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.mensaje-banner-imagen {
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: auto;
|
|
11
|
+
display: block;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* ── Título ──────────────────────────────────────────────── */
|
|
15
|
+
.mensaje-titulo {
|
|
16
|
+
font-size: 1.75rem;
|
|
17
|
+
font-weight: 700;
|
|
18
|
+
color: #002f6b;
|
|
19
|
+
text-align: center;
|
|
20
|
+
line-height: 1.3;
|
|
21
|
+
margin: 0.70rem 0 0.70rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ── Imagen del carnet ───────────────────────────────────── */
|
|
25
|
+
.mensaje-figura {
|
|
26
|
+
display: flex;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
margin: 0 0 0.875rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.mensaje-imagen {
|
|
32
|
+
max-width: 480px;
|
|
33
|
+
max-height: 200px;
|
|
34
|
+
width: 100%;
|
|
35
|
+
border-radius: 16px;
|
|
36
|
+
display: block;
|
|
37
|
+
object-fit: contain;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* ── Descripción ─────────────────────────────────────────── */
|
|
41
|
+
.mensaje-descripcion {
|
|
42
|
+
font-size: 1rem;
|
|
43
|
+
font-weight: 700;
|
|
44
|
+
color: #002f6b;
|
|
45
|
+
text-align: justify;
|
|
46
|
+
line-height: 1.85;
|
|
47
|
+
margin: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ── Pie ─────────────────────────────────────────────────── */
|
|
51
|
+
.mensaje-pie {
|
|
52
|
+
height: 24px;
|
|
53
|
+
background: #002f6b;
|
|
54
|
+
margin: 0 -8px;
|
|
55
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<div class="mensaje-banner">
|
|
2
|
+
<img src="https://storage.sigu.utn.ac.cr/images/MensajesInstitucionales/Banner.png"
|
|
3
|
+
alt="Información SIGU"
|
|
4
|
+
class="mensaje-banner-imagen" />
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<mat-dialog-content>
|
|
8
|
+
<h1 class="mensaje-titulo">{{ datos.Titulo }}</h1>
|
|
9
|
+
|
|
10
|
+
<figure class="mensaje-figura">
|
|
11
|
+
<img src="https://storage.sigu.utn.ac.cr/images/MensajesInstitucionales/Carnet.png"
|
|
12
|
+
alt="Carnet institucional"
|
|
13
|
+
class="mensaje-imagen" />
|
|
14
|
+
</figure>
|
|
15
|
+
|
|
16
|
+
<p class="mensaje-descripcion">{{ datos.Texto }}</p>
|
|
17
|
+
</mat-dialog-content>
|
|
18
|
+
|
|
19
|
+
<mat-dialog-actions>
|
|
20
|
+
<button mat-button (click)="Cerrar()">Cerrar</button>
|
|
21
|
+
</mat-dialog-actions>
|
|
22
|
+
|
|
23
|
+
<footer class="mensaje-pie"></footer>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Component, Inject, inject } from '@angular/core';
|
|
2
|
+
import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef } from '@angular/material/dialog';
|
|
3
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'app-mensaje-institucional',
|
|
7
|
+
templateUrl: './mensaje-institucional.component.html',
|
|
8
|
+
styleUrl: './mensaje-institucional.component.css',
|
|
9
|
+
imports: [MatDialogContent, MatDialogActions, MatButtonModule]
|
|
10
|
+
})
|
|
11
|
+
export class MensajeInstitucionalComponent {
|
|
12
|
+
readonly dialogRef = inject(MatDialogRef<MensajeInstitucionalComponent>);
|
|
13
|
+
|
|
14
|
+
constructor(@Inject(MAT_DIALOG_DATA) public datos: { Titulo: string, Texto: string }) { }
|
|
15
|
+
|
|
16
|
+
Cerrar(): void {
|
|
17
|
+
this.dialogRef.close();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -7,13 +7,20 @@
|
|
|
7
7
|
</div>
|
|
8
8
|
<div class="pie-col derecha">
|
|
9
9
|
@if(TienePermiso) {
|
|
10
|
-
<button class="botonDeNavegacion" matTooltip="Salir" title="Salir" (click)="Salir()">
|
|
11
|
-
<mat-icon>logout</mat-icon>
|
|
12
|
-
</button>
|
|
13
10
|
<button class="botonDeNavegacion" [matTooltip]="NombreUsuario || 'Perfil'" [title]="NombreUsuario || 'Perfil'"
|
|
14
11
|
(click)="irAPerfil()">
|
|
15
12
|
<mat-icon>person</mat-icon>
|
|
16
13
|
</button>
|
|
14
|
+
<button class="botonDeNavegacion" matTooltip="Mensajes" title="Mensajes" (click)="irAMensajes()">
|
|
15
|
+
@if (Mensajes.length > 0) {
|
|
16
|
+
<mat-icon>mark_email_unread</mat-icon>
|
|
17
|
+
} @else {
|
|
18
|
+
<mat-icon>email_unread</mat-icon>
|
|
19
|
+
}
|
|
20
|
+
</button>
|
|
21
|
+
<button class="botonDeNavegacion" matTooltip="Salir" title="Salir" (click)="Salir()">
|
|
22
|
+
<mat-icon>logout</mat-icon>
|
|
23
|
+
</button>
|
|
17
24
|
} @else {
|
|
18
25
|
<button class="botonDeNavegacion" matTooltip="Entrar" title="Entrar" (click)="Entrar()">
|
|
19
26
|
<mat-icon>login</mat-icon>
|
|
@@ -123,27 +130,22 @@
|
|
|
123
130
|
</button>
|
|
124
131
|
|
|
125
132
|
<mat-menu #menuAplicaciones="matMenu">
|
|
126
|
-
<button mat-menu-item (click)="irASugerencias()">
|
|
133
|
+
<button mat-menu-item (click)="irASugerencias()" title="Sugerencias">
|
|
127
134
|
<mat-icon>lightbulb</mat-icon>
|
|
128
135
|
<span>Sugerencias</span>
|
|
129
136
|
</button>
|
|
130
137
|
@if(TienePermiso) {
|
|
131
|
-
<button mat-menu-item (click)="irAActividad()">
|
|
138
|
+
<button mat-menu-item (click)="irAActividad()" title="Actividad de la cuenta">
|
|
132
139
|
<mat-icon>history</mat-icon>
|
|
133
140
|
<span>Actividad de la cuenta</span>
|
|
134
141
|
</button>
|
|
142
|
+
<button mat-menu-item (click)="irAEstadisticasDelModulo()" title="Estadísticas del módulo">
|
|
143
|
+
<mat-icon>bar_chart</mat-icon>
|
|
144
|
+
<span>Estadísticas del módulo</span>
|
|
145
|
+
</button>
|
|
135
146
|
}
|
|
136
147
|
</mat-menu>
|
|
137
148
|
|
|
138
|
-
@if(TienePermiso) {
|
|
139
|
-
<button class="botonDeNavegacion" matTooltip="Mensajes" title="Mensajes" (click)="irAMensajes()">
|
|
140
|
-
@if (Mensajes.length > 0) {
|
|
141
|
-
<mat-icon>mark_email_unread</mat-icon>
|
|
142
|
-
} @else {
|
|
143
|
-
<mat-icon>email_unread</mat-icon>
|
|
144
|
-
}
|
|
145
|
-
</button>
|
|
146
|
-
}
|
|
147
149
|
@if(EnlaceDelVideo !== '-') {
|
|
148
150
|
<button class="botonDeNavegacion" matTooltip="Vídeo" title="Vídeo" (click)="irAVideo()">
|
|
149
151
|
<mat-icon>live_tv</mat-icon>
|
|
@@ -10,6 +10,7 @@ import { ReporteDeIncidenciasComponent } from '../../../Componentes/Nucleo/repor
|
|
|
10
10
|
import { MensajesComponent } from '../../../Componentes/Nucleo/mensajes/mensajes.component';
|
|
11
11
|
import { MensajeConfirmacionHTMLComponent } from '../../../Componentes/Nucleo/mensaje-confirmacion-html/mensaje-confirmacion-html';
|
|
12
12
|
import { ReporteDeSugerenciasComponent } from '../../../Componentes/Nucleo/reporte-de-sugerencias/reporte-de-sugerencias.component';
|
|
13
|
+
import { EstadisticasDelModuloComponent } from '../../../Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component';
|
|
13
14
|
|
|
14
15
|
import { MatMenuModule } from '@angular/material/menu';
|
|
15
16
|
|
|
@@ -89,6 +90,9 @@ export class ContenedorComponentesComponent implements OnInit, OnDestroy, AfterV
|
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// Registro de las visita
|
|
94
|
+
this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/VistaDelModulo`).pipe().subscribe({ error: () => { } });
|
|
95
|
+
|
|
92
96
|
this.Titulo = body.Titulo;
|
|
93
97
|
this.Modulo = body.Modulo;
|
|
94
98
|
this.Version = body.Version;
|
|
@@ -208,6 +212,12 @@ export class ContenedorComponentesComponent implements OnInit, OnDestroy, AfterV
|
|
|
208
212
|
this.dialog.open(ReporteDeSugerenciasComponent);
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
irAEstadisticasDelModulo(): void {
|
|
216
|
+
this.http.get(this.datosGlobalesService.ObtenerURL() + 'misc/obtenerVistas').subscribe((datos: any) => {
|
|
217
|
+
this.dialog.open(EstadisticasDelModuloComponent, { data: datos.body });
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
211
221
|
irAActividad(): void {
|
|
212
222
|
this.router.navigate(['/Actividad']);
|
|
213
223
|
}
|
|
@@ -14,7 +14,7 @@ export class DatosGlobalesService {
|
|
|
14
14
|
ObtenerToken() {
|
|
15
15
|
const baseUrl = this.ObtenerURL();
|
|
16
16
|
if (baseUrl === 'http://localhost/') {
|
|
17
|
-
return 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
17
|
+
return 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxMiIsIklkZW50aWZpY2Fkb3IiOiIxMiIsImlhdCI6MTc3ODkzNTYyNiwiZXhwIjoxNzc4OTcxNjI2fQ.c5UQ8Umi-yjQorS_jYrAhBDDupesxapI1OsAlfcoVDY';
|
|
18
18
|
}
|
|
19
19
|
const match = document.cookie.match(/(?:^|;\s*)_siguid=([^;]+)/);
|
|
20
20
|
let token = match ? decodeURIComponent(match[1]) : '';
|
|
@@ -45,6 +45,9 @@ export class DatosGlobalesService {
|
|
|
45
45
|
// portalv2-frontend-pruebas
|
|
46
46
|
// const protocolo = new URL(window.location.href).protocol
|
|
47
47
|
window.location.href = 'https://accesov2-frontend.sigu.utn.ac.cr/';
|
|
48
|
+
document.cookie = "_siguid=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/";
|
|
49
|
+
const redirect = encodeURIComponent(window.location.href);
|
|
50
|
+
window.location.href = `https://accesov2-frontend.sigu.utn.ac.cr/?redirect=${redirect}`;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
RedirigirAPortal() {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
<title>Universidad Técnica Nacional</title>
|
|
15
15
|
<base href="/">
|
|
16
16
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
17
|
+
<meta name="theme-color" content="#adbccf">
|
|
17
18
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
18
19
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
|
19
20
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
<meta description="Universidad Técnica Nacional - SIGU">
|
|
22
23
|
</head>
|
|
23
24
|
|
|
24
|
-
<body>
|
|
25
|
+
<body bgcolor="#adbccf">
|
|
25
26
|
<app-root></app-root>
|
|
26
27
|
<!-- ACCESIBILIDAD -->
|
|
27
28
|
<script type="text/javascript">
|