utn-cli 2.1.1 → 2.1.3

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 (35) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/commands/commit.js +14 -1
  3. package/commands/frontend.js +1 -1
  4. package/package.json +1 -1
  5. package/templates/backend/package.json +3 -3
  6. package/templates/backend/rutas/misc.js +69 -0
  7. package/templates/backend/servicios/Nucleo/Miscelaneas.js +188 -22
  8. package/templates/backend/servicios/Nucleo/ReportePDF.js +1 -1
  9. package/templates/bd/docker-scripts/1-crear estructura.sql +8 -0
  10. package/templates/frontend/package.json +9 -8
  11. package/templates/frontend/public/Manual.md +1 -0
  12. package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.ts +11 -3
  13. package/templates/frontend/src/app/Componentes/Nucleo/graficos/graficos.component.ts +2 -4
  14. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.css +318 -0
  15. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.html +43 -0
  16. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.ts +77 -0
  17. package/templates/frontend/src/app/Componentes/Nucleo/mensajes/mensajes.component.ts +5 -3
  18. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-incidencias/reporte-de-incidencias.component.ts +12 -4
  19. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-sugerencias/reporte-de-sugerencias.component.ts +12 -3
  20. package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.html +13 -10
  21. package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.ts +18 -6
  22. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta/tarjeta.component.html +2 -2
  23. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta/tarjeta.component.ts +9 -13
  24. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-personalizada/tarjeta-personalizada.component.ts +7 -9
  25. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-reporte/tarjeta-reporte.component.ts +1 -6
  26. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.css +72 -0
  27. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.html +17 -4
  28. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.ts +45 -57
  29. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.css +11 -0
  30. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.html +4 -4
  31. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.ts +65 -69
  32. package/templates/frontend/src/app/Paginas/gestion-de-reportes/gestion-de-reportes.component.html +14 -3
  33. package/templates/frontend/src/app/Paginas/gestion-de-reportes/gestion-de-reportes.component.ts +52 -14
  34. package/templates/frontend/src/app/app.routes.ts +4 -0
  35. package/templates/frontend/src/app/datos-globales.service.ts +4 -1
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(pandoc --version)"
5
+ ]
6
+ }
7
+ }
@@ -20,6 +20,8 @@ export async function runCommit() {
20
20
  '¿Ya realizó el update de los módulos de UTN?',
21
21
  '¿Ya revisó la visualización en móvil?',
22
22
  '¿Ya hizo el stage de todos los cambios?',
23
+ '¿Ya hizo la actualización del manual?',
24
+ '¿Ya actualizó la función de monitoreo?',
23
25
  '¿El archivo app.routes.ts hace uso de la estrategia de "Lazy Loading"?'
24
26
  ];
25
27
 
@@ -29,8 +31,19 @@ export async function runCommit() {
29
31
  [preguntas[i], preguntas[j]] = [preguntas[j], preguntas[i]];
30
32
  }
31
33
 
34
+ const PREGUNTA_MANUAL = '¿Ya hizo la actualización del manual?';
35
+ const PROMPT_MANUAL = `Necesito actualizar el manual Manual.md que se encuentra en la carpeta public, en donde se aborden todas las secciones nuevas que se contemplan en el frontend, el nivel de detalle del manual debe de ser alto, ya que no va a contener imágenes para guiar al usuario en la forma de ejecutar las diferentes acciones, debe ser compatible con las reglas de accesibilidad y para que sea leío programas lectores de pantalla, en el archivo app.routes.ts están las rutas disponibles que se deben incluir en el manual omitiendo las que estén comentadas.`;
36
+
32
37
  for (const pregunta of preguntas) {
33
- if (!await confirmarPaso(pregunta)) return closeReadLine();
38
+ if (!await confirmarPaso(pregunta)) {
39
+ if (pregunta === PREGUNTA_MANUAL) {
40
+ console.log('\nPuede usar el siguiente prompt con Claude para actualizar el manual:\n');
41
+ console.log('─'.repeat(60));
42
+ console.log(PROMPT_MANUAL);
43
+ console.log('─'.repeat(60));
44
+ }
45
+ return closeReadLine();
46
+ }
34
47
  }
35
48
 
36
49
  try {
@@ -42,7 +42,7 @@ export async function initFrontend() {
42
42
 
43
43
  export async function updateFrontend(opciones = { cerrarAlFinalizar: true }) {
44
44
  console.log('Actualizando el proyecto de frontend...');
45
- const archivosAExcluir = ['app.routes.ts', 'contenedor-principal.component.ts', '.vscode', 'dist'];
45
+ const archivosAExcluir = ['app.routes.ts', 'contenedor-principal.component.ts', '.vscode', 'dist', 'Manual.md'];
46
46
  const directoriodePlantillas = path.join(__dirname, '../templates/frontend');
47
47
  const directorioDestino = process.cwd();
48
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Herramienta CLI unificada para la gestión de plantillas en SIGU.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -21,11 +21,11 @@
21
21
  "bcryptjs": "^2.4.3",
22
22
  "cors": "^2.8.6",
23
23
  "express": "^4.22.1",
24
- "helmet": "^7.2.0",
25
24
  "google-auth-library": "^9.15.1",
25
+ "helmet": "^7.2.0",
26
26
  "jsonwebtoken": "^9.0.3",
27
- "mysql2": "^3.20.0",
28
- "nodemailer": "^8.0.5",
27
+ "mysql2": "^3.22.3",
28
+ "nodemailer": "^8.0.7",
29
29
  "pdf-lib": "^1.17.1",
30
30
  "pdfkit": "^0.15.2"
31
31
  }
@@ -209,6 +209,24 @@ Router.get('/obtenerDetalleDelModulo', async (solicitud, respuesta, next) => {
209
209
  }
210
210
  });
211
211
 
212
+ Router.get('/VistaDelManual', async (solicitud, respuesta, next) => {
213
+ try {
214
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
215
+ try {
216
+ await Miscelaneo.registrarVistaDelManual(solicitud.headers.authorization);
217
+ return respuesta.json({ body: undefined, error: undefined });
218
+ } catch (error) {
219
+ const MensajeDeError = 'No fue posible registrar la vista del manual';
220
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
221
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
222
+ }
223
+ }
224
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
225
+ } catch (error) {
226
+ next(error);
227
+ }
228
+ });
229
+
212
230
  Router.get('/obtenerDatosDelUsuario', async (solicitud, respuesta, next) => {
213
231
  try {
214
232
  if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
@@ -486,6 +504,23 @@ Router.get('/validarToken', async (solicitud, respuesta, next) => {
486
504
  return respuesta.json({ body: await Miscelaneo.validarTokenV2(solicitud.headers.authorization), error: undefined });
487
505
  });
488
506
 
507
+ Router.get('/inicializar', async (solicitud, respuesta, next) => {
508
+ try {
509
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
510
+ try {
511
+ return respuesta.json({ body: await Miscelaneo.inicializar(solicitud), error: undefined });
512
+ } catch (error) {
513
+ const MensajeDeError = 'No fue posible inicializar el módulo';
514
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
515
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
516
+ }
517
+ }
518
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
519
+ } catch (error) {
520
+ next(error);
521
+ }
522
+ });
523
+
489
524
  Router.get('/descargarArchivo/:ArchivoId', async (solicitud, respuesta, next) => {
490
525
  try {
491
526
  if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
@@ -777,4 +812,38 @@ Router.get('/obtenerIdentificacion/:Identificador', async (solicitud, respuesta,
777
812
  }
778
813
  });
779
814
 
815
+ Router.get('/obtenerTarjetasDelContenedor', async (solicitud, respuesta, next) => {
816
+ try {
817
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
818
+ try {
819
+ return respuesta.json({ body: await Miscelaneo.obtenerTarjetasDelContenedor(solicitud.headers.authorization), error: undefined });
820
+ } catch (error) {
821
+ const MensajeDeError = 'No fue posible obtener las tarjetas del contenedor';
822
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
823
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
824
+ }
825
+ }
826
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
827
+ } catch (error) {
828
+ next(error);
829
+ }
830
+ });
831
+
832
+ Router.get('/obtenerTarjetas', async (solicitud, respuesta, next) => {
833
+ try {
834
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
835
+ try {
836
+ return respuesta.json({ body: await Miscelaneo.obtenerTarjetas(), error: undefined });
837
+ } catch (error) {
838
+ const MensajeDeError = 'No fue posible obtener las tarjetas';
839
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
840
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
841
+ }
842
+ }
843
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
844
+ } catch (error) {
845
+ next(error);
846
+ }
847
+ });
848
+
780
849
  module.exports = Router;
@@ -250,7 +250,8 @@ class Miscelaneo {
250
250
  }
251
251
  }
252
252
 
253
- async RegistrarElServicio(Servicio) {
253
+ async RegistrarElServicio(Servicio, Tarjetas = null) {
254
+ const archivo = this._archivoFuenteActual;
254
255
  try {
255
256
  await ejecutarConsultaSIGU("REPLACE INTO `SIGU`.`SIGU_ModulosV2Secciones` (`Modulo`, `Descripcion`) VALUES (?, ?)"
256
257
  , [this.NombreCanonicoDelModulo, Servicio]);
@@ -258,6 +259,70 @@ class Miscelaneo {
258
259
  } catch (error) {
259
260
  console.error(error.message);
260
261
  }
262
+
263
+ if (!Tarjetas) return;
264
+
265
+ const listaTarjetas = Array.isArray(Tarjetas) ? Tarjetas : [Tarjetas];
266
+
267
+ for (const tarjeta of listaTarjetas) {
268
+ try {
269
+ const titulo = tarjeta['Título'];
270
+ const existentes = await ejecutarConsultaSIGU(
271
+ `SELECT CAST(JSON_VALUE(\`Datos\`, '$."Posición"') AS UNSIGNED) AS Posicion, JSON_VALUE(\`Datos\`, '$."Título"') AS Titulo, JSON_VALUE(\`Datos\`, '$."Descripción"') AS Descripcion FROM \`SIGU\`.\`ModulosV2Tarjetas\` WHERE \`Modulo\` = ? AND JSON_VALUE(\`Datos\`, '$."Título"') = ?`,
272
+ [this.NombreCanonicoDelModulo, titulo]
273
+ );
274
+
275
+ let datosFinales;
276
+ if (existentes.length > 0) {
277
+ datosFinales = { ...tarjeta, 'Posición': existentes[0].Posicion, 'Título': existentes[0].Titulo, 'Descripción': existentes[0].Descripcion, 'Archivo': archivo };
278
+ await ejecutarConsultaSIGU(
279
+ `UPDATE \`SIGU\`.\`ModulosV2Tarjetas\` SET \`Datos\` = ?, \`LastUser\` = 'Sistema' WHERE \`Modulo\` = ? AND JSON_VALUE(\`Datos\`, '$."Título"') = ?`,
280
+ [JSON.stringify(datosFinales), this.NombreCanonicoDelModulo, titulo]
281
+ );
282
+ } else {
283
+ let posicion = tarjeta['Posición'];
284
+ if (posicion === undefined) {
285
+ const [{ NuevaPosicion }] = await ejecutarConsultaSIGU(
286
+ `SELECT COALESCE(MAX(CAST(JSON_VALUE(\`Datos\`, '$."Posición"') AS UNSIGNED)), 0) + 10 AS NuevaPosicion FROM \`SIGU\`.\`ModulosV2Tarjetas\` WHERE \`Modulo\` = ?`,
287
+ [this.NombreCanonicoDelModulo]
288
+ );
289
+ posicion = NuevaPosicion;
290
+ }
291
+ datosFinales = { ...tarjeta, 'Posición': posicion, 'Archivo': archivo };
292
+ await ejecutarConsultaSIGU(
293
+ `INSERT INTO \`SIGU\`.\`ModulosV2Tarjetas\` (\`Modulo\`, \`Datos\`, \`LastUser\`) VALUES (?, ?, 'Sistema')`,
294
+ [this.NombreCanonicoDelModulo, JSON.stringify(datosFinales)]
295
+ );
296
+ }
297
+ console.log(`Tarjeta "${titulo}" registrada correctamente`);
298
+ } catch (error) {
299
+ console.error(`Error al registrar tarjeta "${tarjeta['Título']}":`, error.message);
300
+ }
301
+ }
302
+ }
303
+
304
+ _archivoFuenteActual = null;
305
+ _archivoCache = new Map();
306
+
307
+ _buscarArchivoRecursivo(dir, nombre) {
308
+ if (this._archivoCache.has(nombre)) return this._archivoCache.get(nombre);
309
+ const fs = require('fs');
310
+ const path = require('path');
311
+ const buscar = (directorio) => {
312
+ for (const entrada of fs.readdirSync(directorio, { withFileTypes: true })) {
313
+ const ruta = path.join(directorio, entrada.name);
314
+ if (entrada.isDirectory()) {
315
+ const resultado = buscar(ruta);
316
+ if (resultado) return resultado;
317
+ } else if (entrada.name === nombre) {
318
+ return ruta;
319
+ }
320
+ }
321
+ return null;
322
+ };
323
+ const resultado = buscar(dir);
324
+ this._archivoCache.set(nombre, resultado);
325
+ return resultado;
261
326
  }
262
327
 
263
328
  async DatosParaReporteCSV() {
@@ -381,7 +446,7 @@ class Miscelaneo {
381
446
  // }
382
447
  let LastUser = '';
383
448
  if (Resultado) {
384
- LastUser = Resultado.uid;
449
+ LastUser = Resultado.Identificador;
385
450
  }
386
451
  LastUser = LastUser + '@' + Solicitud.headers.host.trim()
387
452
  + '@' + Solicitud.headers["user-agent"].trim()
@@ -815,6 +880,14 @@ class Miscelaneo {
815
880
  });
816
881
  }
817
882
 
883
+ async registrarVistaDelManual(Token) {
884
+ const { uid } = await this.obtenerDatosDelUsuario(Token);
885
+ await ejecutarConsulta(
886
+ "INSERT INTO `DatosMiscelaneos` (`DatoMiscelaneo`, `Datos`, `LastUser`) VALUES ('VistasDelManual', JSON_OBJECT('Total', 1), ?) ON DUPLICATE KEY UPDATE `Datos` = JSON_SET(`Datos`, '$.Total', CAST(JSON_VALUE(`Datos`, '$.Total') AS UNSIGNED) + 1), `LastUser` = ?",
887
+ [uid, uid]
888
+ );
889
+ }
890
+
818
891
  async obtenerMensajesModulares() {
819
892
  return await ejecutarConsultaSIGU("SELECT `MensajeModularId`, `Titulo`, `Texto`, `FechaYHoraDeInicio`, `FechaYHoraDeFinalizacion` FROM `SIGU`.`SIGU_MensajesModulares` WHERE NOW(4) BETWEEN `FechaYHoraDeInicio` AND `FechaYHoraDeFinalizacion` AND `Modulo` = ?"
820
893
  , [this.NombreCanonicoDelModulo]);
@@ -867,7 +940,7 @@ class Miscelaneo {
867
940
  return;
868
941
  }
869
942
  const Permisos = await ejecutarConsultaSIGU("SELECT `PermisoId` FROM `SIGU`.`SIGU_PermisosPersonasV2` WHERE `Identificador` = ?"
870
- , [Resultado.uid]);
943
+ , [Resultado.Identificador]);
871
944
  const Modulos = await ejecutarConsultaSIGU("SELECT `a`.`Nombre`, `a`.`Padre`, `a`.`Descripcion`, `a`.`Detalle`,\
872
945
  `a`.`Tipo`, IF(`a`.`Icono` <> '', CONCAT('https://storage.sigu.utn.ac.cr/images/cards/', `a`.`Icono`), '') AS `Icono`, `a`.`Color`, `a`.`Correo`,\
873
946
  `a`.`Version`, `a`.`FechaDePublicacion`, `a`.`AcuerdoDeNivelDeServicio`, `a`.`DiccionarioDeDatos`, `a`.`Repositorios`,\
@@ -902,7 +975,7 @@ class Miscelaneo {
902
975
  return;
903
976
  }
904
977
  return await ejecutarConsultaSIGU("UPDATE `SIGU`.`SIGU_NotificacionesV2` SET `Estado` = 'Leída' WHERE `Identificador` = ? AND `FechaYHoraDeCreacion` = ?"
905
- , [Resultado.uid, FechaYHoraDeCreacion]);
978
+ , [Resultado.Identificador, FechaYHoraDeCreacion]);
906
979
  }
907
980
 
908
981
  async obtenerNotificaciones(Token) {
@@ -916,7 +989,7 @@ class Miscelaneo {
916
989
  console.log(error);
917
990
  return;
918
991
  }
919
- return await ejecutarConsultaSIGU("SELECT `Notificacion` AS `valor`, CONCAT(`FechaYHoraDeCreacion`) AS `llave`, false AS `tachado` FROM `SIGU`.`SIGU_NotificacionesV2` WHERE `Identificador` = ? AND `Estado` = 'Sin leer' ORDER BY `FechaYHoraDeCreacion` DESC LIMIT 10", [Resultado.uid]);
992
+ return await ejecutarConsultaSIGU("SELECT `Notificacion` AS `valor`, CONCAT(`FechaYHoraDeCreacion`) AS `llave`, false AS `tachado` FROM `SIGU`.`SIGU_NotificacionesV2` WHERE `Identificador` = ? AND `Estado` = 'Sin leer' ORDER BY `FechaYHoraDeCreacion` DESC LIMIT 10", [Resultado.Identificador]);
920
993
  }
921
994
 
922
995
  async reporteDeIncidencia(Solicitud, Datos) {
@@ -1373,7 +1446,7 @@ class Miscelaneo {
1373
1446
  , `a`.`Nombre`, `a`.`PrimerApellido`, `a`.`SegundoApellido`\
1374
1447
  , (SELECT GROUP_CONCAT(`b`.`CuentaIBAN`) FROM `SIGU`.`SIGU_CuentasBancariasPersonas` `b` WHERE `b`.`Estado` = TRUE AND `a`.`Identificador` = `b`.`Identificador`) AS `CuentasIBAN`\
1375
1448
  FROM `SIGU`.`SIGU_Personas` `a` WHERE `a`.`Identificador` = ?"
1376
- , [Resultado.uid]);
1449
+ , [Resultado.Identificador]);
1377
1450
  }
1378
1451
 
1379
1452
  async obtenerDatosDeLaPersonaPorIdentificacion(Identificacion) {
@@ -1465,7 +1538,7 @@ class Miscelaneo {
1465
1538
  Resultado.token = token;
1466
1539
  const DatosDeLaPersona = await ejecutarConsultaSIGU("SELECT `Identificacion`, `Nombre`, `PrimerApellido`,\
1467
1540
  `SegundoApellido`, CONCAT(`FechaNacimiento`) AS `FechaDeNacimiento`\
1468
- FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` = ?", [Resultado.uid]);
1541
+ FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` = ?", [Resultado.Identificador]);
1469
1542
  Resultado.Identificacion = DatosDeLaPersona[0]['Identificacion'];
1470
1543
  Resultado.Nombre = DatosDeLaPersona[0]['Nombre'];
1471
1544
  Resultado.PrimerApellido = DatosDeLaPersona[0]['PrimerApellido'];
@@ -1490,16 +1563,16 @@ class Miscelaneo {
1490
1563
  try {
1491
1564
  // Validación del token en la sesión
1492
1565
  const tokenAlmacenado = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_Sesiones`\
1493
- WHERE `Identificador` = ? AND `Token` = ?", [Resultado.uid, Resultado.token]);
1566
+ WHERE `Identificador` = ? AND `Token` = ?", [Resultado.Identificador, Resultado.token]);
1494
1567
  if (tokenAlmacenado[0]['Total'] >= 1) {
1495
1568
  // Validación de permisos
1496
1569
  const rolPermisoId = await this.permisoIdV2();
1497
1570
  const permisos = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_PermisosPersonasV2`\
1498
- WHERE `PermisoId` = ? AND `Identificador` = ?", [rolPermisoId, Resultado.uid]);
1571
+ WHERE `PermisoId` = ? AND `Identificador` = ?", [rolPermisoId, Resultado.Identificador]);
1499
1572
  if (permisos[0]['Total'] === 1) {
1500
1573
  if (permisoExtraId) {
1501
1574
  const ValidacionPermisoExtra = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_PermisosExtraPersonasV2`\
1502
- WHERE `PermisoExtraId` = ? AND `Identificador` = ?", [permisoExtraId, Resultado.uid]);
1575
+ WHERE `PermisoExtraId` = ? AND `Identificador` = ?", [permisoExtraId, Resultado.Identificador]);
1503
1576
  if (ValidacionPermisoExtra[0]['Total'] === 1) {
1504
1577
  return true;
1505
1578
  }
@@ -1533,13 +1606,13 @@ class Miscelaneo {
1533
1606
  // try {
1534
1607
  // // Validación del token en la sesión
1535
1608
  // const tokenAlmacenado = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_Sesiones`\
1536
- // WHERE `Identificador` = ? AND `Token` = ?", [Resultado.uid, Resultado.token]);
1609
+ // WHERE `Identificador` = ? AND `Token` = ?", [Resultado.Identificador, Resultado.token]);
1537
1610
  // if (tokenAlmacenado[0]['Total'] >= 1) { // Se compara con >= para preveer multisesiones
1538
1611
  // // Validación de permisos
1539
1612
  // const perfilGeneralId = await this.perfilGeneralId();
1540
1613
  // const rolPermisoId = await this.rolPermisoIdDelModulo();
1541
1614
  // const permisos = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_RolesPersonas`\
1542
- // WHERE `RolPermisoId` = ? AND `Identificador` = ? AND `PerfilGeneralId` = ?", [rolPermisoId, Resultado.uid, perfilGeneralId]);
1615
+ // WHERE `RolPermisoId` = ? AND `Identificador` = ? AND `PerfilGeneralId` = ?", [rolPermisoId, Resultado.Identificador, perfilGeneralId]);
1543
1616
  // if (permisos[0]['Total'] === 1) {
1544
1617
  // return true;
1545
1618
  // } else {
@@ -1855,16 +1928,16 @@ class Miscelaneo {
1855
1928
  const informacionDelArchivo = await this.almacenarArchivoEnDisco(Solicitud, Resultado['uid']);
1856
1929
  const Respuesta = await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_Adjuntos` (`AdjuntosId`, `Identificador`, `Modulo`, `Seccion`, `Nombre`,\
1857
1930
  `NombreOriginal`, `Ruta`, `Tipo`, `Tamanio`, `Etiqueta`, `LastUpdate`, `LastUser`)\
1858
- VALUES (NULL, ?, ?, 'No aplica', ?, ?, ?, ?, ?, 'No aplica', NOW(4), ?)"
1931
+ VALUES (NULL, ?, ?, 'No aplica', ?, ?, ?, ?, ?, ?, NOW(4), ?)"
1859
1932
  , [Resultado['uid'], this.NombreCanonicoDelModulo, informacionDelArchivo.nombreDeArchivo, informacionDelArchivo.nombreDeArchivo
1860
1933
  , informacionDelArchivo.rutaDeArchivo, informacionDelArchivo.tipoDeContenido, informacionDelArchivo.tamanioTotal
1861
- , Resultado['uid']]);
1934
+ , Etiquetas, Resultado['uid']]);
1862
1935
  informacionDelArchivo.insertId = Respuesta.insertId;
1863
1936
  informacionDelArchivo.Etiquetas = Etiquetas;
1864
1937
  await ejecutarConsulta("INSERT INTO `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
1865
1938
  VALUES (?, ?, ?, ?, ?, NOW(4), ?)"
1866
- , [Respuesta.insertId, Resultado.uid, informacionDelArchivo.rutaDeArchivo, informacionDelArchivo.nombreDeArchivo
1867
- , Etiquetas, Resultado.uid]);
1939
+ , [Respuesta.insertId, Resultado.Identificador, informacionDelArchivo.rutaDeArchivo, informacionDelArchivo.nombreDeArchivo
1940
+ , Etiquetas, Resultado.Identificador]);
1868
1941
  return informacionDelArchivo;
1869
1942
  }
1870
1943
  return;
@@ -2148,7 +2221,7 @@ class Miscelaneo {
2148
2221
  let Token = undefined;
2149
2222
  if (this.NombresParalocalhost().includes(process.env.HOST) && (typeof process.env.DB_HOST_SIGU === "undefined")) {
2150
2223
  const jwt = require('jsonwebtoken');
2151
- Token = await jwt.sign({ uid: Identificador }, await this.palabraSecretaParaTokens(), { expiresIn: '10h' });
2224
+ Token = await jwt.sign({ uid: Identificador, Identificador }, await this.palabraSecretaParaTokens(), { expiresIn: '10h' });
2152
2225
  await ejecutarConsultaSIGU("DELETE FROM `SIGU`.`SIGU_Sesiones` WHERE `Identificador` = ?", [Identificador]);
2153
2226
  await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_Sesiones` VALUES (?, 'Backend', ?, NOW(4), USER())", [Identificador, Token]);
2154
2227
  }
@@ -2198,7 +2271,7 @@ class Miscelaneo {
2198
2271
  return await ejecutarConsulta("SELECT `ArchivoId`, `Identificador`, `Ruta`, CONCAT(`Nombre`, ' (', DATE_FORMAT(`LastUpdate`, '%Y-%M-%d %H:%i'), ')') AS `Nombre`, `Etiquetas`\
2199
2272
  FROM `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
2200
2273
  WHERE `Identificador` = ? AND `Etiquetas` = ?"
2201
- , [Resultado.uid, Etiquetas]);
2274
+ , [Resultado.Identificador, Etiquetas]);
2202
2275
  }
2203
2276
  if (ultimaParte === 'Servicio') {
2204
2277
  return await ejecutarConsulta("SELECT `ArchivoId`, `Identificador`, `Ruta`, CONCAT(`Nombre`, ' (', DATE_FORMAT(`LastUpdate`, '%Y-%M-%d %H:%i'), ')') AS `Nombre`, `Etiquetas`\
@@ -2258,14 +2331,14 @@ class Miscelaneo {
2258
2331
  const Archivo = await ejecutarConsulta("SELECT `Ruta`\
2259
2332
  FROM `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
2260
2333
  WHERE `Identificador` = ? AND `ArchivoId` = ?"
2261
- , [Resultado.uid, Datos.ArchivoId]);
2334
+ , [Resultado.Identificador, Datos.ArchivoId]);
2262
2335
  fs.unlinkSync(Archivo[0]['Ruta']);
2263
2336
  await ejecutarConsulta("DELETE FROM `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
2264
2337
  WHERE `Identificador` = ? AND `ArchivoId` = ?"
2265
- , [Resultado.uid, Datos.ArchivoId]);
2338
+ , [Resultado.Identificador, Datos.ArchivoId]);
2266
2339
  await ejecutarConsultaSIGU("DELETE FROM `SIGU`.`SIGU_Adjuntos`\
2267
2340
  WHERE `Identificador` = ? AND `AdjuntosId` = ?"
2268
- , [Resultado.uid, Datos.ArchivoId]);
2341
+ , [Resultado.Identificador, Datos.ArchivoId]);
2269
2342
  return;
2270
2343
  }
2271
2344
 
@@ -2288,7 +2361,7 @@ class Miscelaneo {
2288
2361
  RutaDelArchivo = await ejecutarConsulta("SELECT `Ruta`\
2289
2362
  FROM `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
2290
2363
  WHERE `Identificador` = ? AND `ArchivoId` = ?"
2291
- , [Resultado.uid, ArchivoId]);
2364
+ , [Resultado.Identificador, ArchivoId]);
2292
2365
  }
2293
2366
  if (ultimaParte === 'Servicio') {
2294
2367
  RutaDelArchivo = await ejecutarConsulta("SELECT `Ruta`\
@@ -2388,8 +2461,14 @@ class Miscelaneo {
2388
2461
  }
2389
2462
 
2390
2463
  async ejecucionDiferida(callback) {
2464
+ const stackLines = new Error().stack.split('\n');
2465
+ const lineaCaller = stackLines[2] ?? '';
2466
+ const match = lineaCaller.match(/\((.+?):\d+:\d+\)/) ?? lineaCaller.match(/at (.+?):\d+:\d+/);
2467
+ const archivo = match ? require('path').basename(match[1]) : null;
2468
+
2391
2469
  while (true) {
2392
2470
  try {
2471
+ this._archivoFuenteActual = archivo;
2393
2472
  await callback();
2394
2473
  break;
2395
2474
  } catch (error) {
@@ -2398,6 +2477,7 @@ class Miscelaneo {
2398
2477
  await new Promise(resolve => setTimeout(resolve, 5000));
2399
2478
  }
2400
2479
  }
2480
+ this._archivoFuenteActual = null;
2401
2481
  }
2402
2482
 
2403
2483
  // async obtenerEnlaceDelModuloPadre() {
@@ -2414,6 +2494,92 @@ class Miscelaneo {
2414
2494
  async obtenerEnlaceDePerfil() {
2415
2495
  return this.EnlaceDePerfil;
2416
2496
  }
2497
+
2498
+ async obtenerTarjetas() {
2499
+ const resultados = await ejecutarConsultaSIGU(
2500
+ "SELECT `Datos` FROM `SIGU`.`ModulosV2Tarjetas` WHERE `Modulo` = ? AND JSON_EXTRACT(`Datos`, '$.Activa') = TRUE",
2501
+ [this.NombreCanonicoDelModulo]
2502
+ );
2503
+ return resultados.map(fila => typeof fila.Datos === 'string' ? JSON.parse(fila.Datos) : fila.Datos);
2504
+ }
2505
+
2506
+ async inicializar(solicitud) {
2507
+ const ConsentimientoInformado = require('./ConsentimientoInformado.js');
2508
+ const LastUser = await this.generarLastUser(solicitud);
2509
+ const [configuracion, detalleDelModulo, notificaciones, mensajesModulares, consentimiento] = await Promise.all([
2510
+ this.configurarFrontend(solicitud.headers.authorization),
2511
+ this.obtenerDetalleDelModulo(),
2512
+ this.obtenerNotificaciones(solicitud.headers.authorization),
2513
+ this.obtenerMensajesModulares(),
2514
+ ConsentimientoInformado.ConsentimientoInformado({ LastUser })
2515
+ ]);
2516
+ return {
2517
+ TienePermiso: true,
2518
+ ...configuracion,
2519
+ Descripcion: detalleDelModulo[0]?.Descripcion ?? configuracion.Descripcion,
2520
+ Detalle: detalleDelModulo[0]?.Detalle ?? configuracion.Detalle,
2521
+ EnlaceDelManual: detalleDelModulo[0]?.EnlaceDelManual ?? '-',
2522
+ EnlaceDelVideo: detalleDelModulo[0]?.EnlaceDelVideo ?? '-',
2523
+ Notificaciones: notificaciones,
2524
+ MensajesModulares: mensajesModulares,
2525
+ Consentimiento: consentimiento
2526
+ };
2527
+ }
2528
+
2529
+ async obtenerTarjetasDelContenedor(token) {
2530
+ const ConfiguracionDeTarjetas = require('./ConfiguracionDeTarjetas.js');
2531
+ const Personas = require('../Personas.js');
2532
+ const path = require('path');
2533
+ const serviciosDir = path.join(__dirname, '..');
2534
+
2535
+ const usuario = await this.obtenerDatosDelUsuario(token);
2536
+ const [tarjetas, configuracion, tienePermisoExtra] = await Promise.all([
2537
+ this.obtenerTarjetas(),
2538
+ ConfiguracionDeTarjetas.obtener({ Identificador: usuario.uid }),
2539
+ Personas.PermisoExtra(token)
2540
+ ]);
2541
+
2542
+ const cantidadesEntradas = await Promise.all(
2543
+ tarjetas
2544
+ .filter(t => t.Archivo)
2545
+ .map(async t => {
2546
+ try {
2547
+ const rutaArchivo = this._buscarArchivoRecursivo(serviciosDir, t.Archivo);
2548
+ if (!rutaArchivo) return null;
2549
+ const servicio = require(rutaArchivo);
2550
+ if (typeof servicio.Cantidades !== 'function') return null;
2551
+ const cantidades = await servicio.Cantidades(token);
2552
+ return [t['Título'], cantidades];
2553
+ } catch { return null; }
2554
+ })
2555
+ );
2556
+
2557
+ const cantidadesPorTitulo = Object.fromEntries(cantidadesEntradas.filter(Boolean));
2558
+
2559
+ const titulosAExcluir = new Set(
2560
+ Object.entries(cantidadesPorTitulo)
2561
+ .filter(([, c]) => !(c.cantidadMaxima > 0))
2562
+ .map(([titulo]) => titulo)
2563
+ );
2564
+
2565
+ return tarjetas
2566
+ .filter(t => !t.RequierePermisoExtra || tienePermisoExtra)
2567
+ .filter(t => !titulosAExcluir.has(t['Título']))
2568
+ .map(t => {
2569
+ const config = configuracion.find(c => c.Titulo === t['Título']);
2570
+ if (config) {
2571
+ t['Posición'] = config.Posicion;
2572
+ if (config.ColorDeBorde) t.ColorDeBorde = config.ColorDeBorde;
2573
+ }
2574
+ const cantidades = cantidadesPorTitulo[t['Título']];
2575
+ if (cantidades) {
2576
+ t['Cantidad'] = cantidades.cantidad;
2577
+ t['CantidadMaxima'] = cantidades.cantidadMaxima;
2578
+ }
2579
+ return t;
2580
+ })
2581
+ .sort((a, b) => a['Posición'] - b['Posición']);
2582
+ }
2417
2583
  }
2418
2584
 
2419
2585
  module.exports = new Miscelaneo();
@@ -661,7 +661,7 @@ class ReportePDF {
661
661
 
662
662
  // Función auxiliar para aplicar el formato y escribir el buffer
663
663
  const flushText = (token, isLast) => {
664
- if (token === "") return;
664
+ if (!token) return;
665
665
 
666
666
  let fontName = this.config.fuenteBase;
667
667
  if (isBold && isItalic) fontName = this.config.fuenteNegritaCursiva;
@@ -37,3 +37,11 @@ CREATE OR REPLACE TABLE `NOMBRE_DEL_REPOSITORIO_DE_BASE_DE_DATOS`.`Configuracion
37
37
  `LastUser` VARCHAR(1000) NOT NULL DEFAULT '-' COMMENT 'Último usuario que modificó la fila',
38
38
  PRIMARY KEY (`Identificador`, `Titulo`)
39
39
  ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_spanish_ci COMMENT = 'Almacena la configuración de las tarjetas del contenedor principal por usuario';
40
+
41
+ CREATE OR REPLACE TABLE `NOMBRE_DEL_REPOSITORIO_DE_BASE_DE_DATOS`.`DatosMiscelaneos` (
42
+ `DatoMiscelaneo` VARCHAR(200) NOT NULL COMMENT 'Nombre del datos misceláneo que se desea almacenar',
43
+ `Datos` JSON NOT NULL COMMENT 'Configuración completa de la tarjeta: Tipo, Posición, Título, Descripción, Ícono, RutaASeguir, Rutas, ColorDeBorde, RequierePermisoExtra, Activo',
44
+ `LastUpdate` DATETIME(4) NOT NULL DEFAULT CURRENT_TIMESTAMP(4) ON UPDATE CURRENT_TIMESTAMP(4) COMMENT 'Fecha de la última actualización de la fila',
45
+ `LastUser` VARCHAR(1000) NOT NULL DEFAULT '-' COMMENT 'Último usuario que modificó la fila',
46
+ PRIMARY KEY (`DatoMiscelaneo`)
47
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_spanish_ci COMMENT = 'Datos misceláneos sobre el módulo';
@@ -16,22 +16,23 @@
16
16
  "@angular/common": "^21.0.0",
17
17
  "@angular/compiler": "^21.0.0",
18
18
  "@angular/core": "^21.0.0",
19
- "@angular/forms": "^21.2.8",
20
- "@angular/material": "^21.2.5",
19
+ "@angular/forms": "^21.2.11",
20
+ "@angular/material": "^21.2.9",
21
21
  "@angular/platform-browser": "^21.0.0",
22
- "@angular/platform-browser-dynamic": "^21.2.8",
23
- "@angular/router": "^21.2.8",
22
+ "@angular/platform-browser-dynamic": "^21.2.11",
23
+ "@angular/router": "^21.2.11",
24
24
  "chart.js": "^4.5.1",
25
+ "marked": "^18.0.3",
25
26
  "ng2-charts": "^8.0.0",
26
27
  "rxjs": "~7.8.0",
27
28
  "tslib": "^2.3.0",
28
29
  "zone.js": "^0.16.1"
29
30
  },
30
31
  "devDependencies": {
31
- "@angular-devkit/build-angular": "^21.2.7",
32
+ "@angular-devkit/build-angular": "^21.2.9",
32
33
  "@angular/build": "^21.0.3",
33
- "@angular/cli": "^21.2.7",
34
- "@angular/compiler-cli": "^21.2.8",
34
+ "@angular/cli": "^21.2.9",
35
+ "@angular/compiler-cli": "^21.2.11",
35
36
  "@types/jasmine": "^5.1.15",
36
37
  "jasmine-core": "^5.13.0",
37
38
  "jsdom": "^27.4.0",
@@ -41,6 +42,6 @@
41
42
  "karma-jasmine": "^5.1.0",
42
43
  "karma-jasmine-html-reporter": "^2.2.0",
43
44
  "typescript": "~5.9.2",
44
- "vitest": "^4.1.3"
45
+ "vitest": "^4.1.5"
45
46
  }
46
47
  }
@@ -0,0 +1 @@
1
+ # Manual de Usuario — Módulo de Mantenimientos V2
@@ -1,4 +1,4 @@
1
- import { Component, OnInit } from '@angular/core';
1
+ import { Component, OnDestroy, OnInit } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import { HttpClient } from '@angular/common/http';
4
4
  import { MatIconModule } from '@angular/material/icon';
@@ -8,6 +8,8 @@ import { MatButtonModule } from '@angular/material/button';
8
8
  import { MatDialog } from '@angular/material/dialog';
9
9
  import { MensajeConfirmacionComponent } from '../mensaje-confirmacion/mensaje-confirmacion';
10
10
  import { DatosGlobalesService } from '../../../datos-globales.service';
11
+ import { Subject } from 'rxjs';
12
+ import { takeUntil } from 'rxjs/operators';
11
13
 
12
14
  @Component({
13
15
  selector: 'app-gestion-actividad',
@@ -16,10 +18,11 @@ import { DatosGlobalesService } from '../../../datos-globales.service';
16
18
  templateUrl: './gestion-actividad.component.html',
17
19
  styleUrls: ['./gestion-actividad.component.css']
18
20
  })
19
- export class GestionActividadComponent implements OnInit {
21
+ export class GestionActividadComponent implements OnInit, OnDestroy {
20
22
  public actividad: any[] = [];
21
23
  public cargando = false;
22
24
  private tokenActual = '';
25
+ private _destroy$ = new Subject<void>();
23
26
 
24
27
  constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService, private dialog: MatDialog) { }
25
28
 
@@ -30,7 +33,7 @@ export class GestionActividadComponent implements OnInit {
30
33
 
31
34
  obtenerActividad(): void {
32
35
  this.cargando = true;
33
- this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/ListarActividades`).subscribe({
36
+ this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/ListarActividades`).pipe(takeUntil(this._destroy$)).subscribe({
34
37
  next: (datos: any) => {
35
38
  this.actividad = datos.body;
36
39
  },
@@ -69,6 +72,11 @@ export class GestionActividadComponent implements OnInit {
69
72
  },
70
73
  });
71
74
  }
75
+ ngOnDestroy(): void {
76
+ this._destroy$.next();
77
+ this._destroy$.complete();
78
+ }
79
+
72
80
  esSesionActual(token: string): boolean {
73
81
 
74
82
  return token === this.tokenActual;