utn-cli 2.0.52 → 2.0.54

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.
@@ -229,6 +229,7 @@ export async function addServiceBackend(nombreServicio = null, opciones = { cerr
229
229
  // Si tenemos información de la tabla, generamos el CRUD
230
230
  if (tableInfo) {
231
231
  generarCRUDBackend(newServicioPath, newRutaPath, Servicio, tableInfo);
232
+ actualizarArchivoRest(Servicio, tableInfo);
232
233
  }
233
234
 
234
235
  // Reemplazar el título amigable si se proporciona
@@ -392,6 +393,68 @@ Router.get('/reporte', async (solicitud, respuesta, next) => {
392
393
  fs.writeFileSync(rutaPath, contenidoRutas);
393
394
  }
394
395
 
396
+ function actualizarArchivoRest(nombreServicio, tableInfo) {
397
+ const archivos = fs.readdirSync(process.cwd());
398
+ const archivoRest = archivos.find(f => f.endsWith('.rest'));
399
+
400
+ if (!archivoRest) return;
401
+
402
+ const { columns, primaryKey } = tableInfo;
403
+ const nombreServicioLower = nombreServicio.toLowerCase();
404
+
405
+ // Crear objeto de ejemplo para agregar/actualizar
406
+ const ejemploCuerpo = {};
407
+ columns.forEach(col => {
408
+ ejemploCuerpo[col.name] = col.type.includes('INT') ? 1 : 'Ejemplo';
409
+ });
410
+
411
+ const nuevoContenido = `
412
+ #
413
+ # Rutas del servicio ${nombreServicio}
414
+ #
415
+
416
+ ### Listar registros
417
+ GET {{ BASE_URL }}/${nombreServicio}/listar HTTP/1.1
418
+ Authorization: Bearer {{ AUTH_TOKEN }}
419
+ Content-Type: application/json
420
+ Origin: {{ BASE_URL }}
421
+
422
+ ### Agregar registro
423
+ POST {{ BASE_URL }}/${nombreServicio}/agregar HTTP/1.1
424
+ Authorization: Bearer {{ AUTH_TOKEN }}
425
+ Content-Type: application/json
426
+ Origin: {{ BASE_URL }}
427
+
428
+ ${JSON.stringify(ejemploCuerpo, null, 2)}
429
+
430
+ ### Actualizar registro
431
+ POST {{ BASE_URL }}/${nombreServicio}/actualizar HTTP/1.1
432
+ Authorization: Bearer {{ AUTH_TOKEN }}
433
+ Content-Type: application/json
434
+ Origin: {{ BASE_URL }}
435
+
436
+ ${JSON.stringify(ejemploCuerpo, null, 2)}
437
+
438
+ ### Borrar registro
439
+ POST {{ BASE_URL }}/${nombreServicio}/borrar HTTP/1.1
440
+ Authorization: Bearer {{ AUTH_TOKEN }}
441
+ Content-Type: application/json
442
+ Origin: {{ BASE_URL }}
443
+
444
+ {
445
+ "${primaryKey}": ${ejemploCuerpo[primaryKey] ? (typeof ejemploCuerpo[primaryKey] === 'number' ? ejemploCuerpo[primaryKey] : '"' + ejemploCuerpo[primaryKey] + '"') : 1}
446
+ }
447
+
448
+ ### Generar reporte CSV
449
+ GET {{ BASE_URL }}/${nombreServicio}/reporte HTTP/1.1
450
+ Authorization: Bearer {{ AUTH_TOKEN }}
451
+ Content-Type: application/json
452
+ Origin: {{ BASE_URL }}
453
+ `;
454
+
455
+ fs.appendFileSync(archivoRest, nuevoContenido);
456
+ }
457
+
395
458
  export function showBackendVersion() {
396
459
  mostrarVersion(path.join(__dirname, '../package.json'));
397
460
  closeReadLine();
@@ -79,49 +79,71 @@ export async function createComponent() {
79
79
  function findTableByComment(sqlPath, serviceTitle) {
80
80
  const content = fs.readFileSync(sqlPath, 'utf-8');
81
81
 
82
- // Regex para encontrar tablas y sus comentarios
83
- // Buscamos CREATE OR REPLACE TABLE `db`.`table` ... COMMENT = '...serviceTitle...'
84
- const tableRegex = /CREATE OR REPLACE TABLE `(.+?)`\.`(.+?)` \(([\s\S]+?)\) ENGINE = InnoDB.+?COMMENT = '.*?(?:servicio de |almacena los datos del servicio de |almacena los datos del servicio de: )?([\s\S]+?)';/gi;
82
+ // Dividir el archivo por cada definición de tabla
83
+ const tableBlocks = content.split(/CREATE OR REPLACE TABLE/i).slice(1);
85
84
 
86
- let match;
87
- while ((match = tableRegex.exec(content)) !== null) {
88
- const dbName = match[1];
89
- const tableName = match[2];
90
- const tableBody = match[3];
91
- const tableComment = match[4].trim();
85
+ for (const block of tableBlocks) {
86
+ // Reconstruir el inicio para facilitar el regex
87
+ const fullBlock = 'CREATE OR REPLACE TABLE' + block;
88
+
89
+ // Regex para extraer DB, Tabla, Cuerpo y Comentario de este bloque específico
90
+ const tableRegex = /CREATE OR REPLACE TABLE `(.+?)`\.`(.+?)` \(([\s\S]+?)\) ENGINE = InnoDB.+?COMMENT = '([\s\S]+?)';/gi;
91
+ const match = tableRegex.exec(fullBlock);
92
+
93
+ if (match) {
94
+ const dbName = match[1];
95
+ const tableName = match[2];
96
+ const tableBody = match[3];
97
+ const tableComment = match[4].trim();
92
98
 
93
- // Si el comentario coincide con el título del servicio
94
- if (tableComment.toLowerCase().includes(serviceTitle.toLowerCase()) || serviceTitle.toLowerCase().includes(tableComment.toLowerCase())) {
95
- const columns = [];
96
- let primaryKey = '';
99
+ // Verificar si el comentario coincide con el título del servicio
100
+ if (tableComment.toLowerCase().includes(serviceTitle.toLowerCase()) || serviceTitle.toLowerCase().includes(tableComment.toLowerCase())) {
101
+ const columns = [];
102
+ let primaryKey = '';
97
103
 
98
- // Extraer columnas
99
- const colRegex = /`(.+?)` (.+?)(?: COMMENT '(.+?)')?,/g;
100
- let colMatch;
101
- while ((colMatch = colRegex.exec(tableBody)) !== null) {
102
- const colName = colMatch[1];
103
- if (colName !== 'LastUpdate' && colName !== 'LastUser') {
104
- columns.push({
105
- name: colName,
106
- type: colMatch[2],
107
- alias: colMatch[3] || colName
108
- });
104
+ // Limpiar el cuerpo para procesar líneas individualmente
105
+ const lines = tableBody.split('\n').map(l => l.trim()).filter(l => l.length > 0);
106
+
107
+ for (const line of lines) {
108
+ // Ignorar líneas que no definen columnas reales (PRIMARY KEY, KEY, CONSTRAINT)
109
+ if (line.toUpperCase().startsWith('PRIMARY KEY')) {
110
+ const pkMatch = line.match(/PRIMARY KEY \(`(.+?)`\)/i);
111
+ if (pkMatch) primaryKey = pkMatch[1];
112
+ continue;
113
+ }
114
+ if (line.toUpperCase().startsWith('KEY') || line.toUpperCase().startsWith('CONSTRAINT') || line.toUpperCase().startsWith('UNIQUE')) {
115
+ continue;
116
+ }
117
+
118
+ // Regex para extraer nombre de columna, tipo y comentario
119
+ const colMatch = line.match(/^`(.+?)` (.+?)(?: COMMENT '(.+?)')?,?$/i);
120
+ if (colMatch) {
121
+ const colName = colMatch[1];
122
+ // Excluir metadatos automáticos
123
+ if (colName !== 'LastUpdate' && colName !== 'LastUser') {
124
+ columns.push({
125
+ name: colName,
126
+ type: colMatch[2].replace(/,$/, ''), // Limpiar coma final si existe
127
+ alias: colMatch[3] || colName
128
+ });
129
+ }
130
+ }
109
131
  }
110
- }
111
132
 
112
- // Extraer Primary Key
113
- const pkMatch = tableBody.match(/PRIMARY KEY \(`(.+?)`\)/);
114
- if (pkMatch) {
115
- primaryKey = pkMatch[1];
116
- }
133
+ // Si no se encontró PK en las líneas, buscarla de nuevo con un regex más flexible en todo el cuerpo
134
+ if (!primaryKey) {
135
+ const pkMatch = tableBody.match(/PRIMARY KEY \(`(.+?)`\)/i);
136
+ if (pkMatch) primaryKey = pkMatch[1];
137
+ }
117
138
 
118
- return {
119
- dbName,
120
- tableName,
121
- columns,
122
- primaryKey,
123
- serviceTitle
124
- };
139
+ return {
140
+ dbName,
141
+ tableName,
142
+ columns,
143
+ primaryKey,
144
+ serviceTitle
145
+ };
146
+ }
125
147
  }
126
148
  }
127
149
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.0.52",
3
+ "version": "2.0.54",
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('/ListarActividades', 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.ListarActividades(solicitud.headers.authorization), error: undefined });
12
+ } catch (error) {
13
+ const MensajeDeError = 'No fue posible listar las actividades';
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.get('/UsuariosActuales', async (solicitud, respuesta, next) => {
8
25
  try {
9
26
  if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
@@ -30,7 +30,7 @@ app.use(async (solicitud, respuesta, next) => {
30
30
  cookies: solicitud.cookies,
31
31
  signedCookies: solicitud.signedCookies,
32
32
  userAgent: solicitud.get('User-Agent'),
33
- authUser: solicitud.user ?? null,
33
+ authUser: await Miscelaneo.obtenerDatosDelUsuario(solicitud.headers.authorization) ?? null,
34
34
  };
35
35
  try {
36
36
  const { ejecutarConsultaSIGU } = require('./servicios/Nucleo/db.js');
@@ -61,7 +61,9 @@ asignarRutasAExpress(app);
61
61
 
62
62
  app.use((error, solicitud, respuesta, next) => {
63
63
  const { envioDeCorreo } = require('./servicios/Nucleo/EnvioDeCorreos.js');
64
- envioDeCorreo(process.env.DESTINATARIODEINFORMESDEERROR, 'Informe de error desde: ' + process.env.NOMBRECANONICODELMODULO, error.stack);
64
+ const route = solicitud.originalUrl;
65
+ const message = `<b>Ruta consultada:</b> ${route}<BR /><BR />` + error.stack;
66
+ envioDeCorreo(process.env.DESTINATARIODEINFORMESDEERROR, 'Informe de error desde: ' + process.env.NOMBRECANONICODELMODULO, message);
65
67
  console.error(error.stack);
66
68
  respuesta.status(500).send('Error interno del servidor, el detalle del mismo fue enviado a ' + process.env.DESTINATARIODEINFORMESDEERROR);
67
69
  });
@@ -40,6 +40,51 @@ class Miscelaneo {
40
40
  this.EnlaceDeAcceso = undefined;
41
41
  };
42
42
 
43
+ async ListarActividades(Datos) {
44
+ try {
45
+ const decoded = await this.obtenerDatosDelUsuario(Datos);
46
+ const RUTAS_EXCLUIDAS = [
47
+ '/misc',
48
+ '/Actividad',
49
+ '/misc/ListarActividades',
50
+ '/misc/obtenerNotificaciones',
51
+ '/misc/configurarFrontend',
52
+ '/ConsentimientoInformado/ConsentimientoInformado',
53
+ '/misc/obtenerMensajesModulares',
54
+ '/misc/obtenerDetalleDelModulo',
55
+ '/misc/validarToken',
56
+ '/misc/UsuariosActuales',
57
+ '/misc/obtenerEnlaceDePortal',
58
+ '/Personas/PermisoExtra'
59
+ ];
60
+ const datos = await ejecutarConsultaSIGU(`
61
+ SELECT
62
+ Consecutivo,
63
+ LastUpdate AS Fecha,
64
+ JSON_VALUE(Solicitud, '$.ip') AS IP,
65
+ JSON_VALUE(Solicitud, '$.userAgent') AS Navegador,
66
+ JSON_VALUE(Solicitud, '$.method') AS Metodo,
67
+ JSON_VALUE(Solicitud, '$.originalUrl') AS URL,
68
+ IFNULL(JSON_VALUE(Solicitud, '$.country'), 'Costa Rica') AS Pais,
69
+ CASE
70
+ WHEN JSON_VALUE(Solicitud, '$.userAgent') LIKE '%Mobile%' THEN 'Móvil'
71
+ WHEN JSON_VALUE(Solicitud, '$.userAgent') LIKE '%Android%' AND JSON_VALUE(Solicitud, '$.userAgent') NOT LIKE '%Mobile%' THEN 'Tablet'
72
+ WHEN JSON_VALUE(Solicitud, '$.userAgent') LIKE '%iPad%' THEN 'Tablet'
73
+ ELSE 'PC'
74
+ END AS Dispositivo
75
+ FROM SIGU.SIGU_BitacoraDeSolicitudes
76
+ WHERE JSON_VALUE(Solicitud, '$.authUser.uid') = ?
77
+ AND JSON_VALUE(Solicitud, '$.originalUrl') NOT IN (?)
78
+ ORDER BY Consecutivo DESC
79
+ LIMIT 100
80
+ `, [decoded.uid, RUTAS_EXCLUIDAS]);
81
+ return datos;
82
+ } catch (error) {
83
+ console.error('Error al listar actividad:', error);
84
+ return [];
85
+ }
86
+ }
87
+
43
88
  async UsuariosActuales() {
44
89
  const ConexionSigu = await crearObjetoConexionSIGU();
45
90
  const Actuales = await ConexionSigu.query("SELECT COUNT(DISTINCT `Identificador`) AS `Total` FROM `SIGU`.`SIGU_Sesiones` WHERE `LastUpdate` >= NOW() - INTERVAL 2 HOUR");
@@ -494,15 +539,15 @@ class Miscelaneo {
494
539
  }
495
540
 
496
541
  async generarLastUser(Solicitud) {
497
- let Resultado = undefined;
498
- try {
499
- Resultado = await this.obtenerDatosDelUsuario(Solicitud.headers.authorization);
500
- if (!Resultado) {
501
- throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorVerificacionDeToken(), ManejadorDeErrores.obtenerNumeroDeLinea());
502
- }
503
- } catch (error) {
504
- console.log(error);
505
- }
542
+ const Resultado = await this.obtenerDatosDelUsuario(Solicitud.headers.authorization);
543
+ // try {
544
+ // Resultado = await this.obtenerDatosDelUsuario(Solicitud.headers.authorization);
545
+ // if (!Resultado) {
546
+ // throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorVerificacionDeToken(), ManejadorDeErrores.obtenerNumeroDeLinea());
547
+ // }
548
+ // } catch (error) {
549
+ // console.log(error);
550
+ // }
506
551
  let LastUser = '';
507
552
  if (Resultado) {
508
553
  LastUser = Resultado.uid;
@@ -0,0 +1,193 @@
1
+ .contenedor-actividad {
2
+ background-color: transparent;
3
+ margin: 0;
4
+ padding: 10px;
5
+ }
6
+
7
+ .header-actividad {
8
+ background-color: white;
9
+ padding: 20px 24px;
10
+ border-radius: 12px;
11
+ margin-bottom: 20px;
12
+ border: 1px solid #e0e6ed;
13
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
14
+ text-align: left;
15
+ }
16
+
17
+ .header-actividad h2 {
18
+ margin: 0;
19
+ color: #002f6b;
20
+ font-size: 1.5rem;
21
+ font-weight: 700;
22
+ display: flex;
23
+ align-items: center;
24
+ gap: 12px;
25
+ }
26
+
27
+ .header-actividad h2 mat-icon {
28
+ font-size: 28px;
29
+ width: 28px;
30
+ height: 28px;
31
+ color: #0b4fce;
32
+ }
33
+
34
+ .header-actividad p {
35
+ margin: 8px 0 0 0;
36
+ color: #5f6368;
37
+ font-size: 0.95rem;
38
+ }
39
+
40
+ .mensaje-informativo {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 12px;
44
+ background-color: #e3f2fd;
45
+ border-left: 4px solid #1976d2;
46
+ padding: 12px 16px;
47
+ margin-top: 20px;
48
+ border-radius: 4px;
49
+ }
50
+
51
+ .mensaje-informativo mat-icon {
52
+ color: #1976d2;
53
+ font-size: 24px;
54
+ width: 24px;
55
+ height: 24px;
56
+ }
57
+
58
+ .mensaje-informativo p {
59
+ margin: 0 !important;
60
+ font-size: 13.5px !important;
61
+ color: #0d47a1 !important;
62
+ line-height: 1.4;
63
+ }
64
+
65
+ .mensaje-informativo strong {
66
+ color: #d32f2f;
67
+ }
68
+
69
+ .lista-actividad {
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: 12px;
73
+ }
74
+
75
+ .actividad-item {
76
+ display: flex;
77
+ align-items: center;
78
+ padding: 16px;
79
+ background-color: white;
80
+ border: 1px solid #e0e6ed;
81
+ border-radius: 12px;
82
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
83
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
84
+ text-align: left;
85
+ }
86
+
87
+ .actividad-item:hover {
88
+ transform: translateY(-2px);
89
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
90
+ border-color: #0b4fce;
91
+ background-color: #f0f7ff;
92
+ }
93
+
94
+ .item-icono {
95
+ margin-right: 12px;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ width: 40px;
100
+ height: 40px;
101
+ border-radius: 50%;
102
+ background-color: #f1f3f4;
103
+ flex-shrink: 0;
104
+ }
105
+
106
+ .item-icono mat-icon {
107
+ font-size: 20px;
108
+ width: 20px;
109
+ height: 20px;
110
+ }
111
+
112
+ /* Colores por método */
113
+ .metodo-get { color: #0069B4; background-color: #e3f2fd; }
114
+ .metodo-post { color: #518a5f; background-color: #e8f5e9; }
115
+ .metodo-put { color: #FFA757; background-color: #fff3e0; }
116
+ .metodo-delete { color: #F82617; background-color: #ffebee; }
117
+ .metodo-default { color: #7d7d7d; background-color: #f5f5f5; }
118
+
119
+ .item-contenido {
120
+ flex: 1;
121
+ display: flex;
122
+ flex-direction: column;
123
+ min-width: 0; /* Evita que el flex desborde */
124
+ }
125
+
126
+ .item-titulo {
127
+ font-size: 14px;
128
+ color: #002f6b;
129
+ font-weight: 600;
130
+ word-break: break-all;
131
+ margin-bottom: 2px;
132
+ }
133
+
134
+ .item-detalles {
135
+ display: flex;
136
+ flex-wrap: wrap;
137
+ gap: 8px;
138
+ font-size: 12px;
139
+ color: #5f6368;
140
+ }
141
+
142
+ .item-detalles span {
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 3px;
146
+ white-space: nowrap;
147
+ }
148
+
149
+ .item-detalles mat-icon {
150
+ font-size: 14px;
151
+ width: 14px;
152
+ height: 14px;
153
+ }
154
+
155
+ .detalle-dispositivo mat-icon { color: #673ab7; }
156
+ .detalle-browser mat-icon { color: #5f6368; }
157
+ .detalle-ip mat-icon { color: #0069B4; }
158
+ .detalle-pais mat-icon { color: #F87600; }
159
+
160
+ .item-fecha {
161
+ font-size: 11px;
162
+ color: #9aa0a6;
163
+ white-space: nowrap;
164
+ margin-left: 12px;
165
+ display: flex;
166
+ flex-direction: column;
167
+ align-items: flex-end;
168
+ font-family: 'Roboto Mono', monospace;
169
+ flex-shrink: 0;
170
+ }
171
+
172
+ mat-progress-bar {
173
+ height: 4px;
174
+ border-radius: 2px;
175
+ margin-bottom: 10px;
176
+ }
177
+
178
+ @media (max-width: 600px) {
179
+ .actividad-item {
180
+ flex-direction: column;
181
+ align-items: flex-start;
182
+ }
183
+
184
+ .item-icono {
185
+ margin-bottom: 12px;
186
+ }
187
+
188
+ .item-fecha {
189
+ margin-left: 0;
190
+ margin-top: 12px;
191
+ align-items: flex-start;
192
+ }
193
+ }
@@ -0,0 +1,50 @@
1
+ <div class="contenedor-actividad">
2
+ <div class="header-actividad">
3
+ <h2><mat-icon>history</mat-icon> Actividad reciente de la cuenta</h2>
4
+ <p>Historial de acciones realizadas en el sistema</p>
5
+
6
+ <div class="mensaje-informativo">
7
+ <mat-icon>info</mat-icon>
8
+ <p>Si tiene dudas sobre la información que ve en pantalla, por favor comparta una captura de pantalla al correo <strong>soporte@utn.ac.cr</strong>.</p>
9
+ </div>
10
+ </div>
11
+
12
+ @if(cargando){
13
+ <mat-progress-bar mode="indeterminate"></mat-progress-bar>
14
+ }
15
+
16
+ <div class="lista-actividad">
17
+ @if(actividad.length === 0 && !cargando) {
18
+ <p class="sin-datos">No hay actividad reciente.</p>
19
+ }
20
+
21
+ @for (item of actividad; track item.Consecutivo) {
22
+ <div class="actividad-item">
23
+ <div class="item-icono" [ngClass]="{
24
+ 'metodo-get': item.Metodo === 'GET',
25
+ 'metodo-post': item.Metodo === 'POST',
26
+ 'metodo-put': item.Metodo === 'PUT',
27
+ 'metodo-delete': item.Metodo === 'DELETE'
28
+ }">
29
+ <mat-icon [matTooltip]="item.Metodo">{{ getIcon(item.Metodo) }}</mat-icon>
30
+ </div>
31
+ <div class="item-contenido">
32
+ <div class="item-titulo">{{ item.URL }}</div>
33
+ <div class="item-detalles">
34
+ <span class="detalle-dispositivo">
35
+ <mat-icon>{{ item.Dispositivo === 'Móvil' ? 'smartphone' : (item.Dispositivo === 'Tablet' ? 'tablet' : 'laptop') }}</mat-icon>
36
+ {{ item.Dispositivo }}
37
+ </span>
38
+ <span class="detalle-browser"><mat-icon>public</mat-icon> {{ item.Navegador }}</span>
39
+ <span class="detalle-ip"><mat-icon>lan</mat-icon> {{ item.IP }}</span>
40
+ <span class="detalle-pais"><mat-icon>location_on</mat-icon> {{ item.Pais }}</span>
41
+ </div>
42
+ </div>
43
+ <div class="item-fecha">
44
+ <span>{{ item.Fecha | date: 'dd/MM/yyyy' }}</span>
45
+ <strong>{{ item.Fecha | date: 'HH:mm:ss' }}</strong>
46
+ </div>
47
+ </div>
48
+ }
49
+ </div>
50
+ </div>
@@ -0,0 +1,61 @@
1
+ import { Component, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { HttpClient } from '@angular/common/http';
4
+ import { MatIconModule } from '@angular/material/icon';
5
+ import { MatTooltipModule } from '@angular/material/tooltip';
6
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
7
+ import { DatosGlobalesService } from '../../../datos-globales.service';
8
+
9
+ @Component({
10
+ selector: 'app-gestion-actividad',
11
+ standalone: true,
12
+ imports: [CommonModule, MatIconModule, MatTooltipModule, MatProgressBarModule],
13
+ templateUrl: './gestion-actividad.component.html',
14
+ styleUrls: ['./gestion-actividad.component.css']
15
+ })
16
+ export class GestionActividadComponent implements OnInit {
17
+ public actividad: any[] = [];
18
+ public cargando = false;
19
+
20
+ constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService) { }
21
+
22
+ ngOnInit(): void {
23
+ this.obtenerActividad();
24
+ }
25
+
26
+ obtenerActividad(): void {
27
+ this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/ListarActividades`).subscribe({
28
+ next: (datos: any) => {
29
+ this.actividad = datos.body;
30
+ },
31
+ error: (error) => {
32
+ console.error('Error al obtener actividad:', error);
33
+ this.cargando = false;
34
+ },
35
+ complete: () => {
36
+ this.cargando = false;
37
+ }
38
+ });
39
+ // this.cargando = true;
40
+ // this.http.get<any[]>(`${this.datosGlobalesService.ObtenerURL()}Actividad`).subscribe({
41
+ // next: (datos) => {
42
+ // this.actividad = datos;
43
+ // this.cargando = false;
44
+ // },
45
+ // error: (error) => {
46
+ // console.error('Error al obtener actividad:', error);
47
+ // this.cargando = false;
48
+ // }
49
+ // });
50
+ }
51
+
52
+ getIcon(metodo: string): string {
53
+ switch (metodo) {
54
+ case 'GET': return 'search';
55
+ case 'POST': return 'add_circle';
56
+ case 'PUT': return 'edit';
57
+ case 'DELETE': return 'delete';
58
+ default: return 'help_outline';
59
+ }
60
+ }
61
+ }
@@ -1,4 +1,7 @@
1
+ import { Injectable } from '@angular/core';
1
2
  import { MatPaginatorIntl } from '@angular/material/paginator';
3
+
4
+ @Injectable()
2
5
  export class PaginadorPersonalizado extends MatPaginatorIntl {
3
6
  override itemsPerPageLabel = 'Elementos por página:';
4
7
  override nextPageLabel = 'Siguiente página';
@@ -38,6 +38,15 @@
38
38
  border: none;
39
39
  background: none;
40
40
  cursor: pointer;
41
+ transition: all 0.2s ease;
42
+ padding: 4px;
43
+ border-radius: 50%;
44
+ }
45
+
46
+ .botonDeNavegacion:hover {
47
+ background-color: rgba(25, 118, 210, 0.1);
48
+ transform: scale(1.1);
49
+ color: #0b4fce;
41
50
  }
42
51
 
43
52
  /* Contenedor */
@@ -107,27 +116,24 @@
107
116
 
108
117
  .pie-col {
109
118
  display: flex;
110
- gap: 10px;
119
+ gap: 5px;
111
120
  align-items: center;
112
121
  flex: 1;
113
- /* Cada columna ocupará el mismo espacio */
114
122
  overflow: hidden;
115
- /* Evita que el contenido desborde el tercio asignado */
116
123
  }
117
124
 
118
125
  .pie-col.izquierda {
119
126
  justify-content: flex-start;
120
- direction: ltr;
121
127
  }
122
128
 
123
129
  .pie-col.centro {
124
130
  justify-content: center;
131
+ flex: 0 1 auto;
132
+ min-width: 0;
125
133
  }
126
134
 
127
135
  .pie-col.derecha {
128
136
  justify-content: flex-end;
129
- flex-direction: row-reverse;
130
- direction: rtl;
131
137
  }
132
138
 
133
139
  /* Página */
@@ -225,3 +231,43 @@
225
231
  opacity: 1;
226
232
  }
227
233
  }
234
+
235
+ /* Estilos para el mat-menu de aplicaciones */
236
+ ::ng-deep .mat-mdc-menu-panel {
237
+ border-radius: 12px !important;
238
+ border: 1px solid rgba(0, 47, 107, 0.1) !important;
239
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !important;
240
+ margin-top: 8px !important;
241
+ background-color: white !important;
242
+ min-width: 200px !important;
243
+ }
244
+
245
+ ::ng-deep .mat-mdc-menu-item {
246
+ height: 48px !important;
247
+ padding: 0 20px !important;
248
+ transition: background-color 0.2s ease !important;
249
+ }
250
+
251
+ ::ng-deep .mat-mdc-menu-item:hover {
252
+ background-color: #f0f7ff !important;
253
+ }
254
+
255
+ ::ng-deep .mat-mdc-menu-item .mat-icon {
256
+ color: #002f6b !important;
257
+ margin-right: 12px !important;
258
+ font-size: 22px !important;
259
+ width: 22px !important;
260
+ height: 22px !important;
261
+ }
262
+
263
+ ::ng-deep .mat-mdc-menu-item span {
264
+ font-size: 14px !important;
265
+ font-weight: 500 !important;
266
+ color: #444 !important;
267
+ margin-left: 0 !important;
268
+ /* Material sometimes adds weird margins */
269
+ }
270
+
271
+ ::ng-deep .mat-mdc-menu-item:hover span {
272
+ color: #0b4fce !important;
273
+ }
@@ -7,14 +7,14 @@
7
7
  </div>
8
8
  <div class="pie-col derecha">
9
9
  @if(TienePermiso) {
10
- <button class="botonDeNavegacion" matTooltip="Salir" mat-button (click)="Salir()">
10
+ <button class="botonDeNavegacion" matTooltip="Salir" (click)="Salir()">
11
11
  <mat-icon>logout</mat-icon>
12
12
  </button>
13
- <button class="botonDeNavegacion" matTooltip="Perfil" mat-button (click)="irAPerfil()">
13
+ <button class="botonDeNavegacion" matTooltip="Perfil" (click)="irAPerfil()">
14
14
  <mat-icon>person</mat-icon>
15
15
  </button>
16
16
  } @else {
17
- <button class="botonDeNavegacion" matTooltip="Entrar" mat-button (click)="Entrar()">
17
+ <button class="botonDeNavegacion" matTooltip="Entrar" (click)="Entrar()">
18
18
  <mat-icon>login</mat-icon>
19
19
  </button>
20
20
  }
@@ -28,16 +28,16 @@
28
28
  <div [ngClass]="claseDelContenedor">
29
29
  @if(TienePermiso) {
30
30
  <div class="botonesDeNavegacion">
31
- <button class="botonDeNavegacion" matTooltip="Ir atrás" mat-button (click)="irAtras()">
31
+ <button class="botonDeNavegacion" matTooltip="Ir atrás" (click)="irAtras()">
32
32
  <mat-icon>arrow_back</mat-icon>
33
33
  </button>
34
- <button class="botonDeNavegacion" matTooltip="Ir al menú del módulo" mat-button (click)="irAlMenuDeModulo()">
34
+ <button class="botonDeNavegacion" matTooltip="Ir al menú del módulo" (click)="irAlMenuDeModulo()">
35
35
  <mat-icon>menu</mat-icon>
36
36
  </button>
37
- <!-- <button class="botonDeNavegacion" matTooltip="Ir al móudulo padre" mat-button (click)="irAlModuloPadre()">
37
+ <!-- <button class="botonDeNavegacion" matTooltip="Ir al móudulo padre" (click)="irAlModuloPadre()">
38
38
  <mat-icon>apps</mat-icon>
39
39
  </button> -->
40
- <button class="botonDeNavegacion" matTooltip="Ir al inicio" mat-button (click)="irAlInicio()">
40
+ <button class="botonDeNavegacion" matTooltip="Ir al inicio" (click)="irAlInicio()">
41
41
  <mat-icon>home</mat-icon>
42
42
  </button>
43
43
  </div>
@@ -79,8 +79,25 @@
79
79
  </div>
80
80
  </div>
81
81
  <div class="pie-col derecha">
82
+ <button class="botonDeNavegacion" matTooltip="Aplicaciones" [matMenuTriggerFor]="menuAplicaciones">
83
+ <mat-icon>keyboard_arrow_up</mat-icon>
84
+ </button>
85
+
86
+ <mat-menu #menuAplicaciones="matMenu">
87
+ <button mat-menu-item (click)="irASugerencias()">
88
+ <mat-icon>lightbulb</mat-icon>
89
+ <span>Sugerencias</span>
90
+ </button>
91
+ @if(TienePermiso) {
92
+ <button mat-menu-item (click)="irAActividad()">
93
+ <mat-icon>history</mat-icon>
94
+ <span>Actividad de la cuenta</span>
95
+ </button>
96
+ }
97
+ </mat-menu>
98
+
82
99
  @if(TienePermiso) {
83
- <button class="botonDeNavegacion" matTooltip="Mensajes" mat-button (click)="irAMensajes()">
100
+ <button class="botonDeNavegacion" matTooltip="Mensajes" (click)="irAMensajes()">
84
101
  @if (Mensajes.length > 0) {
85
102
  <mat-icon>mark_email_unread</mat-icon>
86
103
  } @else {
@@ -89,21 +106,18 @@
89
106
  </button>
90
107
  }
91
108
  @if(EnlaceDelVideo !== '-') {
92
- <button class="botonDeNavegacion" matTooltip="Vídeo" mat-button (click)="irAVideo()">
109
+ <button class="botonDeNavegacion" matTooltip="Vídeo" (click)="irAVideo()">
93
110
  <mat-icon>live_tv</mat-icon>
94
111
  </button>
95
112
  }
96
113
  @if(EnlaceDelManual !== '-') {
97
- <button class="botonDeNavegacion" matTooltip="Ayuda" mat-button (click)="irAAyuda()">
114
+ <button class="botonDeNavegacion" matTooltip="Ayuda" (click)="irAAyuda()">
98
115
  <mat-icon>help</mat-icon>
99
116
  </button>
100
117
  }
101
- <button class="botonDeNavegacion" matTooltip="Reporte" mat-button (click)="irASoporte()">
118
+ <button class="botonDeNavegacion" matTooltip="Reporte" (click)="irASoporte()">
102
119
  <mat-icon>support_agent</mat-icon>
103
120
  </button>
104
- <button class="botonDeNavegacion" matTooltip="Sugerencias" mat-button (click)="irASugerencias()">
105
- <mat-icon>lightbulb</mat-icon>
106
- </button>
107
121
  </div>
108
122
  </div>
109
123
  </div>
@@ -11,9 +11,11 @@ import { MensajesComponent } from '../../../Componentes/Nucleo/mensajes/mensajes
11
11
  import { MensajeConfirmacionHTMLComponent } from '../../../Componentes/Nucleo/mensaje-confirmacion-html/mensaje-confirmacion-html';
12
12
  import { ReporteDeSugerenciasComponent } from '../../../Componentes/Nucleo/reporte-de-sugerencias/reporte-de-sugerencias.component';
13
13
 
14
+ import { MatMenuModule } from '@angular/material/menu';
15
+
14
16
  @Component({
15
17
  selector: 'app-contenedor-componentes',
16
- imports: [RouterOutlet, MatIconModule, MatTooltipModule, CommonModule],
18
+ imports: [RouterOutlet, MatIconModule, MatTooltipModule, CommonModule, MatMenuModule],
17
19
  templateUrl: './contenedor-componentes.component.html',
18
20
  styleUrl: './contenedor-componentes.component.css'
19
21
  })
@@ -210,6 +212,11 @@ export class ContenedorComponentesComponent implements OnInit, OnDestroy {
210
212
  this.dialog.open(ReporteDeSugerenciasComponent);
211
213
  }
212
214
 
215
+ irAActividad(): void {
216
+ const url = new URL(window.location.href);
217
+ window.location.href = `${url.origin}/Actividad`;
218
+ }
219
+
213
220
  ngOnDestroy() {
214
221
  if (this.intervaloUsuarios) {
215
222
  clearInterval(this.intervaloUsuarios);
@@ -3,7 +3,6 @@ import { provideRouter } from '@angular/router';
3
3
 
4
4
  import { routes } from './app.routes';
5
5
  import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
6
- import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
7
6
  import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
8
7
  import { MatPaginatorIntl } from '@angular/material/paginator';
9
8
  import { PaginadorPersonalizado } from './Componentes/Nucleo/tabla/paginador-personalizado';
@@ -16,10 +15,8 @@ export const appConfig: ApplicationConfig = {
16
15
  { provide: MatPaginatorIntl, useClass: PaginadorPersonalizado },
17
16
  provideZoneChangeDetection({ eventCoalescing: true }),
18
17
  provideRouter(routes),
19
- provideClientHydration(withEventReplay()),
20
- provideAnimationsAsync(),
21
18
  provideHttpClient(withFetch()),
22
- provideClientHydration(),
19
+ provideClientHydration(withEventReplay()),
23
20
  provideCharts(withDefaultRegisterables()),
24
21
  ...AnalyticsModule.forRoot().providers!,
25
22
  provideHttpClient(withInterceptors([AuthInterceptor]))
@@ -1,4 +1,5 @@
1
1
  import { Routes } from '@angular/router';
2
+ import { GestionActividadComponent } from './Componentes/Nucleo/gestion-actividad/gestion-actividad.component';
2
3
  import { GestionTablaComponent } from './Paginas/gestion-tabla/gestion-tabla.component';
3
4
  import { ContenedorPrincipalComponent } from './Paginas/contenedor-principal/contenedor-principal.component';
4
5
  import { GestionTablaJefeComponent } from './Paginas/gestion-tabla-jefe/gestion-tabla-jefe.component';
@@ -7,6 +8,7 @@ import { GestionDeReportesComponent } from './Paginas/gestion-de-reportes/gestio
7
8
  import { GestionIframe1Component } from './Paginas/gestion-iframe1/gestion-iframe1.component';
8
9
 
9
10
  export const routes: Routes = [
11
+ { path: 'Actividad', component: GestionActividadComponent },
10
12
  { path: '', component: ContenedorPrincipalComponent },
11
13
  { path: 'tabla', component: GestionTablaComponent },
12
14
  { path: 'aprobaciones', component: GestionTablaJefeComponent},