utn-cli 2.0.45 → 2.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.0.45",
3
+ "version": "2.0.46",
4
4
  "description": "Herramienta CLI unificada para la gestión de plantillas en SIGU.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -4,6 +4,23 @@ const Router = express.Router();
4
4
  const Miscelaneo = require('./../servicios/Nucleo/Miscelaneas.js');
5
5
  const ManejadorDeErrores = require('../servicios/Nucleo/ManejadorDeErrores.js');
6
6
 
7
+ Router.get('/UsuariosActuales', async (solicitud, respuesta, next) => {
8
+ try {
9
+ if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
10
+ try {
11
+ return respuesta.json({ body: await Miscelaneo.UsuariosActuales(), error: undefined });
12
+ } catch (error) {
13
+ const MensajeDeError = 'No fue posible obtener los usuarios actuales';
14
+ console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
15
+ return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
16
+ }
17
+ }
18
+ return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
19
+ } catch (error) {
20
+ next(error);
21
+ }
22
+ });
23
+
7
24
  Router.post('/Verificar2FA', async (solicitud, respuesta, next) => {
8
25
  try {
9
26
  return respuesta.json({ body: await Miscelaneo.Verificar2FA(solicitud), error: undefined });
@@ -40,7 +40,98 @@ class Miscelaneo {
40
40
  this.EnlaceDeAcceso = undefined;
41
41
  };
42
42
 
43
+ async UsuariosActuales() {
44
+ const ConexionSigu = await crearObjetoConexionSIGU();
45
+ const Actuales = await ConexionSigu.query("SELECT COUNT(DISTINCT `Identificador`) AS `Total` FROM `SIGU`.`SIGU_Sesiones` WHERE `LastUpdate` >= NOW() - INTERVAL 2 HOUR");
46
+ const Activos = await ConexionSigu.query("SELECT COUNT(DISTINCT CONCAT(JSON_VALUE(`Solicitud`, '$.ip'), JSON_VALUE(`Solicitud`, '$.userAgent')) ) FROM `SIGU`.`SIGU_BitacoraDeSolicitudes` WHERE `LastUpdate` >= NOW() - INTERVAL 5 MINUTE");
47
+ if (ConexionSigu) await ConexionSigu.end();
48
+ return {
49
+ UsuariosActuales: Actuales[0][0]['Total'],
50
+ UsuariosActivos: Activos[0][0]['Total']
51
+ }
52
+ }
53
+
43
54
  //REPORTE INICIA AQUÍ
55
+ GenerarReporteHTMLRegistrosVerticales(ElementosParaLaTabla, ParametrosExcluidos = [], ParametrosExtra = []) {
56
+ //Ejemplo ElementosParaLaTabla:
57
+ // [
58
+ // {
59
+ // "Fecha del traslado": "16-03-2026",
60
+ // "Justificacion": "Traslado por mantenimiento",
61
+ // "Dependencia que entrega": "Juan Pérez",
62
+ // "Dependencia que recibe": "María Gómez",
63
+ // Placa: "ABC123",
64
+ // "Descripción": "Laptop Dell",
65
+ // "Código de activo": "ACT-001",
66
+ // "N° de serie": "SN123456",
67
+ // Marca: "Dell",
68
+ // Modelo: "Latitude 5420",
69
+ // Estado: "Bueno",
70
+ // IdentificadorOrigen: 101,
71
+ // IdentificadorDestino: 202
72
+ // },
73
+ // {
74
+ // "Fecha del traslado": "16-03-2026",
75
+ // "Justificacion": "Traslado por mantenimiento",
76
+ // "Dependencia que entrega": "Juan Pérez",
77
+ // "Dependencia que recibe": "María Gómez",
78
+ // Placa: "XYZ789",
79
+ // "Descripción": "Monitor Samsung",
80
+ // "Código de activo": "ACT-002",
81
+ // "N° de serie": "SN654321",
82
+ // Marca: "Samsung",
83
+ // Modelo: "S24F350",
84
+ // Estado: "Excelente",
85
+ // IdentificadorOrigen: 101,
86
+ // IdentificadorDestino: 202
87
+ // }
88
+ // ]
89
+ // Espera un Array
90
+ //Ejemplo ParametrosExcluidos:
91
+ //const ParametrosExcluidos = ['IdentificadorOrigen', 'Justificacion', 'IdentificadorDestino'];
92
+ //Valores devueltos por el QUERY que no sean necesarios mostrar en la tabla.
93
+
94
+ //Ejemplo ParametrosExtra:
95
+ //const ParametrosExtra = ['Toma física'];
96
+ //En caso de necesitar un espacio extra en la tabla que no se encuentra en la lista principal.
97
+ if (!ElementosParaLaTabla?.length) return '<p>No hay datos para mostrar.</p>';
98
+
99
+ const baseColumnas = Object.keys(ElementosParaLaTabla[0]).filter(col => !ParametrosExcluidos.includes(col));
100
+ const columnas = [...baseColumnas, ...ParametrosExtra.filter(p => !baseColumnas.includes(p))];
101
+
102
+ let htmlFinal = '';
103
+
104
+ ElementosParaLaTabla.forEach((fila, index) => {
105
+
106
+ const filasHTML = columnas.map(col => {
107
+ const valor = fila[col] ?? '-';
108
+ return `
109
+ <tr>
110
+ <th style="width: 20%;">${col}</th>
111
+ <td style="width: 80%;">${valor}</td>
112
+ </tr>
113
+ `;
114
+ }).join('');
115
+
116
+ htmlFinal += `
117
+ <div style="margin-top: 20px; page-break-inside: avoid; border: 1px solid #ccc; border-radius: 8px; overflow: hidden;">
118
+
119
+ <div style="background-color: #002f6b; color: white; padding: 6px 10px; font-weight: bold; font-size: 14px;">
120
+ Registro N° ${index + 1}
121
+ </div>
122
+
123
+ <table style="border: none; border-radius: 0; margin-top: 0;">
124
+ <tbody>
125
+ ${filasHTML}
126
+ </tbody>
127
+ </table>
128
+
129
+ </div>`;
130
+ });
131
+
132
+ return htmlFinal;
133
+ }
134
+
44
135
  GenerarReporteHTMLEncabezado(InformacionDeLaDerecha, titulares, marcaDeAgua = '') {
45
136
  const date = new Date();
46
137
  const year = date.getFullYear();
@@ -90,6 +181,7 @@ class Miscelaneo {
90
181
 
91
182
  </div> `;
92
183
  }
184
+
93
185
  GenerarReporteHTMLFecha() {
94
186
  const now = new Date();
95
187
 
@@ -122,6 +214,7 @@ class Miscelaneo {
122
214
  </div>
123
215
  `
124
216
  }
217
+
125
218
  GenerarReporteHTMLTablas(ElementosParaLaTabla, ParametrosExcluidos = [], ParametrosExtra = []) {
126
219
  //Ejemplo ElementosParaLaTabla:
127
220
  // [
@@ -181,7 +274,7 @@ class Miscelaneo {
181
274
  });
182
275
 
183
276
  const colCount = columnas.length + 1;
184
- const minRows = 8;
277
+ const minRows = 1;
185
278
  let currentIndex = rows.length;
186
279
  while (rows.length < minRows) {
187
280
  let emptyCells = `<td><strong>${currentIndex + 1}</strong></td>`;
@@ -1,5 +1,6 @@
1
1
  import { Component, EventEmitter, Input, Output } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
+ import { MiscelaneosService } from '../../../miscelaneos.service';
3
4
 
4
5
 
5
6
  @Component({
@@ -13,11 +14,13 @@ export class TarjetaModuloComponent {
13
14
  @Output() moduloSeleccionado = new EventEmitter<any>();
14
15
  @Output() favoritoToggled = new EventEmitter<string>();
15
16
 
17
+ constructor(private miscelaneosService: MiscelaneosService) { }
18
+
16
19
  onClick() {
17
20
  this.moduloSeleccionado.emit(this.modulo);
18
21
  }
19
22
 
20
- alternarFavorito(event: MouseEvent) {
23
+ async alternarFavorito(event: MouseEvent) {
21
24
  event.stopPropagation();
22
25
  const favoritos = this.obtenerFavoritos();
23
26
  const index = favoritos.indexOf(this.modulo.Nombre);
@@ -26,7 +29,9 @@ export class TarjetaModuloComponent {
26
29
  } else {
27
30
  favoritos.push(this.modulo.Nombre);
28
31
  }
29
- localStorage.setItem('ModulosFavoritos', JSON.stringify(favoritos));
32
+
33
+ // Actualiza localmente y en DB a través del servicio
34
+ await this.miscelaneosService.actualizarFavoritos(favoritos);
30
35
  this.favoritoToggled.emit(this.modulo.Padre);
31
36
  }
32
37
 
@@ -109,6 +109,10 @@
109
109
  display: flex;
110
110
  gap: 10px;
111
111
  align-items: center;
112
+ flex: 1;
113
+ /* Cada columna ocupará el mismo espacio */
114
+ overflow: hidden;
115
+ /* Evita que el contenido desborde el tercio asignado */
112
116
  }
113
117
 
114
118
  .pie-col.izquierda {
@@ -117,7 +121,6 @@
117
121
  }
118
122
 
119
123
  .pie-col.centro {
120
- flex: 1;
121
124
  justify-content: center;
122
125
  }
123
126
 
@@ -140,3 +143,86 @@
140
143
  display: flex;
141
144
  flex-direction: column;
142
145
  }
146
+
147
+ /* Estilos para el contador de usuarios */
148
+ .contador-usuarios {
149
+ display: flex;
150
+ align-items: center;
151
+ background: rgba(255, 255, 255, 0.5);
152
+ padding: 4px 15px;
153
+ border-radius: 20px;
154
+ font-family: 'Roboto', sans-serif;
155
+ font-size: 14px;
156
+ color: #002f6b;
157
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
158
+ border: 1px solid rgba(0, 47, 107, 0.1);
159
+ transition: all 0.3s ease;
160
+ }
161
+
162
+ .contador-usuarios:hover {
163
+ background: rgba(255, 255, 255, 0.8);
164
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
165
+ transform: translateY(-1px);
166
+ }
167
+
168
+ .icono-usuarios {
169
+ font-size: 18px !important;
170
+ width: 18px !important;
171
+ height: 18px !important;
172
+ margin-right: 8px;
173
+ color: #1976d2;
174
+ }
175
+
176
+ .etiqueta-usuarios {
177
+ margin-right: 8px;
178
+ font-weight: 500;
179
+ color: #555;
180
+ }
181
+
182
+ .separador-usuarios {
183
+ margin: 0 10px;
184
+ color: #ccc;
185
+ font-weight: 200;
186
+ }
187
+
188
+ .valor-fijo {
189
+ font-weight: bold;
190
+ color: #002f6b;
191
+ }
192
+
193
+ .numeros-container {
194
+ display: inline-flex;
195
+ /* Cambiado a inline-flex para mejor control */
196
+ justify-content: center;
197
+ align-items: center;
198
+ overflow: hidden;
199
+ height: 20px;
200
+ min-width: 25px;
201
+ /* Reserva espacio para al menos 2-3 dígitos y evita el salto horizontal */
202
+ line-height: 20px;
203
+ vertical-align: middle;
204
+ }
205
+ }
206
+
207
+ .numero-animado {
208
+ display: block;
209
+ font-weight: bold;
210
+ color: #002f6b;
211
+ width: 100%;
212
+ text-align: center;
213
+ /* Animación tipo YouTube: deslizamiento estrictamente vertical */
214
+ animation: youtubeSlide 0.6s cubic-bezier(0.23, 1, 0.32, 1) forwards;
215
+ will-change: transform, opacity;
216
+ }
217
+
218
+ @keyframes youtubeSlide {
219
+ 0% {
220
+ transform: translateY(100%);
221
+ opacity: 0;
222
+ }
223
+
224
+ 100% {
225
+ transform: translateY(0);
226
+ opacity: 1;
227
+ }
228
+ }
@@ -61,6 +61,17 @@
61
61
  Módulo: {{ Modulo }}. Versión: {{ Version }}.
62
62
  </div>
63
63
  <div class="pie-col centro">
64
+ <div class="contador-usuarios">
65
+ <mat-icon class="icono-usuarios" matTooltip="Sesiones abiertas en las últimas dos horas">group</mat-icon>
66
+ <div class="numeros-container">
67
+ @if (AnimarUsuarios) {
68
+ <div class="numero-animado">{{ UsuariosActuales | number }}</div>
69
+ }
70
+ </div>
71
+ <span class="separador-usuarios">|</span>
72
+ <mat-icon class="icono-usuarios" style="color: #4caf50;" matTooltip="Usuarios activos">remove_red_eye</mat-icon>
73
+ <span class="valor-fijo">{{ UsuariosActivos | number }}</span>
74
+ </div>
64
75
  </div>
65
76
  <div class="pie-col derecha">
66
77
  @if(TienePermiso) {
@@ -90,4 +101,4 @@
90
101
  </button>
91
102
  </div>
92
103
  </div>
93
- </div>
104
+ </div>
@@ -1,5 +1,5 @@
1
1
  import { HttpClient, HttpHeaders } from '@angular/common/http';
2
- import { Component } from '@angular/core';
2
+ import { Component, OnInit, OnDestroy } from '@angular/core';
3
3
  import { RouterOutlet } from '@angular/router';
4
4
  import { DatosGlobalesService } from '../../../datos-globales.service';
5
5
  import { Location, CommonModule } from '@angular/common';
@@ -17,7 +17,7 @@ import { ReporteDeSugerenciasComponent } from '../../../Componentes/Nucleo/repor
17
17
  templateUrl: './contenedor-componentes.component.html',
18
18
  styleUrl: './contenedor-componentes.component.css'
19
19
  })
20
- export class ContenedorComponentesComponent {
20
+ export class ContenedorComponentesComponent implements OnInit, OnDestroy {
21
21
  public TienePermiso: boolean = false;
22
22
  public Titulo: string = '';
23
23
  public Modulo: string = '';
@@ -28,6 +28,11 @@ export class ContenedorComponentesComponent {
28
28
  public claseDelContenedor: string = '';
29
29
  public Descripcion: string = '';
30
30
  public Detalle: string = '';
31
+ public UsuariosActuales: number = 0;
32
+ public UsuariosActivos: number = 0;
33
+ public AnimarUsuarios: boolean = true;
34
+ private intervaloUsuarios: any;
35
+
31
36
  constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService, private location: Location, private dialog: MatDialog) {
32
37
  if (datosGlobalesService.ObtenerToken() === '') {
33
38
  datosGlobalesService.RedirigirALogin();
@@ -48,6 +53,12 @@ export class ContenedorComponentesComponent {
48
53
  } else {
49
54
  this.claseDelContenedor = 'contenedor';
50
55
  }
56
+
57
+ this.obtenerUsuariosActuales();
58
+ this.intervaloUsuarios = setInterval(() => {
59
+ this.obtenerUsuariosActuales();
60
+ }, 30000);
61
+
51
62
  this.http.get(this.datosGlobalesService.ObtenerURL() + 'misc/validarToken').subscribe((datos: any) => {
52
63
  this.TienePermiso = datos.body;
53
64
  if (!this.TienePermiso) {
@@ -197,4 +208,24 @@ export class ContenedorComponentesComponent {
197
208
  irASugerencias(): void {
198
209
  this.dialog.open(ReporteDeSugerenciasComponent);
199
210
  }
211
+
212
+ ngOnDestroy() {
213
+ if (this.intervaloUsuarios) {
214
+ clearInterval(this.intervaloUsuarios);
215
+ }
216
+ }
217
+
218
+ obtenerUsuariosActuales(): void {
219
+ this.http.get(this.datosGlobalesService.ObtenerURL() + 'misc/UsuariosActuales').subscribe((datos: any) => {
220
+ const data = datos.body;
221
+ if (this.UsuariosActuales !== data.UsuariosActuales) {
222
+ this.UsuariosActuales = data.UsuariosActuales;
223
+ this.AnimarUsuarios = false;
224
+ setTimeout(() => {
225
+ this.AnimarUsuarios = true;
226
+ }, 50);
227
+ }
228
+ this.UsuariosActivos = data.UsuariosActivos;
229
+ });
230
+ }
200
231
  }
@@ -61,7 +61,7 @@ export class GestionTablaComponent {
61
61
  { icono: 'delete', color: 'rojo', textoAyuda: 'Eliminar', ejecutar: (fila: any) => this.confirmacionEliminar(fila), },
62
62
  ];
63
63
  public accionesSiempreClickeable = [
64
- { icono: 'upload_file', color: 'verde', textoAyuda: 'Subir adjuntos', ejecutar: (fila: any) => this.AbrirModalArchivo(fila), },
64
+ { icono: 'upload_file', color: 'verde', textoAyuda: 'Adjuntos', ejecutar: (fila: any) => this.AbrirModalArchivo(fila), },
65
65
  { icono: 'info', color: 'verdeoscuro', textoAyuda: 'Información', ejecutar: (fila: any) => this.mostrarDetalleDelRegistro(fila.LicenciaId), }
66
66
  ];
67
67
  constructor(private http: HttpClient, private dialog: MatDialog, private datosGlobalesService: DatosGlobalesService) {
@@ -63,7 +63,7 @@ export class GestionTablaXYZComponent {
63
63
  ];
64
64
  public accionesDinamicas = [];
65
65
  public accionesSiempreClickeable = [
66
- { icono: 'upload_file', color: 'verde', textoAyuda: 'Subir adjuntos', ejecutar: (fila: any) => this.AbrirModalArchivo(fila), },
66
+ { icono: 'upload_file', color: 'verde', textoAyuda: 'Adjuntos', ejecutar: (fila: any) => this.AbrirModalArchivo(fila), },
67
67
  { icono: 'info', color: 'verdeoscuro', textoAyuda: 'Información', ejecutar: (fila: any) => this.mostrarDetalleDelRegistro(fila.LicenciaId), }
68
68
  ];
69
69
  public subAcciones = [];
@@ -54,7 +54,7 @@ export class GestionTablaJefeComponent {
54
54
  { icono: 'close', color: 'rojo', textoAyuda: 'Rechazar', ejecutar: (row: any) => this.confirmacionRechazoDelJefe(row), },
55
55
  ];
56
56
  public accionesSiempreClickeable = [
57
- { icono: 'upload_file', color: 'verde', textoAyuda: 'Subir adjuntos', ejecutar: (row: any) => this.AbrirModalArchivo(row), },
57
+ { icono: 'upload_file', color: 'verde', textoAyuda: 'Adjuntos', ejecutar: (row: any) => this.AbrirModalArchivo(row), },
58
58
  { icono: 'info', color: 'verdeoscuro', textoAyuda: 'Información', ejecutar: (fila: any) => this.mostrarDetalleDelRegistro(fila.LicenciaId), }
59
59
  ];
60
60
  constructor(private http: HttpClient, private dialog: MatDialog, private datosGlobalesService: DatosGlobalesService) {