utn-cli 2.1.0 → 2.1.2

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 (29) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/commands/commit.js +14 -1
  3. package/package.json +1 -1
  4. package/templates/backend/package.json +3 -3
  5. package/templates/backend/rutas/misc.js +51 -0
  6. package/templates/backend/servicios/Nucleo/Miscelaneas.js +176 -20
  7. package/templates/frontend/package.json +9 -8
  8. package/templates/frontend/public/Manual.md +1 -0
  9. package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.ts +11 -3
  10. package/templates/frontend/src/app/Componentes/Nucleo/graficos/graficos.component.ts +2 -4
  11. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.css +318 -0
  12. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.html +43 -0
  13. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.ts +74 -0
  14. package/templates/frontend/src/app/Componentes/Nucleo/mensajes/mensajes.component.ts +5 -3
  15. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-incidencias/reporte-de-incidencias.component.ts +12 -4
  16. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-sugerencias/reporte-de-sugerencias.component.ts +12 -3
  17. package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.html +13 -10
  18. package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.ts +18 -6
  19. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta/tarjeta.component.html +2 -2
  20. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta/tarjeta.component.ts +9 -13
  21. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-personalizada/tarjeta-personalizada.component.ts +7 -9
  22. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-reporte/tarjeta-reporte.component.ts +1 -6
  23. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.html +0 -2
  24. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.ts +34 -57
  25. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.html +3 -3
  26. package/templates/frontend/src/app/Paginas/contenedor-principal/contenedor-principal.component.ts +37 -203
  27. package/templates/frontend/src/app/Paginas/gestion-de-reportes/gestion-de-reportes.component.html +14 -3
  28. package/templates/frontend/src/app/Paginas/gestion-de-reportes/gestion-de-reportes.component.ts +52 -14
  29. package/templates/frontend/src/app/app.routes.ts +4 -0
@@ -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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
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
  }
@@ -486,6 +486,23 @@ Router.get('/validarToken', async (solicitud, respuesta, next) => {
486
486
  return respuesta.json({ body: await Miscelaneo.validarTokenV2(solicitud.headers.authorization), error: undefined });
487
487
  });
488
488
 
489
+ Router.get('/inicializar', async (solicitud, respuesta, next) => {
490
+ try {
491
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
492
+ try {
493
+ return respuesta.json({ body: await Miscelaneo.inicializar(solicitud), error: undefined });
494
+ } catch (error) {
495
+ const MensajeDeError = 'No fue posible inicializar el módulo';
496
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
497
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
498
+ }
499
+ }
500
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
501
+ } catch (error) {
502
+ next(error);
503
+ }
504
+ });
505
+
489
506
  Router.get('/descargarArchivo/:ArchivoId', async (solicitud, respuesta, next) => {
490
507
  try {
491
508
  if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
@@ -777,4 +794,38 @@ Router.get('/obtenerIdentificacion/:Identificador', async (solicitud, respuesta,
777
794
  }
778
795
  });
779
796
 
797
+ Router.get('/obtenerTarjetasDelContenedor', async (solicitud, respuesta, next) => {
798
+ try {
799
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
800
+ try {
801
+ return respuesta.json({ body: await Miscelaneo.obtenerTarjetasDelContenedor(solicitud.headers.authorization), error: undefined });
802
+ } catch (error) {
803
+ const MensajeDeError = 'No fue posible obtener las tarjetas del contenedor';
804
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
805
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
806
+ }
807
+ }
808
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
809
+ } catch (error) {
810
+ next(error);
811
+ }
812
+ });
813
+
814
+ Router.get('/obtenerTarjetas', async (solicitud, respuesta, next) => {
815
+ try {
816
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
817
+ try {
818
+ return respuesta.json({ body: await Miscelaneo.obtenerTarjetas(), error: undefined });
819
+ } catch (error) {
820
+ const MensajeDeError = 'No fue posible obtener las tarjetas';
821
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
822
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
823
+ }
824
+ }
825
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
826
+ } catch (error) {
827
+ next(error);
828
+ }
829
+ });
830
+
780
831
  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()
@@ -867,7 +932,7 @@ class Miscelaneo {
867
932
  return;
868
933
  }
869
934
  const Permisos = await ejecutarConsultaSIGU("SELECT `PermisoId` FROM `SIGU`.`SIGU_PermisosPersonasV2` WHERE `Identificador` = ?"
870
- , [Resultado.uid]);
935
+ , [Resultado.Identificador]);
871
936
  const Modulos = await ejecutarConsultaSIGU("SELECT `a`.`Nombre`, `a`.`Padre`, `a`.`Descripcion`, `a`.`Detalle`,\
872
937
  `a`.`Tipo`, IF(`a`.`Icono` <> '', CONCAT('https://storage.sigu.utn.ac.cr/images/cards/', `a`.`Icono`), '') AS `Icono`, `a`.`Color`, `a`.`Correo`,\
873
938
  `a`.`Version`, `a`.`FechaDePublicacion`, `a`.`AcuerdoDeNivelDeServicio`, `a`.`DiccionarioDeDatos`, `a`.`Repositorios`,\
@@ -902,7 +967,7 @@ class Miscelaneo {
902
967
  return;
903
968
  }
904
969
  return await ejecutarConsultaSIGU("UPDATE `SIGU`.`SIGU_NotificacionesV2` SET `Estado` = 'Leída' WHERE `Identificador` = ? AND `FechaYHoraDeCreacion` = ?"
905
- , [Resultado.uid, FechaYHoraDeCreacion]);
970
+ , [Resultado.Identificador, FechaYHoraDeCreacion]);
906
971
  }
907
972
 
908
973
  async obtenerNotificaciones(Token) {
@@ -916,7 +981,7 @@ class Miscelaneo {
916
981
  console.log(error);
917
982
  return;
918
983
  }
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]);
984
+ 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
985
  }
921
986
 
922
987
  async reporteDeIncidencia(Solicitud, Datos) {
@@ -1373,7 +1438,7 @@ class Miscelaneo {
1373
1438
  , `a`.`Nombre`, `a`.`PrimerApellido`, `a`.`SegundoApellido`\
1374
1439
  , (SELECT GROUP_CONCAT(`b`.`CuentaIBAN`) FROM `SIGU`.`SIGU_CuentasBancariasPersonas` `b` WHERE `b`.`Estado` = TRUE AND `a`.`Identificador` = `b`.`Identificador`) AS `CuentasIBAN`\
1375
1440
  FROM `SIGU`.`SIGU_Personas` `a` WHERE `a`.`Identificador` = ?"
1376
- , [Resultado.uid]);
1441
+ , [Resultado.Identificador]);
1377
1442
  }
1378
1443
 
1379
1444
  async obtenerDatosDeLaPersonaPorIdentificacion(Identificacion) {
@@ -1465,7 +1530,7 @@ class Miscelaneo {
1465
1530
  Resultado.token = token;
1466
1531
  const DatosDeLaPersona = await ejecutarConsultaSIGU("SELECT `Identificacion`, `Nombre`, `PrimerApellido`,\
1467
1532
  `SegundoApellido`, CONCAT(`FechaNacimiento`) AS `FechaDeNacimiento`\
1468
- FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` = ?", [Resultado.uid]);
1533
+ FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` = ?", [Resultado.Identificador]);
1469
1534
  Resultado.Identificacion = DatosDeLaPersona[0]['Identificacion'];
1470
1535
  Resultado.Nombre = DatosDeLaPersona[0]['Nombre'];
1471
1536
  Resultado.PrimerApellido = DatosDeLaPersona[0]['PrimerApellido'];
@@ -1490,16 +1555,16 @@ class Miscelaneo {
1490
1555
  try {
1491
1556
  // Validación del token en la sesión
1492
1557
  const tokenAlmacenado = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_Sesiones`\
1493
- WHERE `Identificador` = ? AND `Token` = ?", [Resultado.uid, Resultado.token]);
1558
+ WHERE `Identificador` = ? AND `Token` = ?", [Resultado.Identificador, Resultado.token]);
1494
1559
  if (tokenAlmacenado[0]['Total'] >= 1) {
1495
1560
  // Validación de permisos
1496
1561
  const rolPermisoId = await this.permisoIdV2();
1497
1562
  const permisos = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_PermisosPersonasV2`\
1498
- WHERE `PermisoId` = ? AND `Identificador` = ?", [rolPermisoId, Resultado.uid]);
1563
+ WHERE `PermisoId` = ? AND `Identificador` = ?", [rolPermisoId, Resultado.Identificador]);
1499
1564
  if (permisos[0]['Total'] === 1) {
1500
1565
  if (permisoExtraId) {
1501
1566
  const ValidacionPermisoExtra = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_PermisosExtraPersonasV2`\
1502
- WHERE `PermisoExtraId` = ? AND `Identificador` = ?", [permisoExtraId, Resultado.uid]);
1567
+ WHERE `PermisoExtraId` = ? AND `Identificador` = ?", [permisoExtraId, Resultado.Identificador]);
1503
1568
  if (ValidacionPermisoExtra[0]['Total'] === 1) {
1504
1569
  return true;
1505
1570
  }
@@ -1533,13 +1598,13 @@ class Miscelaneo {
1533
1598
  // try {
1534
1599
  // // Validación del token en la sesión
1535
1600
  // const tokenAlmacenado = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_Sesiones`\
1536
- // WHERE `Identificador` = ? AND `Token` = ?", [Resultado.uid, Resultado.token]);
1601
+ // WHERE `Identificador` = ? AND `Token` = ?", [Resultado.Identificador, Resultado.token]);
1537
1602
  // if (tokenAlmacenado[0]['Total'] >= 1) { // Se compara con >= para preveer multisesiones
1538
1603
  // // Validación de permisos
1539
1604
  // const perfilGeneralId = await this.perfilGeneralId();
1540
1605
  // const rolPermisoId = await this.rolPermisoIdDelModulo();
1541
1606
  // const permisos = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_RolesPersonas`\
1542
- // WHERE `RolPermisoId` = ? AND `Identificador` = ? AND `PerfilGeneralId` = ?", [rolPermisoId, Resultado.uid, perfilGeneralId]);
1607
+ // WHERE `RolPermisoId` = ? AND `Identificador` = ? AND `PerfilGeneralId` = ?", [rolPermisoId, Resultado.Identificador, perfilGeneralId]);
1543
1608
  // if (permisos[0]['Total'] === 1) {
1544
1609
  // return true;
1545
1610
  // } else {
@@ -1863,8 +1928,8 @@ class Miscelaneo {
1863
1928
  informacionDelArchivo.Etiquetas = Etiquetas;
1864
1929
  await ejecutarConsulta("INSERT INTO `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
1865
1930
  VALUES (?, ?, ?, ?, ?, NOW(4), ?)"
1866
- , [Respuesta.insertId, Resultado.uid, informacionDelArchivo.rutaDeArchivo, informacionDelArchivo.nombreDeArchivo
1867
- , Etiquetas, Resultado.uid]);
1931
+ , [Respuesta.insertId, Resultado.Identificador, informacionDelArchivo.rutaDeArchivo, informacionDelArchivo.nombreDeArchivo
1932
+ , Etiquetas, Resultado.Identificador]);
1868
1933
  return informacionDelArchivo;
1869
1934
  }
1870
1935
  return;
@@ -2148,7 +2213,7 @@ class Miscelaneo {
2148
2213
  let Token = undefined;
2149
2214
  if (this.NombresParalocalhost().includes(process.env.HOST) && (typeof process.env.DB_HOST_SIGU === "undefined")) {
2150
2215
  const jwt = require('jsonwebtoken');
2151
- Token = await jwt.sign({ uid: Identificador }, await this.palabraSecretaParaTokens(), { expiresIn: '10h' });
2216
+ Token = await jwt.sign({ uid: Identificador, Identificador }, await this.palabraSecretaParaTokens(), { expiresIn: '10h' });
2152
2217
  await ejecutarConsultaSIGU("DELETE FROM `SIGU`.`SIGU_Sesiones` WHERE `Identificador` = ?", [Identificador]);
2153
2218
  await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_Sesiones` VALUES (?, 'Backend', ?, NOW(4), USER())", [Identificador, Token]);
2154
2219
  }
@@ -2198,7 +2263,7 @@ class Miscelaneo {
2198
2263
  return await ejecutarConsulta("SELECT `ArchivoId`, `Identificador`, `Ruta`, CONCAT(`Nombre`, ' (', DATE_FORMAT(`LastUpdate`, '%Y-%M-%d %H:%i'), ')') AS `Nombre`, `Etiquetas`\
2199
2264
  FROM `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
2200
2265
  WHERE `Identificador` = ? AND `Etiquetas` = ?"
2201
- , [Resultado.uid, Etiquetas]);
2266
+ , [Resultado.Identificador, Etiquetas]);
2202
2267
  }
2203
2268
  if (ultimaParte === 'Servicio') {
2204
2269
  return await ejecutarConsulta("SELECT `ArchivoId`, `Identificador`, `Ruta`, CONCAT(`Nombre`, ' (', DATE_FORMAT(`LastUpdate`, '%Y-%M-%d %H:%i'), ')') AS `Nombre`, `Etiquetas`\
@@ -2258,14 +2323,14 @@ class Miscelaneo {
2258
2323
  const Archivo = await ejecutarConsulta("SELECT `Ruta`\
2259
2324
  FROM `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
2260
2325
  WHERE `Identificador` = ? AND `ArchivoId` = ?"
2261
- , [Resultado.uid, Datos.ArchivoId]);
2326
+ , [Resultado.Identificador, Datos.ArchivoId]);
2262
2327
  fs.unlinkSync(Archivo[0]['Ruta']);
2263
2328
  await ejecutarConsulta("DELETE FROM `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
2264
2329
  WHERE `Identificador` = ? AND `ArchivoId` = ?"
2265
- , [Resultado.uid, Datos.ArchivoId]);
2330
+ , [Resultado.Identificador, Datos.ArchivoId]);
2266
2331
  await ejecutarConsultaSIGU("DELETE FROM `SIGU`.`SIGU_Adjuntos`\
2267
2332
  WHERE `Identificador` = ? AND `AdjuntosId` = ?"
2268
- , [Resultado.uid, Datos.ArchivoId]);
2333
+ , [Resultado.Identificador, Datos.ArchivoId]);
2269
2334
  return;
2270
2335
  }
2271
2336
 
@@ -2288,7 +2353,7 @@ class Miscelaneo {
2288
2353
  RutaDelArchivo = await ejecutarConsulta("SELECT `Ruta`\
2289
2354
  FROM `" + this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3) + "`.`Archivos`\
2290
2355
  WHERE `Identificador` = ? AND `ArchivoId` = ?"
2291
- , [Resultado.uid, ArchivoId]);
2356
+ , [Resultado.Identificador, ArchivoId]);
2292
2357
  }
2293
2358
  if (ultimaParte === 'Servicio') {
2294
2359
  RutaDelArchivo = await ejecutarConsulta("SELECT `Ruta`\
@@ -2388,8 +2453,14 @@ class Miscelaneo {
2388
2453
  }
2389
2454
 
2390
2455
  async ejecucionDiferida(callback) {
2456
+ const stackLines = new Error().stack.split('\n');
2457
+ const lineaCaller = stackLines[2] ?? '';
2458
+ const match = lineaCaller.match(/\((.+?):\d+:\d+\)/) ?? lineaCaller.match(/at (.+?):\d+:\d+/);
2459
+ const archivo = match ? require('path').basename(match[1]) : null;
2460
+
2391
2461
  while (true) {
2392
2462
  try {
2463
+ this._archivoFuenteActual = archivo;
2393
2464
  await callback();
2394
2465
  break;
2395
2466
  } catch (error) {
@@ -2398,6 +2469,7 @@ class Miscelaneo {
2398
2469
  await new Promise(resolve => setTimeout(resolve, 5000));
2399
2470
  }
2400
2471
  }
2472
+ this._archivoFuenteActual = null;
2401
2473
  }
2402
2474
 
2403
2475
  // async obtenerEnlaceDelModuloPadre() {
@@ -2414,6 +2486,90 @@ class Miscelaneo {
2414
2486
  async obtenerEnlaceDePerfil() {
2415
2487
  return this.EnlaceDePerfil;
2416
2488
  }
2489
+
2490
+ async obtenerTarjetas() {
2491
+ const resultados = await ejecutarConsultaSIGU(
2492
+ "SELECT `Datos` FROM `SIGU`.`ModulosV2Tarjetas` WHERE `Modulo` = ? AND JSON_EXTRACT(`Datos`, '$.Activa') = TRUE",
2493
+ [this.NombreCanonicoDelModulo]
2494
+ );
2495
+ return resultados.map(fila => typeof fila.Datos === 'string' ? JSON.parse(fila.Datos) : fila.Datos);
2496
+ }
2497
+
2498
+ async inicializar(solicitud) {
2499
+ const ConsentimientoInformado = require('./ConsentimientoInformado.js');
2500
+ const LastUser = await this.generarLastUser(solicitud);
2501
+ const [configuracion, detalleDelModulo, notificaciones, mensajesModulares, consentimiento] = await Promise.all([
2502
+ this.configurarFrontend(solicitud.headers.authorization),
2503
+ this.obtenerDetalleDelModulo(),
2504
+ this.obtenerNotificaciones(solicitud.headers.authorization),
2505
+ this.obtenerMensajesModulares(),
2506
+ ConsentimientoInformado.ConsentimientoInformado({ LastUser })
2507
+ ]);
2508
+ return {
2509
+ TienePermiso: true,
2510
+ ...configuracion,
2511
+ EnlaceDelManual: detalleDelModulo[0]?.EnlaceDelManual ?? '-',
2512
+ EnlaceDelVideo: detalleDelModulo[0]?.EnlaceDelVideo ?? '-',
2513
+ Notificaciones: notificaciones,
2514
+ MensajesModulares: mensajesModulares,
2515
+ Consentimiento: consentimiento
2516
+ };
2517
+ }
2518
+
2519
+ async obtenerTarjetasDelContenedor(token) {
2520
+ const ConfiguracionDeTarjetas = require('./ConfiguracionDeTarjetas.js');
2521
+ const Personas = require('../Personas.js');
2522
+ const path = require('path');
2523
+ const serviciosDir = path.join(__dirname, '..');
2524
+
2525
+ const usuario = await this.obtenerDatosDelUsuario(token);
2526
+ const [tarjetas, configuracion, tienePermisoExtra] = await Promise.all([
2527
+ this.obtenerTarjetas(),
2528
+ ConfiguracionDeTarjetas.obtener({ Identificador: usuario.uid }),
2529
+ Personas.PermisoExtra(token)
2530
+ ]);
2531
+
2532
+ const cantidadesEntradas = await Promise.all(
2533
+ tarjetas
2534
+ .filter(t => t.Archivo)
2535
+ .map(async t => {
2536
+ try {
2537
+ const rutaArchivo = this._buscarArchivoRecursivo(serviciosDir, t.Archivo);
2538
+ if (!rutaArchivo) return null;
2539
+ const servicio = require(rutaArchivo);
2540
+ if (typeof servicio.Cantidades !== 'function') return null;
2541
+ const cantidades = await servicio.Cantidades(token);
2542
+ return [t['Título'], cantidades];
2543
+ } catch { return null; }
2544
+ })
2545
+ );
2546
+
2547
+ const cantidadesPorTitulo = Object.fromEntries(cantidadesEntradas.filter(Boolean));
2548
+
2549
+ const titulosAExcluir = new Set(
2550
+ Object.entries(cantidadesPorTitulo)
2551
+ .filter(([, c]) => !(c.cantidadMaxima > 0))
2552
+ .map(([titulo]) => titulo)
2553
+ );
2554
+
2555
+ return tarjetas
2556
+ .filter(t => !t.RequierePermisoExtra || tienePermisoExtra)
2557
+ .filter(t => !titulosAExcluir.has(t['Título']))
2558
+ .map(t => {
2559
+ const config = configuracion.find(c => c.Titulo === t['Título']);
2560
+ if (config) {
2561
+ t['Posición'] = config.Posicion;
2562
+ if (config.ColorDeBorde) t.ColorDeBorde = config.ColorDeBorde;
2563
+ }
2564
+ const cantidades = cantidadesPorTitulo[t['Título']];
2565
+ if (cantidades) {
2566
+ t['Cantidad'] = cantidades.cantidad;
2567
+ t['CantidadMaxima'] = cantidades.cantidadMaxima;
2568
+ }
2569
+ return t;
2570
+ })
2571
+ .sort((a, b) => a['Posición'] - b['Posición']);
2572
+ }
2417
2573
  }
2418
2574
 
2419
2575
  module.exports = new Miscelaneo();
@@ -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;
@@ -1,4 +1,4 @@
1
- import { Component, Input } from '@angular/core';
1
+ import { Component, Input, OnChanges } from '@angular/core';
2
2
  import { BaseChartDirective } from 'ng2-charts';
3
3
  import { ChartConfiguration, ChartData, Plugin } from 'chart.js';
4
4
 
@@ -9,7 +9,7 @@ import { ChartConfiguration, ChartData, Plugin } from 'chart.js';
9
9
  templateUrl: './graficos.component.html',
10
10
  styleUrl: './graficos.component.css'
11
11
  })
12
- export class GraficosComponent {
12
+ export class GraficosComponent implements OnChanges {
13
13
 
14
14
  @Input() public TipoDeGrafico: string = '';
15
15
  @Input() public Datos: any[] = [];
@@ -162,8 +162,6 @@ export class GraficosComponent {
162
162
  }
163
163
  };
164
164
 
165
- constructor() { }
166
-
167
165
  ngOnChanges() {
168
166
 
169
167
  // ✅ Si no hay datos, limpia todo para evitar gráficos raros