utn-cli 2.0.57 → 2.0.59
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/commands/backend.js +6 -5
- package/commands/frontend.js +192 -83
- package/package.json +1 -1
- package/templates/backend/rutas/misc.js +17 -0
- package/templates/backend/servicios/Nucleo/Miscelaneas.js +6 -0
- package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.css +62 -15
- package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.html +34 -24
- package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.ts +26 -12
- package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.component.css +237 -2
- package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.component.html +56 -18
- package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.ts +72 -51
package/commands/backend.js
CHANGED
|
@@ -249,6 +249,7 @@ export async function addServiceBackend(nombreServicio = null, opciones = { cerr
|
|
|
249
249
|
function generarCRUDBackend(servicioPath, rutaPath, nombreServicio, tableInfo) {
|
|
250
250
|
const { dbName, tableName, columns, primaryKey } = tableInfo;
|
|
251
251
|
const dbNameSinDB = dbName.endsWith('-db') ? dbName.slice(0, -3) : dbName;
|
|
252
|
+
const tituloServicio = tableInfo.serviceTitle || nombreServicio;
|
|
252
253
|
|
|
253
254
|
// 1. Generar funciones en el Servicio
|
|
254
255
|
let contenidoServicio = fs.readFileSync(servicioPath, 'utf-8');
|
|
@@ -303,7 +304,7 @@ Router.get('/listar', async (solicitud, respuesta, next) => {
|
|
|
303
304
|
try {
|
|
304
305
|
return respuesta.json({ body: await ${nombreServicio}.listar(), error: undefined });
|
|
305
306
|
} catch (error) {
|
|
306
|
-
const MensajeDeError = 'No fue posible listar los registros';
|
|
307
|
+
const MensajeDeError = 'No fue posible listar los registros de ${tituloServicio}';
|
|
307
308
|
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, \`Dirección IP: \${solicitud.ip}\`));
|
|
308
309
|
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
309
310
|
}
|
|
@@ -322,7 +323,7 @@ Router.post('/agregar', async (solicitud, respuesta, next) => {
|
|
|
322
323
|
Datos.LastUser = await Miscelaneo.generarLastUser(solicitud);
|
|
323
324
|
return respuesta.json({ body: await ${nombreServicio}.agregar(Datos), error: undefined });
|
|
324
325
|
} catch (error) {
|
|
325
|
-
const MensajeDeError = 'No fue posible agregar el registro';
|
|
326
|
+
const MensajeDeError = 'No fue posible agregar el registro de ${tituloServicio}';
|
|
326
327
|
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, \`Dirección IP: \${solicitud.ip}\`));
|
|
327
328
|
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
328
329
|
}
|
|
@@ -341,7 +342,7 @@ Router.post('/actualizar', async (solicitud, respuesta, next) => {
|
|
|
341
342
|
Datos.LastUser = await Miscelaneo.generarLastUser(solicitud);
|
|
342
343
|
return respuesta.json({ body: await ${nombreServicio}.actualizar(Datos), error: undefined });
|
|
343
344
|
} catch (error) {
|
|
344
|
-
const MensajeDeError = 'No fue posible actualizar el registro';
|
|
345
|
+
const MensajeDeError = 'No fue posible actualizar el registro de ${tituloServicio}';
|
|
345
346
|
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, \`Dirección IP: \${solicitud.ip}\`));
|
|
346
347
|
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
347
348
|
}
|
|
@@ -358,7 +359,7 @@ Router.post('/borrar', async (solicitud, respuesta, next) => {
|
|
|
358
359
|
try {
|
|
359
360
|
return respuesta.json({ body: await ${nombreServicio}.borrar(solicitud.body), error: undefined });
|
|
360
361
|
} catch (error) {
|
|
361
|
-
const MensajeDeError = 'No fue posible borrar el registro';
|
|
362
|
+
const MensajeDeError = 'No fue posible borrar el registro de ${tituloServicio}';
|
|
362
363
|
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, \`Dirección IP: \${solicitud.ip}\`));
|
|
363
364
|
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
364
365
|
}
|
|
@@ -377,7 +378,7 @@ Router.get('/reporte', async (solicitud, respuesta, next) => {
|
|
|
377
378
|
respuesta.attachment('Datos.csv');
|
|
378
379
|
return respuesta.send(encodeURI(await ${nombreServicio}.reporte()));
|
|
379
380
|
} catch (error) {
|
|
380
|
-
const MensajeDeError = 'No fue posible generar el reporte';
|
|
381
|
+
const MensajeDeError = 'No fue posible generar el reporte de ${tituloServicio}';
|
|
381
382
|
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, \`Dirección IP: \${solicitud.ip}\`));
|
|
382
383
|
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
383
384
|
}
|
package/commands/frontend.js
CHANGED
|
@@ -86,6 +86,8 @@ export async function cloneFrontendComponent(nombreComponente = null, tituloCard
|
|
|
86
86
|
const nombreCapitalizado = nombre.charAt(0).toUpperCase() + nombre.slice(1);
|
|
87
87
|
const nombreClase = `GestionTabla${nombreCapitalizado}Component`;
|
|
88
88
|
const nombreRuta = `gestion-tabla-${nombreLower}`;
|
|
89
|
+
const nombreModal = `Guardar${nombreCapitalizado}Component`;
|
|
90
|
+
const nombreCarpetaModal = `guardar-${nombreLower}`;
|
|
89
91
|
|
|
90
92
|
const rutaFuente = path.join(__dirname, '../templates/frontend', 'src', 'app', 'Paginas', 'gestion-tabla-XYZ');
|
|
91
93
|
const rutaDestino = path.join(process.cwd(), 'src', 'app', 'Paginas', nombreRuta);
|
|
@@ -102,103 +104,96 @@ export async function cloneFrontendComponent(nombreComponente = null, tituloCard
|
|
|
102
104
|
process.exit(1);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
console.log(`Clonando carpeta...`);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
console.log(`Clonando carpeta de gestión...`);
|
|
108
|
+
copiarYReemplazar(rutaFuente, rutaDestino, nombreLower, nombreClase, tableInfo, nombreModal, nombreCarpetaModal);
|
|
109
|
+
|
|
110
|
+
// Clonar el modal si tenemos tableInfo
|
|
111
|
+
if (tableInfo) {
|
|
112
|
+
const rutaFuenteModal = path.join(__dirname, '../templates/frontend', 'src', 'app', 'Componentes', 'guardar-incapacidad');
|
|
113
|
+
const rutaDestinoModal = path.join(process.cwd(), 'src', 'app', 'Componentes', nombreCarpetaModal);
|
|
114
|
+
|
|
115
|
+
if (fs.existsSync(rutaFuenteModal)) {
|
|
116
|
+
console.log(`Clonando carpeta del modal...`);
|
|
117
|
+
copiarYReemplazarModal(rutaFuenteModal, rutaDestinoModal, nombreLower, nombreModal, tableInfo);
|
|
110
118
|
}
|
|
111
|
-
const entradas = fs.readdirSync(fuente, { withFileTypes: true });
|
|
112
|
-
entradas.forEach((entrada) => {
|
|
113
|
-
const nombreOriginal = entrada.name;
|
|
114
|
-
const nombreNuevo = nombreOriginal.replace(/XYZ/g, nombreLower);
|
|
115
|
-
const rutaFuente = path.join(fuente, nombreOriginal);
|
|
116
|
-
const rutaDestino = path.join(destino, nombreNuevo);
|
|
117
|
-
|
|
118
|
-
if (entrada.isDirectory()) {
|
|
119
|
-
copiarYReemplazar(rutaFuente, rutaDestino);
|
|
120
|
-
} else if (entrada.isFile()) {
|
|
121
|
-
let contenido = fs.readFileSync(rutaFuente, 'utf-8');
|
|
122
|
-
|
|
123
|
-
// Reemplazos base
|
|
124
|
-
contenido = contenido.replace(/GestionTablaXYZComponent/g, nombreClase);
|
|
125
|
-
contenido = contenido.replace(/XYZ/g, nombreLower);
|
|
126
|
-
|
|
127
|
-
// Si tenemos información de la tabla, personalizamos el componente
|
|
128
|
-
if (tableInfo && entrada.name.endsWith('.ts')) {
|
|
129
|
-
contenido = personalizarComponenteFrontend(contenido, tableInfo, nombreLower);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
fs.writeFileSync(rutaDestino, contenido);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
119
|
}
|
|
136
120
|
|
|
137
|
-
copiarYReemplazar(rutaFuente, rutaDestino);
|
|
138
121
|
console.log(`Componente creado en: ${rutaDestino}`);
|
|
139
122
|
|
|
140
123
|
// --- Modificar app.routes.ts ---
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (fs.existsSync(rutaRoutes)) {
|
|
144
|
-
let contenidoRoutes = fs.readFileSync(rutaRoutes, 'utf-8');
|
|
145
|
-
|
|
146
|
-
const lineaImport = `import { ${nombreClase} } from './Paginas/${nombreRuta}/${nombreRuta}.component';`;
|
|
147
|
-
const ultimoImportIndex = contenidoRoutes.lastIndexOf('import {');
|
|
148
|
-
const finUltimoImport = contenidoRoutes.indexOf(';', ultimoImportIndex);
|
|
149
|
-
|
|
150
|
-
if (finUltimoImport !== -1) {
|
|
151
|
-
contenidoRoutes = contenidoRoutes.slice(0, finUltimoImport + 1) + '\n' + lineaImport + contenidoRoutes.slice(finUltimoImport + 1);
|
|
152
|
-
} else {
|
|
153
|
-
contenidoRoutes = lineaImport + '\n' + contenidoRoutes;
|
|
154
|
-
}
|
|
124
|
+
// ... (rest of the code for routes and html update remains similar, but using nombreClase and nombreRuta)
|
|
125
|
+
actualizarArchivosConfiguracion(nombreClase, nombreRuta, titulo, descripcion);
|
|
155
126
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const nuevaRuta = ` { path: '${nombreRuta}', component: ${nombreClase} },`;
|
|
159
|
-
const textoAntesDeCierre = contenidoRoutes.slice(0, cierreArray).trimEnd();
|
|
160
|
-
const necesitaComa = !textoAntesDeCierre.endsWith(',') && !textoAntesDeCierre.endsWith('[');
|
|
161
|
-
|
|
162
|
-
contenidoRoutes = contenidoRoutes.slice(0, cierreArray) +
|
|
163
|
-
(necesitaComa ? ',' : '') + '\n' +
|
|
164
|
-
nuevaRuta + '\n' +
|
|
165
|
-
contenidoRoutes.slice(cierreArray);
|
|
166
|
-
|
|
167
|
-
fs.writeFileSync(rutaRoutes, contenidoRoutes);
|
|
168
|
-
console.log('Ruta agregada exitosamente.');
|
|
169
|
-
} else {
|
|
170
|
-
console.error('No se pudo encontrar el array de rutas en app.routes.ts');
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
console.error(`No se encontró el archivo de rutas en: ${rutaRoutes}`);
|
|
127
|
+
if (opciones.cerrarAlFinalizar) {
|
|
128
|
+
closeReadLine();
|
|
174
129
|
}
|
|
130
|
+
}
|
|
175
131
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
const
|
|
132
|
+
function copiarYReemplazar(fuente, destino, nombreLower, nombreClase, tableInfo, nombreModal, nombreCarpetaModal) {
|
|
133
|
+
if (!fs.existsSync(destino)) {
|
|
134
|
+
fs.mkdirSync(destino, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
const entradas = fs.readdirSync(fuente, { withFileTypes: true });
|
|
137
|
+
entradas.forEach((entrada) => {
|
|
138
|
+
const nombreOriginal = entrada.name;
|
|
139
|
+
const nombreNuevo = nombreOriginal.replace(/XYZ/g, nombreLower);
|
|
140
|
+
const rutaFuente = path.join(fuente, nombreOriginal);
|
|
141
|
+
const rutaDestino = path.join(destino, nombreNuevo);
|
|
142
|
+
|
|
143
|
+
if (entrada.isDirectory()) {
|
|
144
|
+
copiarYReemplazar(rutaFuente, rutaDestino, nombreLower, nombreClase, tableInfo, nombreModal, nombreCarpetaModal);
|
|
145
|
+
} else if (entrada.isFile()) {
|
|
146
|
+
let contenido = fs.readFileSync(rutaFuente, 'utf-8');
|
|
147
|
+
|
|
148
|
+
// Reemplazos base
|
|
149
|
+
contenido = contenido.replace(/GestionTablaXYZComponent/g, nombreClase);
|
|
150
|
+
contenido = contenido.replace(/XYZ/g, nombreLower);
|
|
151
|
+
|
|
152
|
+
// Si tenemos información de la tabla, personalizamos el componente
|
|
153
|
+
if (tableInfo) {
|
|
154
|
+
if (entrada.name.endsWith('.ts')) {
|
|
155
|
+
contenido = personalizarComponenteFrontend(contenido, tableInfo, nombreLower, nombreModal, nombreCarpetaModal);
|
|
156
|
+
} else if (entrada.name.endsWith('.html')) {
|
|
157
|
+
contenido = contenido.replace(/app-gestion-tabla-XYZ/g, `app-gestion-tabla-${nombreLower}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
185
160
|
|
|
186
|
-
|
|
187
|
-
contenidoHtml = contenidoHtml.slice(0, ultimoDiv) + '\n' + nuevaTarjeta + '\n' + contenidoHtml.slice(ultimoDiv);
|
|
188
|
-
fs.writeFileSync(rutaHtml, contenidoHtml);
|
|
189
|
-
console.log('Tarjeta agregada exitosamente.');
|
|
190
|
-
} else {
|
|
191
|
-
console.error('No se encontró un </div> de cierre en el HTML del contenedor principal.');
|
|
161
|
+
fs.writeFileSync(rutaDestino, contenido);
|
|
192
162
|
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function copiarYReemplazarModal(fuente, destino, nombreLower, nombreModal, tableInfo) {
|
|
167
|
+
if (!fs.existsSync(destino)) {
|
|
168
|
+
fs.mkdirSync(destino, { recursive: true });
|
|
198
169
|
}
|
|
170
|
+
const entradas = fs.readdirSync(fuente, { withFileTypes: true });
|
|
171
|
+
entradas.forEach((entrada) => {
|
|
172
|
+
const nombreOriginal = entrada.name;
|
|
173
|
+
const nombreNuevo = nombreOriginal.replace(/guardar-incapacidad/g, `guardar-${nombreLower}`);
|
|
174
|
+
const rutaFuente = path.join(fuente, nombreOriginal);
|
|
175
|
+
const rutaDestino = path.join(destino, nombreNuevo);
|
|
176
|
+
|
|
177
|
+
if (entrada.isDirectory()) {
|
|
178
|
+
copiarYReemplazarModal(rutaFuente, rutaDestino, nombreLower, nombreModal, tableInfo);
|
|
179
|
+
} else if (entrada.isFile()) {
|
|
180
|
+
let contenido = fs.readFileSync(rutaFuente, 'utf-8');
|
|
181
|
+
|
|
182
|
+
// Reemplazos base
|
|
183
|
+
contenido = contenido.replace(/GuardarIncapacidadComponent/g, nombreModal);
|
|
184
|
+
contenido = contenido.replace(/guardar-incapacidad/g, `guardar-${nombreLower}`);
|
|
185
|
+
contenido = contenido.replace(/Incapacidad/g, nombreLower.charAt(0).toUpperCase() + nombreLower.slice(1));
|
|
186
|
+
contenido = contenido.replace(/incapacidad/g, nombreLower);
|
|
187
|
+
|
|
188
|
+
// Personalización basada en tableInfo
|
|
189
|
+
contenido = personalizarModalFrontend(contenido, tableInfo, entrada.name);
|
|
190
|
+
|
|
191
|
+
fs.writeFileSync(rutaDestino, contenido);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
199
194
|
}
|
|
200
195
|
|
|
201
|
-
function personalizarComponenteFrontend(contenido, tableInfo, nombreServicioLower) {
|
|
196
|
+
function personalizarComponenteFrontend(contenido, tableInfo, nombreServicioLower, nombreModal, nombreCarpetaModal) {
|
|
202
197
|
const { columns, primaryKey } = tableInfo;
|
|
203
198
|
|
|
204
199
|
// 1. Reemplazar columnas
|
|
@@ -216,9 +211,123 @@ function personalizarComponenteFrontend(contenido, tableInfo, nombreServicioLowe
|
|
|
216
211
|
const endpointRegex = new RegExp('licencias/', 'g');
|
|
217
212
|
contenido = contenido.replace(endpointRegex, `${nombreServicioLower}/`);
|
|
218
213
|
|
|
214
|
+
// 4. Reemplazar el Import y uso del modal
|
|
215
|
+
contenido = contenido.replace(/import \{ GuardarIncapacidadComponent \} from '\.\.\/\.\.\/Componentes\/guardar-incapacidad\/guardar-incapacidad\.component';/g,
|
|
216
|
+
`import { ${nombreModal} } from '../../Componentes/${nombreCarpetaModal}/${nombreCarpetaModal}.component';`);
|
|
217
|
+
contenido = contenido.replace(/GuardarIncapacidadComponent/g, nombreModal);
|
|
218
|
+
|
|
219
|
+
// Usar 'Registro' como clave en el modal
|
|
220
|
+
contenido = contenido.replace(/Incapacidad: incapacidad/g, 'Registro: incapacidad');
|
|
221
|
+
|
|
222
|
+
// 5. Limpiar cosas específicas de Incapacidades si existen
|
|
223
|
+
contenido = contenido.replace(/this\.obtenerTiposLicencias\(\);/g, '');
|
|
224
|
+
contenido = contenido.replace(/obtenerTiposLicencias\(\) \{[\s\S]*?\}/g, '');
|
|
225
|
+
contenido = contenido.replace(/public tiposDeLicencias: any\[\] = \[\];/g, '');
|
|
226
|
+
contenido = contenido.replace(/TiposDeLicencias: this\.tiposDeLicencias/g, '');
|
|
227
|
+
|
|
219
228
|
return contenido;
|
|
220
229
|
}
|
|
221
230
|
|
|
231
|
+
function personalizarModalFrontend(contenido, tableInfo, fileName) {
|
|
232
|
+
const { columns, primaryKey } = tableInfo;
|
|
233
|
+
|
|
234
|
+
if (fileName.endsWith('.ts')) {
|
|
235
|
+
// Generar controles del formulario
|
|
236
|
+
const controles = columns.map(col => {
|
|
237
|
+
return ` ${col.name}Control: [this.data.Registro?.${col.name}, Validators.required]`;
|
|
238
|
+
}).join(',\n');
|
|
239
|
+
|
|
240
|
+
contenido = contenido.replace(/grupoFormulario = this\._formBuilder\.group\(\{[\s\S]*?\}\);/,
|
|
241
|
+
`grupoFormulario = this._formBuilder.group({\n${controles}\n });`);
|
|
242
|
+
|
|
243
|
+
// Generar objeto de retorno en Guardar()
|
|
244
|
+
const objetoRetorno = columns.map(col => {
|
|
245
|
+
return ` ${col.name}: controles.${col.name}Control.value`;
|
|
246
|
+
}).join(',\n');
|
|
247
|
+
|
|
248
|
+
contenido = contenido.replace(/let incapacidad = this\.grupoFormulario\.controls;[\s\S]*?this\.dialogRef\.close\(\{[\s\S]*?\}\);/,
|
|
249
|
+
`let controles = this.grupoFormulario.controls;\n this.dialogRef.close({\n${objetoRetorno},\n ${primaryKey}: this.data.Registro?.${primaryKey}\n });`);
|
|
250
|
+
|
|
251
|
+
// Quitar TiposDeLicencias si existe
|
|
252
|
+
contenido = contenido.replace(/TiposDeLicencias = this\.data\.TiposDeLicencias;/g, '');
|
|
253
|
+
contenido = contenido.replace(/readonly Incapacidad = this\.data\.Incapacidad \?\? \{\};/g, '');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (fileName.endsWith('.html')) {
|
|
257
|
+
// Generar campos del formulario
|
|
258
|
+
const campos = columns.map(col => {
|
|
259
|
+
const type = col.type.includes('INT') ? 'number' : (col.type.includes('DATE') ? 'date' : 'text');
|
|
260
|
+
return ` <mat-form-field class="campo_mitad">
|
|
261
|
+
<mat-label>${col.alias}</mat-label>
|
|
262
|
+
<input matInput ${type !== 'text' ? 'type="' + type + '"' : ''} formControlName="${col.name}Control" required />
|
|
263
|
+
</mat-form-field>`;
|
|
264
|
+
}).join('\n');
|
|
265
|
+
|
|
266
|
+
contenido = contenido.replace(/<form \[formGroup\]="grupoFormulario">[\s\S]*?<\/form>/,
|
|
267
|
+
`<form [formGroup]="grupoFormulario">\n <div class="fila">\n${campos}\n </div>\n </form>`);
|
|
268
|
+
|
|
269
|
+
// Reemplazar el título
|
|
270
|
+
const titulo = tableInfo.serviceTitle || 'Registro';
|
|
271
|
+
contenido = contenido.replace(/<h2>[\s\S]*?<\/h2>/, `<h2>{{data.Registro ? 'Editar' : 'Agregar'}} ${titulo}</h2>`);
|
|
272
|
+
contenido = contenido.replace(/{{Incapacidad\.NumeroBoleta\?'Guardar':'Agregar'}}/, `{{data.Registro ? 'Guardar' : 'Agregar'}}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return contenido;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function actualizarArchivosConfiguracion(nombreClase, nombreRuta, titulo, descripcion) {
|
|
279
|
+
// --- Modificar app.routes.ts ---
|
|
280
|
+
console.log('Actualizando app.routes.ts...');
|
|
281
|
+
const rutaRoutes = path.join(process.cwd(), 'src', 'app', 'app.routes.ts');
|
|
282
|
+
if (fs.existsSync(rutaRoutes)) {
|
|
283
|
+
let contenidoRoutes = fs.readFileSync(rutaRoutes, 'utf-8');
|
|
284
|
+
|
|
285
|
+
const lineaImport = `import { ${nombreClase} } from './Paginas/${nombreRuta}/${nombreRuta}.component';`;
|
|
286
|
+
if (!contenidoRoutes.includes(lineaImport)) {
|
|
287
|
+
const ultimoImportIndex = contenidoRoutes.lastIndexOf('import {');
|
|
288
|
+
const finUltimoImport = contenidoRoutes.indexOf(';', ultimoImportIndex);
|
|
289
|
+
|
|
290
|
+
if (finUltimoImport !== -1) {
|
|
291
|
+
contenidoRoutes = contenidoRoutes.slice(0, finUltimoImport + 1) + '\n' + lineaImport + contenidoRoutes.slice(finUltimoImport + 1);
|
|
292
|
+
} else {
|
|
293
|
+
contenidoRoutes = lineaImport + '\n' + contenidoRoutes;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!contenidoRoutes.includes(`path: '${nombreRuta}'`)) {
|
|
298
|
+
const cierreArray = contenidoRoutes.lastIndexOf('];');
|
|
299
|
+
if (cierreArray !== -1) {
|
|
300
|
+
const nuevaRuta = ` { path: '${nombreRuta}', component: ${nombreClase} },`;
|
|
301
|
+
const textoAntesDeCierre = contenidoRoutes.slice(0, cierreArray).trimEnd();
|
|
302
|
+
const necesitaComa = !textoAntesDeCierre.endsWith(',') && !textoAntesDeCierre.endsWith('[');
|
|
303
|
+
|
|
304
|
+
contenidoRoutes = contenidoRoutes.slice(0, cierreArray) +
|
|
305
|
+
(necesitaComa ? ',' : '') + '\n' +
|
|
306
|
+
nuevaRuta + '\n' +
|
|
307
|
+
contenidoRoutes.slice(cierreArray);
|
|
308
|
+
|
|
309
|
+
fs.writeFileSync(rutaRoutes, contenidoRoutes);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// --- Modificar contenedor-principal.component.html ---
|
|
315
|
+
console.log('Actualizando contenedor-principal.component.html...');
|
|
316
|
+
const rutaHtml = path.join(process.cwd(), 'src', 'app', 'Paginas', 'contenedor-principal', 'contenedor-principal.component.html');
|
|
317
|
+
if (fs.existsSync(rutaHtml)) {
|
|
318
|
+
let contenidoHtml = fs.readFileSync(rutaHtml, 'utf-8');
|
|
319
|
+
if (!contenidoHtml.includes(`rutaASeguir]="'${nombreRuta}'"`)) {
|
|
320
|
+
const nuevaTarjeta = ` <app-tarjeta [rutaASeguir]="'${nombreRuta}'" titulo="${titulo}" descripcion="${descripcion}" icono="table_chart"></app-tarjeta>`;
|
|
321
|
+
const ultimoDiv = contenidoHtml.lastIndexOf('</div>');
|
|
322
|
+
if (ultimoDiv !== -1) {
|
|
323
|
+
contenidoHtml = contenidoHtml.slice(0, ultimoDiv) + '\n' + nuevaTarjeta + '\n' + contenidoHtml.slice(ultimoDiv);
|
|
324
|
+
fs.writeFileSync(rutaHtml, contenidoHtml);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
222
331
|
export function showFrontendVersion() {
|
|
223
332
|
mostrarVersion(path.join(__dirname, '../package.json'));
|
|
224
333
|
closeReadLine();
|
package/package.json
CHANGED
|
@@ -353,6 +353,23 @@ Router.get('/cerrarSesion', async (solicitud, respuesta, next) => {
|
|
|
353
353
|
}
|
|
354
354
|
});
|
|
355
355
|
|
|
356
|
+
Router.delete('/cerrarSesionPorToken/:Token', async (solicitud, respuesta, next) => {
|
|
357
|
+
try {
|
|
358
|
+
if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
|
|
359
|
+
try {
|
|
360
|
+
return respuesta.json({ body: await Miscelaneo.cerrarSesionPorToken(solicitud.params.Token), error: undefined });
|
|
361
|
+
} catch (error) {
|
|
362
|
+
const MensajeDeError = 'No fue posible cerrar la sesión';
|
|
363
|
+
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
|
|
364
|
+
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
|
|
368
|
+
} catch (error) {
|
|
369
|
+
next(error);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
356
373
|
Router.get('/obtenerEnlaceDePerfil', async (solicitud, respuesta, next) => {
|
|
357
374
|
try {
|
|
358
375
|
if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
|
|
@@ -66,6 +66,7 @@ class Miscelaneo {
|
|
|
66
66
|
JSON_VALUE(Solicitud, '$.method') AS Metodo,
|
|
67
67
|
JSON_VALUE(Solicitud, '$.originalUrl') AS URL,
|
|
68
68
|
IFNULL(JSON_VALUE(Solicitud, '$.country'), 'Costa Rica') AS Pais,
|
|
69
|
+
SUBSTRING_INDEX(JSON_VALUE(Solicitud, '$.headers.authorization'), ' ', -1) AS Token,
|
|
69
70
|
CASE
|
|
70
71
|
WHEN JSON_VALUE(Solicitud, '$.userAgent') LIKE '%Mobile%' THEN 'Móvil'
|
|
71
72
|
WHEN JSON_VALUE(Solicitud, '$.userAgent') LIKE '%Android%' AND JSON_VALUE(Solicitud, '$.userAgent') NOT LIKE '%Mobile%' THEN 'Tablet'
|
|
@@ -85,6 +86,11 @@ class Miscelaneo {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
async cerrarSesionPorToken(Token) {
|
|
90
|
+
await ejecutarConsultaSIGU("DELETE FROM `SIGU`.`SIGU_Sesiones` WHERE `Token` = ?", [Token]);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
async UsuariosActuales() {
|
|
89
95
|
const ConexionSigu = await crearObjetoConexionSIGU();
|
|
90
96
|
const Actuales = await ConexionSigu.query("SELECT COUNT(DISTINCT `Identificador`) AS `Total` FROM `SIGU`.`SIGU_Sesiones` WHERE `LastUpdate` >= NOW() - INTERVAL 2 HOUR");
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
border-radius: 12px;
|
|
11
11
|
margin-bottom: 20px;
|
|
12
12
|
border: 1px solid #e0e6ed;
|
|
13
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
13
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
14
14
|
text-align: left;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
border: 1px solid #e0e6ed;
|
|
81
81
|
border-radius: 12px;
|
|
82
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);
|
|
83
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
84
84
|
text-align: left;
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -110,17 +110,37 @@
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/* Colores por método */
|
|
113
|
-
.metodo-get {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
.metodo-get {
|
|
114
|
+
color: #0069B4;
|
|
115
|
+
background-color: #e3f2fd;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.metodo-post {
|
|
119
|
+
color: #518a5f;
|
|
120
|
+
background-color: #e8f5e9;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.metodo-put {
|
|
124
|
+
color: #FFA757;
|
|
125
|
+
background-color: #fff3e0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.metodo-delete {
|
|
129
|
+
color: #F82617;
|
|
130
|
+
background-color: #ffebee;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.metodo-default {
|
|
134
|
+
color: #7d7d7d;
|
|
135
|
+
background-color: #f5f5f5;
|
|
136
|
+
}
|
|
118
137
|
|
|
119
138
|
.item-contenido {
|
|
120
139
|
flex: 1;
|
|
121
140
|
display: flex;
|
|
122
141
|
flex-direction: column;
|
|
123
|
-
min-width: 0;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
/* Evita que el flex desborde */
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
.item-titulo {
|
|
@@ -129,6 +149,22 @@
|
|
|
129
149
|
font-weight: 600;
|
|
130
150
|
word-break: break-all;
|
|
131
151
|
margin-bottom: 2px;
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
flex-wrap: wrap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.badge-sesion-actual {
|
|
159
|
+
background-color: #e8f5e9;
|
|
160
|
+
color: #2e7d32;
|
|
161
|
+
font-size: 10px;
|
|
162
|
+
padding: 2px 8px;
|
|
163
|
+
border-radius: 12px;
|
|
164
|
+
border: 1px solid #c8e6c9;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
white-space: nowrap;
|
|
132
168
|
}
|
|
133
169
|
|
|
134
170
|
.item-detalles {
|
|
@@ -152,10 +188,21 @@
|
|
|
152
188
|
height: 14px;
|
|
153
189
|
}
|
|
154
190
|
|
|
155
|
-
.detalle-dispositivo mat-icon {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
191
|
+
.detalle-dispositivo mat-icon {
|
|
192
|
+
color: #673ab7;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.detalle-browser mat-icon {
|
|
196
|
+
color: #5f6368;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.detalle-ip mat-icon {
|
|
200
|
+
color: #0069B4;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.detalle-pais mat-icon {
|
|
204
|
+
color: #F87600;
|
|
205
|
+
}
|
|
159
206
|
|
|
160
207
|
.item-fecha {
|
|
161
208
|
font-size: 11px;
|
|
@@ -180,14 +227,14 @@ mat-progress-bar {
|
|
|
180
227
|
flex-direction: column;
|
|
181
228
|
align-items: flex-start;
|
|
182
229
|
}
|
|
183
|
-
|
|
230
|
+
|
|
184
231
|
.item-icono {
|
|
185
232
|
margin-bottom: 12px;
|
|
186
233
|
}
|
|
187
|
-
|
|
234
|
+
|
|
188
235
|
.item-fecha {
|
|
189
236
|
margin-left: 0;
|
|
190
237
|
margin-top: 12px;
|
|
191
238
|
align-items: flex-start;
|
|
192
239
|
}
|
|
193
|
-
}
|
|
240
|
+
}
|
|
@@ -2,49 +2,59 @@
|
|
|
2
2
|
<div class="header-actividad">
|
|
3
3
|
<h2><mat-icon>history</mat-icon> Actividad reciente de la cuenta</h2>
|
|
4
4
|
<p>Historial de acciones realizadas en el sistema</p>
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
<div class="mensaje-informativo">
|
|
7
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
|
|
8
|
+
<p>Si tiene dudas sobre la información que ve en pantalla, por favor comparta una captura de pantalla al correo
|
|
9
|
+
<strong>soporte@utn.ac.cr</strong>.</p>
|
|
9
10
|
</div>
|
|
10
11
|
</div>
|
|
11
12
|
|
|
12
13
|
@if(cargando){
|
|
13
|
-
|
|
14
|
+
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
<div class="lista-actividad">
|
|
17
18
|
@if(actividad.length === 0 && !cargando) {
|
|
18
|
-
|
|
19
|
+
<p class="sin-datos">No hay actividad reciente.</p>
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
@for (item of actividad; track item.Consecutivo) {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
<div class="actividad-item">
|
|
24
|
+
<div class="item-icono" [ngClass]="{
|
|
24
25
|
'metodo-get': item.Metodo === 'GET',
|
|
25
26
|
'metodo-post': item.Metodo === 'POST',
|
|
26
27
|
'metodo-put': item.Metodo === 'PUT',
|
|
27
28
|
'metodo-delete': item.Metodo === 'DELETE'
|
|
28
29
|
}">
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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>
|
|
30
|
+
<mat-icon [matTooltip]="item.Metodo">{{ getIcon(item.Metodo) }}</mat-icon>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="item-contenido">
|
|
33
|
+
<div class="item-titulo">
|
|
34
|
+
{{ item.URL }}
|
|
35
|
+
@if(esSesionActual(item.Token)){
|
|
36
|
+
<span class="badge-sesion-actual">Sesión actual</span>
|
|
37
|
+
}
|
|
42
38
|
</div>
|
|
43
|
-
<div class="item-
|
|
44
|
-
<span
|
|
45
|
-
|
|
39
|
+
<div class="item-detalles">
|
|
40
|
+
<span class="detalle-dispositivo">
|
|
41
|
+
<mat-icon>{{ item.Dispositivo === 'Móvil' ? 'smartphone' : (item.Dispositivo === 'Tablet' ? 'tablet' :
|
|
42
|
+
'laptop') }}</mat-icon>
|
|
43
|
+
{{ item.Dispositivo }}
|
|
44
|
+
</span>
|
|
45
|
+
<span class="detalle-browser"><mat-icon>public</mat-icon> {{ item.Navegador }}</span>
|
|
46
|
+
<span class="detalle-ip"><mat-icon>lan</mat-icon> {{ item.IP }}</span>
|
|
47
|
+
<span class="detalle-pais"><mat-icon>location_on</mat-icon> {{ item.Pais }}</span>
|
|
46
48
|
</div>
|
|
47
49
|
</div>
|
|
50
|
+
<div class="item-fecha">
|
|
51
|
+
<span>{{ item.Fecha | date: 'dd/MM/yyyy' }}</span>
|
|
52
|
+
<strong>{{ item.Fecha | date: 'HH:mm:ss' }}</strong>
|
|
53
|
+
<button mat-icon-button color="warn" matTooltip="Cerrar esta sesión" (click)="cerrarSesion(item.Token)">
|
|
54
|
+
<mat-icon>logout</mat-icon>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
48
58
|
}
|
|
49
59
|
</div>
|
|
50
|
-
</div>
|
|
60
|
+
</div>
|
|
@@ -4,26 +4,30 @@ import { HttpClient } from '@angular/common/http';
|
|
|
4
4
|
import { MatIconModule } from '@angular/material/icon';
|
|
5
5
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
6
6
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
7
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
7
8
|
import { DatosGlobalesService } from '../../../datos-globales.service';
|
|
8
9
|
|
|
9
10
|
@Component({
|
|
10
11
|
selector: 'app-gestion-actividad',
|
|
11
12
|
standalone: true,
|
|
12
|
-
imports: [CommonModule, MatIconModule, MatTooltipModule, MatProgressBarModule],
|
|
13
|
+
imports: [CommonModule, MatIconModule, MatTooltipModule, MatProgressBarModule, MatButtonModule],
|
|
13
14
|
templateUrl: './gestion-actividad.component.html',
|
|
14
15
|
styleUrls: ['./gestion-actividad.component.css']
|
|
15
16
|
})
|
|
16
17
|
export class GestionActividadComponent implements OnInit {
|
|
17
18
|
public actividad: any[] = [];
|
|
18
19
|
public cargando = false;
|
|
20
|
+
private tokenActual = '';
|
|
19
21
|
|
|
20
22
|
constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService) { }
|
|
21
23
|
|
|
22
24
|
ngOnInit(): void {
|
|
25
|
+
this.tokenActual = this.datosGlobalesService.ObtenerToken().split(' ')[1] || '';
|
|
23
26
|
this.obtenerActividad();
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
obtenerActividad(): void {
|
|
30
|
+
this.cargando = true;
|
|
27
31
|
this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/ListarActividades`).subscribe({
|
|
28
32
|
next: (datos: any) => {
|
|
29
33
|
this.actividad = datos.body;
|
|
@@ -36,17 +40,27 @@ export class GestionActividadComponent implements OnInit {
|
|
|
36
40
|
this.cargando = false;
|
|
37
41
|
}
|
|
38
42
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
cerrarSesion(token: string): void {
|
|
46
|
+
if (confirm('¿Está seguro de que desea cerrar esta sesión?')) {
|
|
47
|
+
this.http.delete(`${this.datosGlobalesService.ObtenerURL()}misc/cerrarSesionPorToken/${token}`).subscribe({
|
|
48
|
+
next: () => {
|
|
49
|
+
if (token === this.tokenActual) {
|
|
50
|
+
this.datosGlobalesService.RedirigirALogin();
|
|
51
|
+
} else {
|
|
52
|
+
this.obtenerActividad();
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
error: (error) => {
|
|
56
|
+
console.error('Error al cerrar sesión:', error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
esSesionActual(token: string): boolean {
|
|
63
|
+
return token === this.tokenActual;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
getIcon(metodo: string): string {
|
|
@@ -1,5 +1,240 @@
|
|
|
1
|
-
.
|
|
2
|
-
|
|
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;
|
|
3
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;
|
|
4
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 {
|
|
114
|
+
color: #0069B4;
|
|
115
|
+
background-color: #e3f2fd;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.metodo-post {
|
|
119
|
+
color: #518a5f;
|
|
120
|
+
background-color: #e8f5e9;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.metodo-put {
|
|
124
|
+
color: #FFA757;
|
|
125
|
+
background-color: #fff3e0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.metodo-delete {
|
|
129
|
+
color: #F82617;
|
|
130
|
+
background-color: #ffebee;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.metodo-default {
|
|
134
|
+
color: #7d7d7d;
|
|
135
|
+
background-color: #f5f5f5;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.item-contenido {
|
|
139
|
+
flex: 1;
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
/* Evita que el flex desborde */
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.item-titulo {
|
|
147
|
+
font-size: 14px;
|
|
148
|
+
color: #002f6b;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
word-break: break-all;
|
|
151
|
+
margin-bottom: 2px;
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
flex-wrap: wrap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.badge-sesion-actual {
|
|
159
|
+
background-color: #e8f5e9;
|
|
160
|
+
color: #2e7d32;
|
|
161
|
+
font-size: 10px;
|
|
162
|
+
padding: 2px 8px;
|
|
163
|
+
border-radius: 12px;
|
|
164
|
+
border: 1px solid #c8e6c9;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.item-detalles {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-wrap: wrap;
|
|
173
|
+
gap: 8px;
|
|
174
|
+
font-size: 12px;
|
|
175
|
+
color: #5f6368;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.item-detalles span {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 3px;
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.item-detalles mat-icon {
|
|
186
|
+
font-size: 14px;
|
|
187
|
+
width: 14px;
|
|
188
|
+
height: 14px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.detalle-dispositivo mat-icon {
|
|
192
|
+
color: #673ab7;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.detalle-browser mat-icon {
|
|
196
|
+
color: #5f6368;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.detalle-ip mat-icon {
|
|
200
|
+
color: #0069B4;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.detalle-pais mat-icon {
|
|
204
|
+
color: #F87600;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.item-fecha {
|
|
208
|
+
font-size: 11px;
|
|
209
|
+
color: #9aa0a6;
|
|
210
|
+
white-space: nowrap;
|
|
211
|
+
margin-left: 12px;
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
align-items: flex-end;
|
|
215
|
+
font-family: 'Roboto Mono', monospace;
|
|
216
|
+
flex-shrink: 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
mat-progress-bar {
|
|
220
|
+
height: 4px;
|
|
221
|
+
border-radius: 2px;
|
|
222
|
+
margin-bottom: 10px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@media (max-width: 600px) {
|
|
226
|
+
.actividad-item {
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
align-items: flex-start;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.item-icono {
|
|
232
|
+
margin-bottom: 12px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.item-fecha {
|
|
236
|
+
margin-left: 0;
|
|
237
|
+
margin-top: 12px;
|
|
238
|
+
align-items: flex-start;
|
|
239
|
+
}
|
|
5
240
|
}
|
|
@@ -1,22 +1,60 @@
|
|
|
1
|
-
<
|
|
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>
|
|
2
5
|
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
</mat-form-field>
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
</mat-dialog-content>
|
|
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
|
|
9
|
+
<strong>soporte@utn.ac.cr</strong>.</p>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<button mat-button (click)="onClose()">{{ data.textoCerrar }}</button>
|
|
13
|
+
@if(cargando){
|
|
14
|
+
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
<div class="lista-actividad">
|
|
18
|
+
@if(actividad.length === 0 && !cargando) {
|
|
19
|
+
<p class="sin-datos">No hay actividad reciente.</p>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@for (item of actividad; track item.Consecutivo) {
|
|
23
|
+
<div class="actividad-item">
|
|
24
|
+
<div class="item-icono" [ngClass]="{
|
|
25
|
+
'metodo-get': item.Metodo === 'GET',
|
|
26
|
+
'metodo-post': item.Metodo === 'POST',
|
|
27
|
+
'metodo-put': item.Metodo === 'PUT',
|
|
28
|
+
'metodo-delete': item.Metodo === 'DELETE'
|
|
29
|
+
}">
|
|
30
|
+
<mat-icon [matTooltip]="item.Metodo">{{ getIcon(item.Metodo) }}</mat-icon>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="item-contenido">
|
|
33
|
+
<div class="item-titulo">
|
|
34
|
+
{{ item.URL }}
|
|
35
|
+
@if(esSesionActual(item.Token)){
|
|
36
|
+
<span class="badge-sesion-actual">Sesión actual</span>
|
|
37
|
+
}
|
|
38
|
+
</div>
|
|
39
|
+
<div class="item-detalles">
|
|
40
|
+
<span class="detalle-dispositivo">
|
|
41
|
+
<mat-icon>{{ item.Dispositivo === 'Móvil' ? 'smartphone' : (item.Dispositivo === 'Tablet' ? 'tablet' :
|
|
42
|
+
'laptop') }}</mat-icon>
|
|
43
|
+
{{ item.Dispositivo }}
|
|
44
|
+
</span>
|
|
45
|
+
<span class="detalle-browser"><mat-icon>public</mat-icon> {{ item.Navegador }}</span>
|
|
46
|
+
<span class="detalle-ip"><mat-icon>lan</mat-icon> {{ item.IP }}</span>
|
|
47
|
+
<span class="detalle-pais"><mat-icon>location_on</mat-icon> {{ item.Pais }}</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="item-fecha">
|
|
51
|
+
<span>{{ item.Fecha | date: 'dd/MM/yyyy' }}</span>
|
|
52
|
+
<strong>{{ item.Fecha | date: 'HH:mm:ss' }}</strong>
|
|
53
|
+
<button mat-icon-button color="warn" matTooltip="Cerrar esta sesión" (click)="cerrarSesion(item.Token)">
|
|
54
|
+
<mat-icon>logout</mat-icon>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.ts
CHANGED
|
@@ -1,65 +1,86 @@
|
|
|
1
|
-
import { Component,
|
|
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';
|
|
2
7
|
import { MatButtonModule } from '@angular/material/button';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
MatDialogContent,
|
|
7
|
-
MatDialogRef,
|
|
8
|
-
MatDialogTitle
|
|
9
|
-
} from '@angular/material/dialog';
|
|
10
|
-
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
11
|
-
import { MatInputModule } from '@angular/material/input';
|
|
12
|
-
import { FormsModule } from '@angular/forms';
|
|
13
|
-
|
|
14
|
-
export interface DialogData {
|
|
15
|
-
titulo: string;
|
|
16
|
-
mensaje: string;
|
|
17
|
-
textoCerrar?: string;
|
|
18
|
-
textoAceptar?: string;
|
|
19
|
-
SolicitaMensaje?: string;
|
|
20
|
-
onClose?: () => void;
|
|
21
|
-
onAccept?: (MensajeBrindado: any) => void;
|
|
22
|
-
}
|
|
8
|
+
import { MatDialog } from '@angular/material/dialog';
|
|
9
|
+
import { MensajeConfirmacionComponent } from '../mensaje-confirmacion/mensaje-confirmacion';
|
|
10
|
+
import { DatosGlobalesService } from '../../../datos-globales.service';
|
|
23
11
|
|
|
24
12
|
@Component({
|
|
25
|
-
selector: 'app-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
MatDialogTitle,
|
|
31
|
-
MatDialogContent,
|
|
32
|
-
MatDialogActions,
|
|
33
|
-
MatFormFieldModule,
|
|
34
|
-
MatInputModule,
|
|
35
|
-
FormsModule
|
|
36
|
-
]
|
|
13
|
+
selector: 'app-gestion-actividad',
|
|
14
|
+
standalone: true,
|
|
15
|
+
imports: [CommonModule, MatIconModule, MatTooltipModule, MatProgressBarModule, MatButtonModule],
|
|
16
|
+
templateUrl: './gestion-actividad.component.html',
|
|
17
|
+
styleUrls: ['./gestion-actividad.component.css']
|
|
37
18
|
})
|
|
38
|
-
export class
|
|
19
|
+
export class GestionActividadComponent implements OnInit {
|
|
20
|
+
public actividad: any[] = [];
|
|
21
|
+
public cargando = false;
|
|
22
|
+
private tokenActual = '';
|
|
23
|
+
|
|
24
|
+
constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService, private dialog: MatDialog) { }
|
|
39
25
|
|
|
40
|
-
|
|
26
|
+
ngOnInit(): void {
|
|
27
|
+
this.tokenActual = this.datosGlobalesService.ObtenerToken().split(' ')[1] || '';
|
|
28
|
+
this.obtenerActividad();
|
|
29
|
+
}
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
obtenerActividad(): void {
|
|
32
|
+
this.cargando = true;
|
|
33
|
+
this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/ListarActividades`).subscribe({
|
|
34
|
+
next: (datos: any) => {
|
|
35
|
+
this.actividad = datos.body;
|
|
36
|
+
},
|
|
37
|
+
error: (error) => {
|
|
38
|
+
console.error('Error al obtener actividad:', error);
|
|
39
|
+
this.cargando = false;
|
|
40
|
+
},
|
|
41
|
+
complete: () => {
|
|
42
|
+
this.cargando = false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
cerrarSesion(token: string): void {
|
|
48
|
+
this.dialog.open(MensajeConfirmacionComponent, {
|
|
49
|
+
data: {
|
|
50
|
+
titulo: 'Confirmación',
|
|
51
|
+
mensaje: '¿Está seguro de que desea cerrar esta sesión?',
|
|
52
|
+
textoCerrar: 'Cancelar',
|
|
53
|
+
textoAceptar: 'Cerrar sesión',
|
|
54
|
+
onClose: () => { },
|
|
55
|
+
onAccept: () => {
|
|
56
|
+
this.http.delete(`${this.datosGlobalesService.ObtenerURL()}misc/cerrarSesionPorToken/${token}`).subscribe({
|
|
57
|
+
next: () => {
|
|
58
|
+
if (token === this.tokenActual) {
|
|
59
|
+
this.datosGlobalesService.RedirigirALogin();
|
|
60
|
+
} else {
|
|
61
|
+
this.obtenerActividad();
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
error: (error) => {
|
|
65
|
+
console.error('Error al cerrar sesión:', error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
50
71
|
}
|
|
51
72
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.data.onClose();
|
|
55
|
-
}
|
|
56
|
-
this.dialogRef.close();
|
|
73
|
+
esSesionActual(token: string): boolean {
|
|
74
|
+
return token === this.tokenActual;
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
getIcon(metodo: string): string {
|
|
78
|
+
switch (metodo) {
|
|
79
|
+
case 'GET': return 'search';
|
|
80
|
+
case 'POST': return 'add_circle';
|
|
81
|
+
case 'PUT': return 'edit';
|
|
82
|
+
case 'DELETE': return 'delete';
|
|
83
|
+
default: return 'help_outline';
|
|
62
84
|
}
|
|
63
|
-
this.dialogRef.close(this.MensajeBrindado);
|
|
64
85
|
}
|
|
65
86
|
}
|