utn-cli 2.1.30 → 2.1.31

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.
Files changed (19) hide show
  1. package/commands/backend.js +3 -2
  2. package/package.json +1 -1
  3. package/templates/backend/InformacionDelModulo.json +0 -1
  4. package/templates/backend/rutas/misc.js +35 -0
  5. package/templates/backend/servicios/Nucleo/Miscelaneas.js +80 -6
  6. package/templates/frontend/src/app/Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component.css +44 -0
  7. package/templates/frontend/src/app/Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component.html +22 -0
  8. package/templates/frontend/src/app/Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component.ts +21 -0
  9. package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.ts +7 -3
  10. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.css +22 -1
  11. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.html +5 -0
  12. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.ts +12 -0
  13. package/templates/frontend/src/app/Componentes/Nucleo/mensaje-institucional/mensaje-institucional.component.css +55 -0
  14. package/templates/frontend/src/app/Componentes/Nucleo/mensaje-institucional/mensaje-institucional.component.html +23 -0
  15. package/templates/frontend/src/app/Componentes/Nucleo/mensaje-institucional/mensaje-institucional.component.ts +19 -0
  16. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.html +16 -14
  17. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.ts +10 -0
  18. package/templates/frontend/src/app/datos-globales.service.ts +4 -1
  19. package/templates/frontend/src/index.html +2 -1
@@ -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?: ', null, 'NOMBRE_DEL_REPOSITORIO_DE_BACKEND', (val) => {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.1.30",
3
+ "version": "2.1.31",
4
4
  "description": "Herramienta CLI unificada para la gestión de plantillas en SIGU.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
- const PermisoExtraIdValor = await this.PermisoExtraId(Permiso);
195
- await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_PermisosExtraPersonasV2` VALUES (?,\
196
- (SELECT `Identificador` FROM `SIGU`.`SIGU_Personas` WHERE `Identificacion` = '111050570'), NOW(4), USER())\
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
- , [PermisoExtraIdValor]);
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 === this.tokenActual) {
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
- esSesionActual(token: string): boolean {
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
- return token === this.tokenActual;
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.eyJ1aWQiOiIxMiIsIklkZW50aWZpY2Fkb3IiOiIxMiIsImlhdCI6MTc3ODQzMDUzMSwiZXhwIjoxNzc4NDY2NTMxfQ.C0bScJZ00863G1GsjFLox-V4wBj4nLUc2eXfswiOaxg';
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">