utn-cli 2.0.44 → 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 +1 -1
- package/templates/backend/rutas/misc.js +17 -0
- package/templates/backend/servicios/Nucleo/Miscelaneas.js +265 -0
- package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.ts +3 -0
- package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-modulo/tarjeta-modulo.component.ts +7 -2
- package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.css +87 -1
- package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.html +12 -1
- package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.ts +33 -2
- package/templates/frontend/src/app/Paginas/gestion-tabla/gestion-tabla.component.ts +1 -1
- package/templates/frontend/src/app/Paginas/gestion-tabla-XYZ/gestion-tabla-XYZ.component.ts +1 -1
- package/templates/frontend/src/app/Paginas/gestion-tabla-jefe/gestion-tabla-jefe.component.ts +1 -1
package/package.json
CHANGED
|
@@ -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,6 +40,271 @@ 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
|
+
|
|
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
|
+
|
|
135
|
+
GenerarReporteHTMLEncabezado(InformacionDeLaDerecha, titulares, marcaDeAgua = '') {
|
|
136
|
+
const date = new Date();
|
|
137
|
+
const year = date.getFullYear();
|
|
138
|
+
|
|
139
|
+
return ` <!DOCTYPE html>
|
|
140
|
+
<html lang="es">
|
|
141
|
+
<head>
|
|
142
|
+
<meta charset="UTF-8">
|
|
143
|
+
<title>${titulares.titulo}</title>
|
|
144
|
+
|
|
145
|
+
<style>
|
|
146
|
+
@media print {
|
|
147
|
+
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
148
|
+
}
|
|
149
|
+
body { font-family: Roboto, "Helvetica Neue", sans-serif; margin: 8px; }
|
|
150
|
+
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; min-height: 70px; }
|
|
151
|
+
.header > div { flex: 1; }
|
|
152
|
+
.logo { flex-shrink: 0; }
|
|
153
|
+
.title-container { text-align: center; flex-grow: 1; }
|
|
154
|
+
.title { font-size: 16px; font-weight: bold; white-space: nowrap; flex-shrink: 0; }
|
|
155
|
+
.subtitle { font-size: 13px; font-weight: bold; margin-top: 4px; }
|
|
156
|
+
.info { text-align: right; font-size: 11px; margin-left: 10px; flex-shrink: 0; }
|
|
157
|
+
table { width: 100%; border-collapse: separate; border-spacing: 0; border-radius: 10px; overflow: hidden; }
|
|
158
|
+
th, td { border: 1px solid #ccc; padding: 4px; text-align: left; font-size: 12px; }
|
|
159
|
+
th { background-color: #002f6b; color: white; }
|
|
160
|
+
tr:nth-child(even) { background-color: #f9f9f9; }
|
|
161
|
+
td:last-child { width: 100px; }
|
|
162
|
+
hr { border: none; border-top: 2px solid #838383; margin: 8px 0; }
|
|
163
|
+
.watermark { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-45deg); font-size: 80px; color: rgba(100, 100, 100, 0.09); font-weight: bold; z-index: 999; white-space: nowrap; pointer-events: none; }
|
|
164
|
+
</style>
|
|
165
|
+
|
|
166
|
+
</head>
|
|
167
|
+
<body>
|
|
168
|
+
<div class="watermark">${marcaDeAgua}</div>
|
|
169
|
+
<div class="header">
|
|
170
|
+
<div class="logo">
|
|
171
|
+
<img src="https://storage.sigu.utn.ac.cr/images/cards/LogoUTN.svg" alt="Logo UTN" style="height:50px;">
|
|
172
|
+
</div>
|
|
173
|
+
<div class="title-container">
|
|
174
|
+
<div class="title">Universidad Técnica Nacional</div>
|
|
175
|
+
<div class="subtitle">${titulares.primerSubtitulo}</div>
|
|
176
|
+
<div class="subtitle">${titulares.segundoSubtitulo}</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="info">
|
|
179
|
+
${InformacionDeLaDerecha}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
</div> `;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
GenerarReporteHTMLFecha() {
|
|
186
|
+
const now = new Date();
|
|
187
|
+
|
|
188
|
+
const day = now.getDate().toString().padStart(2, '0');
|
|
189
|
+
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
|
190
|
+
const year = now.getFullYear();
|
|
191
|
+
|
|
192
|
+
let hours = now.getHours();
|
|
193
|
+
const minutes = now.getMinutes().toString().padStart(2, '0');
|
|
194
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
195
|
+
|
|
196
|
+
hours = hours % 12;
|
|
197
|
+
hours = hours ? hours : 12;
|
|
198
|
+
hours = hours.toString().padStart(2, '0');
|
|
199
|
+
|
|
200
|
+
const fechaFormateada = `${day}-${month}-${year} ${hours}:${minutes} ${ampm}`;
|
|
201
|
+
|
|
202
|
+
return ` <div style="margin-left: 0; font-size: 11px; margin-top:4px;">
|
|
203
|
+
<strong>Fecha:</strong>
|
|
204
|
+
<span style="
|
|
205
|
+
border-bottom: 1px solid #000;
|
|
206
|
+
margin-left: 10px;
|
|
207
|
+
display: inline-block;
|
|
208
|
+
padding: 0 4px 2px 4px;
|
|
209
|
+
vertical-align: middle;
|
|
210
|
+
text-align: center;
|
|
211
|
+
min-width: 120px;
|
|
212
|
+
">${fechaFormateada}
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
GenerarReporteHTMLTablas(ElementosParaLaTabla, ParametrosExcluidos = [], ParametrosExtra = []) {
|
|
219
|
+
//Ejemplo ElementosParaLaTabla:
|
|
220
|
+
// [
|
|
221
|
+
// {
|
|
222
|
+
// "Fecha del traslado": "16-03-2026",
|
|
223
|
+
// "Justificacion": "Traslado por mantenimiento",
|
|
224
|
+
// "Dependencia que entrega": "Juan Pérez",
|
|
225
|
+
// "Dependencia que recibe": "María Gómez",
|
|
226
|
+
// Placa: "ABC123",
|
|
227
|
+
// "Descripción": "Laptop Dell",
|
|
228
|
+
// "Código de activo": "ACT-001",
|
|
229
|
+
// "N° de serie": "SN123456",
|
|
230
|
+
// Marca: "Dell",
|
|
231
|
+
// Modelo: "Latitude 5420",
|
|
232
|
+
// Estado: "Bueno",
|
|
233
|
+
// IdentificadorOrigen: 101,
|
|
234
|
+
// IdentificadorDestino: 202
|
|
235
|
+
// },
|
|
236
|
+
// {
|
|
237
|
+
// "Fecha del traslado": "16-03-2026",
|
|
238
|
+
// "Justificacion": "Traslado por mantenimiento",
|
|
239
|
+
// "Dependencia que entrega": "Juan Pérez",
|
|
240
|
+
// "Dependencia que recibe": "María Gómez",
|
|
241
|
+
// Placa: "XYZ789",
|
|
242
|
+
// "Descripción": "Monitor Samsung",
|
|
243
|
+
// "Código de activo": "ACT-002",
|
|
244
|
+
// "N° de serie": "SN654321",
|
|
245
|
+
// Marca: "Samsung",
|
|
246
|
+
// Modelo: "S24F350",
|
|
247
|
+
// Estado: "Excelente",
|
|
248
|
+
// IdentificadorOrigen: 101,
|
|
249
|
+
// IdentificadorDestino: 202
|
|
250
|
+
// }
|
|
251
|
+
// ]
|
|
252
|
+
// Espera un Array
|
|
253
|
+
//Ejemplo ParametrosExcluidos:
|
|
254
|
+
//const ParametrosExcluidos = ['IdentificadorOrigen', 'Justificacion', 'IdentificadorDestino'];
|
|
255
|
+
//Valores devueltos por el QUERY que no sean necesarios mostrar en la tabla.
|
|
256
|
+
|
|
257
|
+
//Ejemplo ParametrosExtra:
|
|
258
|
+
//const ParametrosExtra = ['Toma física'];
|
|
259
|
+
//En caso de necesitar un espacio extra en la tabla que no se encuentra en la lista principal.
|
|
260
|
+
|
|
261
|
+
if (!ElementosParaLaTabla?.length) return '';
|
|
262
|
+
|
|
263
|
+
const baseColumnas = Object.keys(ElementosParaLaTabla[0]).filter(col => !ParametrosExcluidos.includes(col));
|
|
264
|
+
const columnas = [...baseColumnas, ...ParametrosExtra.filter(p => !baseColumnas.includes(p))];
|
|
265
|
+
|
|
266
|
+
const tableHeaders = `<th>N°</th>` + columnas.map(col => `<th>${col}</th>`).join('');
|
|
267
|
+
|
|
268
|
+
const rows = ElementosParaLaTabla.map((fila, index) => {
|
|
269
|
+
const rowCells = columnas.map(col => {
|
|
270
|
+
const valor = fila[col] ?? '';
|
|
271
|
+
return `<td>${valor}</td>`;
|
|
272
|
+
}).join('');
|
|
273
|
+
return `<tr><td><strong>${index + 1}</strong></td>${rowCells}</tr>`;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const colCount = columnas.length + 1;
|
|
277
|
+
const minRows = 1;
|
|
278
|
+
let currentIndex = rows.length;
|
|
279
|
+
while (rows.length < minRows) {
|
|
280
|
+
let emptyCells = `<td><strong>${currentIndex + 1}</strong></td>`;
|
|
281
|
+
for (let i = 0; i < colCount - 1; i++) {
|
|
282
|
+
emptyCells += '<td></td>';
|
|
283
|
+
}
|
|
284
|
+
rows.push(`<tr style="height:40px">${emptyCells}</tr>`);
|
|
285
|
+
currentIndex++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const tableRows = rows.join('');
|
|
289
|
+
return `
|
|
290
|
+
<table>
|
|
291
|
+
<thead>
|
|
292
|
+
<tr>${tableHeaders}</tr>
|
|
293
|
+
</thead>
|
|
294
|
+
<tbody>
|
|
295
|
+
${tableRows}
|
|
296
|
+
</tbody>
|
|
297
|
+
</table>`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
GenerarReporteHTMLPie() {
|
|
301
|
+
return `
|
|
302
|
+
</body>
|
|
303
|
+
</html>`;
|
|
304
|
+
}
|
|
305
|
+
//la información adicional se concatena desde la llamada
|
|
306
|
+
//REPORTE TERMINA AQUÍ
|
|
307
|
+
|
|
43
308
|
obtenerNombreLaBaseDeDatos() {
|
|
44
309
|
return this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3);
|
|
45
310
|
}
|
package/templates/frontend/src/app/Componentes/Nucleo/subir-archivo/subir-archivo.component.ts
CHANGED
|
@@ -33,6 +33,9 @@ export class SubirArchivoComponent implements OnInit {
|
|
|
33
33
|
constructor(private dialog: MatDialog, private datosGlobalesService: DatosGlobalesService, private http: HttpClient) { }
|
|
34
34
|
|
|
35
35
|
ngOnInit(): void {
|
|
36
|
+
if (this.data.RutaParaCargar) {
|
|
37
|
+
this.RutaParaCargar = this.data.RutaParaCargar;
|
|
38
|
+
}
|
|
36
39
|
if (this.data.RutaParaListar) {
|
|
37
40
|
this.RutaParaListar = this.data.RutaParaListar;
|
|
38
41
|
}
|
package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-modulo/tarjeta-modulo.component.ts
CHANGED
|
@@ -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
|
-
|
|
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: '
|
|
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: '
|
|
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 = [];
|
package/templates/frontend/src/app/Paginas/gestion-tabla-jefe/gestion-tabla-jefe.component.ts
CHANGED
|
@@ -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: '
|
|
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) {
|