utn-cli 2.1.16 → 2.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/commands/backend.js +40 -63
  2. package/package.json +1 -1
  3. package/templates/backend/servicios/InformacionDelModulo.js +168 -48
  4. package/templates/backend/servicios/Nucleo/Miscelaneas.js +84 -2433
  5. package/templates/backend/servicios/Nucleo/MiscelaneasMixins/Archivos.js +329 -0
  6. package/templates/backend/servicios/Nucleo/MiscelaneasMixins/Autenticacion.js +388 -0
  7. package/templates/backend/servicios/Nucleo/MiscelaneasMixins/InicializacionDelModulo.js +254 -0
  8. package/templates/backend/servicios/Nucleo/MiscelaneasMixins/Modulos.js +261 -0
  9. package/templates/backend/servicios/Nucleo/MiscelaneasMixins/Notificaciones.js +82 -0
  10. package/templates/backend/servicios/Nucleo/MiscelaneasMixins/Personas.js +93 -0
  11. package/templates/backend/servicios/Nucleo/MiscelaneasMixins/Reportes.js +370 -0
  12. package/templates/backend/servicios/Nucleo/MiscelaneasMixins/TareasProgramadas.js +105 -0
  13. package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.css +16 -1
  14. package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.html +1 -1
  15. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.html +17 -16
  16. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.ts +11 -2
  17. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-incidencias/reporte-de-incidencias.component.css +0 -1
  18. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-sugerencias/reporte-de-sugerencias.component.css +0 -1
  19. package/templates/frontend/src/app/Componentes/Nucleo/tabla/tabla.component.css +4 -0
  20. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-modulo/tarjeta-modulo.component.css +1 -1
  21. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.css +26 -0
@@ -0,0 +1,370 @@
1
+ const { ejecutarConsulta, ejecutarConsultaSIGU } = require('../db.js');
2
+
3
+ module.exports = {
4
+
5
+ async generarFirmaHTML(Identificador, FechaDeLaFirma) {
6
+ const ReporteHTML = require('./ReporteHTML.js');
7
+ return await ReporteHTML.generarFirmaHTML(Identificador, FechaDeLaFirma);
8
+ },
9
+
10
+ GenerarReporteHTMLRegistrosVerticales(ElementosParaLaTabla, ParametrosExcluidos = [], ParametrosExtra = [], MostrarEncabezado = true) {
11
+ const ReporteHTML = require('./ReporteHTML.js');
12
+ return ReporteHTML.GenerarReporteHTMLRegistrosVerticales(ElementosParaLaTabla, ParametrosExcluidos, ParametrosExtra, MostrarEncabezado);
13
+ },
14
+
15
+ GenerarReporteHTMLEncabezado(InformacionDeLaDerecha, titulares, marcaDeAgua = '') {
16
+ const ReporteHTML = require('./ReporteHTML.js');
17
+ return ReporteHTML.GenerarReporteHTMLEncabezado(InformacionDeLaDerecha, titulares, marcaDeAgua);
18
+ },
19
+
20
+ GenerarReporteHTMLFecha() {
21
+ const ReporteHTML = require('./ReporteHTML.js');
22
+ return ReporteHTML.GenerarReporteHTMLFecha();
23
+ },
24
+
25
+ GenerarReporteHTMLTablas(ElementosParaLaTabla, ParametrosExcluidos = [], ParametrosExtra = []) {
26
+ const ReporteHTML = require('./ReporteHTML.js');
27
+ return ReporteHTML.GenerarReporteHTMLTablas(ElementosParaLaTabla, ParametrosExcluidos, ParametrosExtra);
28
+ },
29
+
30
+ GenerarReporteHTMLPie() {
31
+ const ReporteHTML = require('./ReporteHTML.js');
32
+ return ReporteHTML.GenerarReporteHTMLPie();
33
+ },
34
+
35
+ JSONAHTML(input, title = 'Reporte') {
36
+ let obj = input;
37
+ if (typeof input === 'string') {
38
+ try { obj = JSON.parse(input); }
39
+ catch (e) { /* no es JSON, lo tratamos como texto primitivo */ }
40
+ }
41
+
42
+ const escapeHtml = (s) =>
43
+ String(s)
44
+ .replace(/&/g, '&')
45
+ .replace(/</g, '&lt;')
46
+ .replace(/>/g, '&gt;')
47
+ .replace(/"/g, '&quot;')
48
+ .replace(/'/g, '&#39;');
49
+
50
+ const isPrimitive = v => v === null || v === undefined || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
51
+
52
+ const seen = new WeakSet();
53
+
54
+ function renderValue(v) {
55
+ if (v === null || v === undefined || v === '') return '—';
56
+ if (isPrimitive(v)) return escapeHtml(String(v));
57
+ if (Array.isArray(v)) return renderArray(v);
58
+ return renderObject(v);
59
+ }
60
+
61
+ function renderObject(o) {
62
+ if (seen.has(o)) return '<em>(referencia circular)</em>';
63
+ seen.add(o);
64
+ const keys = Object.keys(o);
65
+ if (keys.length === 0) {
66
+ seen.delete(o);
67
+ return '—';
68
+ }
69
+ let rows = '';
70
+ for (const k of keys) {
71
+ rows += `
72
+ <tr>
73
+ <th>${escapeHtml(k)}</th>
74
+ <td>${renderValue(o[k])}</td>
75
+ </tr>
76
+ `;
77
+ }
78
+ seen.delete(o);
79
+ return `<table border='1'><tbody>${rows}</tbody></table>`;
80
+ }
81
+
82
+ function renderArray(arr) {
83
+ if (arr.length === 0) return '—';
84
+ if (arr.every(isPrimitive)) {
85
+ return `<div>${arr.map(x => `<span>${escapeHtml(String(x))}</span>`).join(' ')}</div>`;
86
+ }
87
+ if (arr.every(it => typeof it === 'object' && it !== null && !Array.isArray(it))) {
88
+ const columns = Array.from(new Set(arr.flatMap(item => Object.keys(item))));
89
+ const header = columns.map(c => `<th>${escapeHtml(c)}</th>`).join('');
90
+ const body = arr.map(item => {
91
+ const cells = columns.map(c => `<td>${item.hasOwnProperty(c) ? renderValue(item[c]) : ''}</td>`).join('');
92
+ return `<tr>${cells}</tr>`;
93
+ }).join('');
94
+ return `<table border='1'><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table>`;
95
+ }
96
+ return `<div>${arr.map(it => `<div>${renderValue(it)}</div>`).join('')}</div>`;
97
+ }
98
+
99
+ let content = '';
100
+ if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
101
+ const topKeys = Object.keys(obj);
102
+ for (const k of topKeys) {
103
+ content += `
104
+ <section>
105
+ <h2>${escapeHtml(k)}</h2>
106
+ <div>${renderValue(obj[k])}</div>
107
+ </section>
108
+ `;
109
+ }
110
+ } else {
111
+ content = `<section><div>${renderValue(obj)}</div></section>`;
112
+ }
113
+
114
+ return `<h1>${escapeHtml(title)}</h1>${content}`;
115
+ },
116
+
117
+ convertirACSV(ArregloDeJSON) {
118
+ if (!ArregloDeJSON || ArregloDeJSON.length === 0) return '';
119
+ const intentarParsearJSON = (valor) => {
120
+ if (typeof valor === 'object' && valor !== null && !Array.isArray(valor)) return valor;
121
+ if (typeof valor === 'string' && valor.trim().startsWith('{')) {
122
+ try {
123
+ const objeto = JSON.parse(valor);
124
+ if (typeof objeto === 'object' && objeto !== null && !Array.isArray(objeto)) return objeto;
125
+ } catch (e) { }
126
+ }
127
+ return null;
128
+ };
129
+ const columnasOriginales = Object.keys(ArregloDeJSON[0]);
130
+ const mapaColumnasJSON = {};
131
+ columnasOriginales.forEach(col => {
132
+ const todasLasLlaves = new Set();
133
+ let esJSON = false;
134
+ ArregloDeJSON.forEach(fila => {
135
+ const obj = intentarParsearJSON(fila[col]);
136
+ if (obj) {
137
+ esJSON = true;
138
+ Object.keys(obj).forEach(k => todasLasLlaves.add(k));
139
+ }
140
+ });
141
+ if (esJSON) mapaColumnasJSON[col] = Array.from(todasLasLlaves);
142
+ });
143
+
144
+ const encabezadosFinales = [];
145
+ columnasOriginales.forEach(col => {
146
+ if (mapaColumnasJSON[col]) {
147
+ mapaColumnasJSON[col].forEach(llave => encabezadosFinales.push(`${col}_${llave}`));
148
+ } else {
149
+ encabezadosFinales.push(col);
150
+ }
151
+ });
152
+
153
+ const filasCSV = ArregloDeJSON.map(fila => {
154
+ const valoresFila = [];
155
+ columnasOriginales.forEach(col => {
156
+ if (mapaColumnasJSON[col]) {
157
+ const obj = intentarParsearJSON(fila[col]) || {};
158
+ mapaColumnasJSON[col].forEach(llave => {
159
+ let valor = obj[llave];
160
+ if (valor === undefined || valor === null || valor === 'null') valor = '';
161
+ valoresFila.push(JSON.stringify(String(valor)).replace(/,/g, ' '));
162
+ });
163
+ } else {
164
+ let valor = fila[col];
165
+ if (valor === null || valor === 'null') valor = '';
166
+ valoresFila.push(JSON.stringify(String(valor)).replace(/,/g, ' '));
167
+ }
168
+ });
169
+ return valoresFila.join(',');
170
+ });
171
+ return [encabezadosFinales.join(','), ...filasCSV].join('\n');
172
+ },
173
+
174
+ convertirACSVConDetalle(ArregloDeJSON, ColumnaConDetalle) {
175
+ if (!ArregloDeJSON || ArregloDeJSON.length === 0) return '';
176
+ const ejemploConDetalle = ArregloDeJSON.find(e => Array.isArray(e[ColumnaConDetalle]) && e[ColumnaConDetalle].length > 0);
177
+ const camposDetalle = ejemploConDetalle ? Object.keys(ejemploConDetalle[ColumnaConDetalle][0]) : [];
178
+ const columnasBase = Object.keys(ArregloDeJSON[0]).filter(key => key !== ColumnaConDetalle);
179
+ const encabezados = [...columnasBase, ...camposDetalle];
180
+ const filas = ArregloDeJSON.flatMap(fila => {
181
+ const detalles = Array.isArray(fila[ColumnaConDetalle]) ? fila[ColumnaConDetalle] : [];
182
+ if (detalles.length === 0) {
183
+ return [
184
+ encabezados.map(col => {
185
+ const valor = fila[col];
186
+ return formatearValor(valor);
187
+ }).join(',')
188
+ ];
189
+ }
190
+ return detalles.map(detalle => {
191
+ return encabezados.map(col => {
192
+ if (camposDetalle.includes(col)) {
193
+ return formatearValor(detalle[col]);
194
+ } else {
195
+ return formatearValor(fila[col]);
196
+ }
197
+ }).join(',');
198
+ });
199
+ });
200
+ return [encabezados.join(','), ...filas].join('\n');
201
+ function formatearValor(valor) {
202
+ if (valor === null || valor === 'null' || typeof valor === 'undefined') {
203
+ return '""';
204
+ }
205
+ return `"${String(valor).replace(/"/g, '""')}"`;
206
+ }
207
+ },
208
+
209
+ jsonATabla(Datos) {
210
+ if (!Array.isArray(Datos) || Datos.length === 0) {
211
+ return 'El arreglo está vacío o no es válido.';
212
+ }
213
+ const columnas = Object.keys(Datos[0]);
214
+ const anchos = columnas.map(columna => {
215
+ return Math.max(
216
+ columna.length,
217
+ ...Datos.map(row => (row[columna] ? row[columna].toString().length : 0))
218
+ );
219
+ });
220
+ const formatearFila = (fila) => '| ' + fila.map((valor, index) => (valor || '').toString().padEnd(anchos[index])).join(' | ') + ' |';
221
+ const separador = '+-' + anchos.map(ancho => '-'.repeat(ancho)).join('-+-') + '-+';
222
+ const encabezado = formatearFila(columnas);
223
+ const filas = Datos.map(row => formatearFila(columnas.map(col => row[col] || '')));
224
+ return [separador, encabezado, separador, ...filas, separador].join('\n');
225
+ },
226
+
227
+ async DatosParaReporteCSV() {
228
+ return this.convertirACSV(await this.DatosParaGraficoDeBarras());
229
+ },
230
+
231
+ async DatosParaGraficoDeBarras() {
232
+ return await ejecutarConsultaSIGU("SELECT MONTHNAME(`FechaNacimiento`) AS `EjeHorizontal`, `Sexo` AS `Etiqueta`, COUNT(*) AS `Total` FROM `SIGU`.`SIGU_Personas` WHERE MONTH(`FechaNacimiento`) > 0 AND `Sexo` <> '' GROUP BY MONTHNAME(`FechaNacimiento`), `Sexo` ORDER BY MONTH(`FechaNacimiento`)");
233
+ },
234
+
235
+ async DatosParaGraficoDePie() {
236
+ return await ejecutarConsultaSIGU("SELECT `Tipo` AS `Etiqueta`, COUNT(*) AS `Total` FROM `SIGU`.`SIGU_ModulosV2` GROUP BY `Tipo`");
237
+ },
238
+
239
+ async generarObjetoInfoDeUnReportePDF() {
240
+ const UUID = await ejecutarConsulta("SELECT UUID() AS `Dato`");
241
+ const PalabrasClave = {
242
+ "Módulo": this.NombreCanonicoDelModulo,
243
+ "UUID": UUID[0]['Dato']
244
+ };
245
+ return {
246
+ Title: 'Nombre del reporte, como por ejemplo: Boleta de Vacaciones',
247
+ Author: 'Universidad Técnica Nacional',
248
+ Subject: 'Reporte PDF',
249
+ Keywords: JSON.stringify(PalabrasClave),
250
+ Creator: 'SIGU',
251
+ Producer: 'SIGU'
252
+ };
253
+ },
254
+
255
+ agregarMarcaDeAgua(doc, texto = 'Borrador', opciones = {}) {
256
+ const {
257
+ color = '#CCCCCC',
258
+ opacity = 0.3,
259
+ } = opciones;
260
+
261
+ const { width, height } = doc.page;
262
+
263
+ doc.save();
264
+ doc
265
+ .font('Times-Roman', 220)
266
+ .fillColor(color)
267
+ .opacity(opacity)
268
+ .rotate(-55, { origin: [width / 2, height / 2] })
269
+ .text(
270
+ texto,
271
+ -width * 0.2,
272
+ height * 0.4,
273
+ {
274
+ align: 'center',
275
+ valign: 'center',
276
+ width: width * 1.5,
277
+ }
278
+ );
279
+ doc.restore();
280
+ },
281
+
282
+ agregarTablaElegante(doc, datos, opciones = {}) {
283
+ const { x = 50, y = 50, columnWidth = 100, rowHeight = 20, headerHeight = 25, fontSize = 10 } = opciones;
284
+
285
+ doc.fontSize(fontSize);
286
+
287
+ const encabezados = Object.keys(datos[0]);
288
+
289
+ encabezados.forEach((encabezado, i) => {
290
+ const posX = x + i * columnWidth;
291
+ doc
292
+ .rect(posX, y, columnWidth, headerHeight)
293
+ .fillAndStroke('#d3d3d3', '#000')
294
+ .fillColor('#000')
295
+ .text(encabezado, posX + 5, y + 5, { width: columnWidth - 10, align: 'left' });
296
+ });
297
+
298
+ datos.forEach((fila, filaIndex) => {
299
+ const filaY = y + headerHeight + filaIndex * rowHeight;
300
+ encabezados.forEach((columna, colIndex) => {
301
+ const posX = x + colIndex * columnWidth;
302
+ const texto = fila[columna] !== undefined ? fila[columna].toString() : '';
303
+ doc
304
+ .rect(posX, filaY, columnWidth, rowHeight)
305
+ .stroke()
306
+ .fillColor('#000')
307
+ .text(texto, posX + 5, filaY + 5, { width: columnWidth - 10, align: 'left' });
308
+ });
309
+ });
310
+
311
+ return doc;
312
+ },
313
+
314
+ async reportePDFDeEjemplo(Respuesta) {
315
+ Respuesta.setHeader('Content-Type', 'application/pdf');
316
+ Respuesta.setHeader('Content-Disposition', 'inline; filename="reporte.pdf"');
317
+ const PDFDocument = require('pdfkit');
318
+
319
+ const opciones = {
320
+ font: 'Courier',
321
+ size: 'LETTER',
322
+ info: await this.generarObjetoInfoDeUnReportePDF()
323
+ };
324
+ var doc = new PDFDocument(opciones);
325
+ doc.pipe(Respuesta);
326
+
327
+ this.agregarMarcaDeAgua(doc, 'Borrador', {
328
+ fontSize: 80,
329
+ opacity: 0.2,
330
+ color: '#FF0000',
331
+ });
332
+
333
+ doc.fontSize(25).text('Here is some vector graphics...', 100, 80);
334
+
335
+ doc.save()
336
+ .moveTo(100, 150)
337
+ .lineTo(100, 250)
338
+ .lineTo(200, 250)
339
+ .fill('#FF3300');
340
+
341
+ doc.circle(280, 200, 50).fill('#6600FF');
342
+
343
+ doc.scale(0.6)
344
+ .translate(470, 130)
345
+ .path('M 250,75 L 323,301 131,161 369,161 177,301 z')
346
+ .fill('red', 'even-odd')
347
+ .restore();
348
+
349
+ doc.text('And here is some wrapped text...', 100, 300)
350
+ .font('Times-Roman', 13)
351
+ .moveDown()
352
+ .text("lorem", {
353
+ width: 412,
354
+ align: 'justify',
355
+ indent: 30,
356
+ columns: 2,
357
+ height: 300,
358
+ ellipsis: true
359
+ });
360
+
361
+ const datos = [
362
+ { Nombre: 'Juan', Edad: 28, Ciudad: 'San José', LugarDeTrabajo: 'UTN' },
363
+ { Nombre: 'Ana', Edad: 34 },
364
+ { Nombre: 'Luis', Edad: 25, Ciudad: 'Alajuela' },
365
+ ];
366
+ this.agregarTablaElegante(doc, datos, { x: 0, y: 400, columnWidth: 150, fontSize: 12 });
367
+ doc.end();
368
+ },
369
+
370
+ };
@@ -0,0 +1,105 @@
1
+ const { ejecutarConsultaSIGU } = require('../db.js');
2
+
3
+ module.exports = {
4
+
5
+ async crearTareaProgramada(Tarea) {
6
+ const os = require('node:os');
7
+ return await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_ModulosTareasProgramadas` VALUES\
8
+ (?, ?, 'Ejecutada', NOW(4), ?) ON DUPLICATE KEY UPDATE `NombreDelEquipo` = ?"
9
+ , [this.NombreDelRepositorioDelBackend, Tarea.name, os.hostname(), os.hostname()]);
10
+ },
11
+
12
+ async pseudoEjecutarTareaProgramada(Tarea) {
13
+ const os = require('node:os');
14
+ const Resultado = await ejecutarConsultaSIGU("UPDATE `SIGU`.`SIGU_ModulosTareasProgramadas` SET `Estado` = 'Procesando'\
15
+ , `NombreDelEquipo` = ?, `FechaYHoraDeLaUltimaEjecucion` = NOW(4)\
16
+ WHERE `Repositorio` = ? AND `TareaProgramada` = ? AND `Estado` IN ('Ejecutada', 'Cancelada', 'Fallida')"
17
+ , [os.hostname(), this.NombreDelRepositorioDelBackend, Tarea]);
18
+ return Resultado['affectedRows'];
19
+ },
20
+
21
+ async finalizarTareaProgramada(Tarea) {
22
+ const os = require('node:os');
23
+ const Resultado = await ejecutarConsultaSIGU("UPDATE `SIGU`.`SIGU_ModulosTareasProgramadas` SET `Estado` = 'Ejecutada'\
24
+ , `NombreDelEquipo` = ?, `FechaYHoraDeLaUltimaEjecucion` = NOW(4)\
25
+ WHERE `Repositorio` = ? AND `TareaProgramada` = ? AND `Estado` IN ('Procesando')"
26
+ , [os.hostname(), this.NombreDelRepositorioDelBackend, Tarea]);
27
+ return Resultado['affectedRows'];
28
+ },
29
+
30
+ async ejecucionCadaXMinutos(callback, intervaloMinutos) {
31
+ while (true) {
32
+ try {
33
+ await this.crearTareaProgramada(callback);
34
+ break;
35
+ } catch (error) {
36
+ console.error('Error al ejecutar crearTareaProgramada:', error);
37
+ console.log('Reintentando en 5 segundos...');
38
+ await new Promise(resolve => setTimeout(resolve, 5000));
39
+ }
40
+ }
41
+ const intervaloMilisegundos = intervaloMinutos * 60 * 1000;
42
+ while (true) {
43
+ const inicio = Date.now();
44
+ try {
45
+ await callback();
46
+ } catch (error) {
47
+ console.error(`Error en la función "${callback.name}":`, error?.message || error);
48
+ }
49
+ const duracion = Date.now() - inicio;
50
+ const tiempoRestante = intervaloMilisegundos - duracion;
51
+ if (tiempoRestante > 0) {
52
+ await new Promise(resolve => setTimeout(resolve, tiempoRestante));
53
+ }
54
+ }
55
+ },
56
+
57
+ async ejecutarEnHoraEspecifica(hora, minuto, segundo, callback) {
58
+ while (true) {
59
+ try {
60
+ await this.crearTareaProgramada(callback);
61
+ break;
62
+ } catch (error) {
63
+ console.error('Error al ejecutar crearTareaProgramada:', error);
64
+ console.log('Reintentando en 5 segundos...');
65
+ await new Promise(resolve => setTimeout(resolve, 5000));
66
+ }
67
+ }
68
+ function programarEjecucion() {
69
+ const ahora = new Date();
70
+ const proximaEjecucion = new Date(ahora);
71
+ proximaEjecucion.setHours(hora, minuto, segundo, 0);
72
+ let tiempoEspera = proximaEjecucion - ahora;
73
+ if (tiempoEspera < 0) {
74
+ proximaEjecucion.setDate(proximaEjecucion.getDate() + 1);
75
+ tiempoEspera = proximaEjecucion - ahora;
76
+ }
77
+ console.log(`Se ha programado a '${callback.name}' para ejecución a las ${proximaEjecucion.toLocaleTimeString()}`);
78
+ setTimeout(() => {
79
+ callback();
80
+ programarEjecucion();
81
+ }, tiempoEspera);
82
+ }
83
+ programarEjecucion();
84
+ },
85
+
86
+ async ejecucionDiferida(callback) {
87
+ const stackLines = new Error().stack.split('\n');
88
+ const lineaCaller = stackLines[2] ?? '';
89
+ const match = lineaCaller.match(/\((.+?):\d+:\d+\)/) ?? lineaCaller.match(/at (.+?):\d+:\d+/);
90
+ const archivo = match ? require('path').basename(match[1]) : null;
91
+ while (true) {
92
+ try {
93
+ this._archivoFuenteActual = archivo;
94
+ await callback();
95
+ break;
96
+ } catch (error) {
97
+ console.error(`Error en la función "${callback.name}": `, error.message);
98
+ console.log('Reintentando en 5 segundos...');
99
+ await new Promise(resolve => setTimeout(resolve, 5000));
100
+ }
101
+ }
102
+ this._archivoFuenteActual = null;
103
+ },
104
+
105
+ };
@@ -120,7 +120,8 @@
120
120
  flex: 1;
121
121
  display: flex;
122
122
  flex-direction: column;
123
- min-width: 0; /* Evita que el flex desborde */
123
+ min-width: 0;
124
+ overflow: hidden;
124
125
  }
125
126
 
126
127
  .item-titulo {
@@ -162,6 +163,20 @@
162
163
  white-space: nowrap;
163
164
  }
164
165
 
166
+ .detalle-browser {
167
+ min-width: 0;
168
+ max-width: 100%;
169
+ overflow: hidden;
170
+ }
171
+
172
+ .texto-browser {
173
+ overflow: hidden;
174
+ text-overflow: ellipsis;
175
+ white-space: nowrap;
176
+ min-width: 0;
177
+ flex: 1;
178
+ }
179
+
165
180
  .item-detalles mat-icon {
166
181
  font-size: 14px;
167
182
  width: 14px;
@@ -40,7 +40,7 @@
40
40
  <mat-icon>{{ item.Dispositivo === 'Móvil' ? 'smartphone' : (item.Dispositivo === 'Tablet' ? 'tablet' : 'laptop') }}</mat-icon>
41
41
  {{ item.Dispositivo }}
42
42
  </span>
43
- <span class="detalle-browser"><mat-icon>public</mat-icon> {{ item.Navegador }}</span>
43
+ <span class="detalle-browser"><mat-icon>public</mat-icon><span class="texto-browser">{{ item.Navegador }}</span></span>
44
44
  <span class="detalle-ip"><mat-icon>lan</mat-icon> {{ item.IP }}</span>
45
45
  <span class="detalle-pais"><mat-icon>location_on</mat-icon> {{ item.Pais }}</span>
46
46
  </div>
@@ -6,11 +6,11 @@
6
6
  <nav>
7
7
  <ul class="toc-lista">
8
8
  @for (entrada of toc; track entrada.id) {
9
- <li [class]="'toc-nivel-' + entrada.nivel">
10
- <button class="toc-enlace" (click)="irA(entrada.id)" [title]="entrada.texto">
11
- {{ entrada.texto }}
12
- </button>
13
- </li>
9
+ <li [class]="'toc-nivel-' + entrada.nivel">
10
+ <button class="toc-enlace" (click)="irA(entrada.id)" [title]="entrada.texto">
11
+ {{ entrada.texto }}
12
+ </button>
13
+ </li>
14
14
  }
15
15
  </ul>
16
16
  </nav>
@@ -20,24 +20,25 @@
20
20
  <main class="manual-main" role="main" aria-label="Manual de usuario">
21
21
 
22
22
  @if (cargando) {
23
- <div class="manual-estado" role="status" aria-live="polite" aria-busy="true">
24
- <p>Cargando manual…</p>
25
- </div>
23
+ <div class="manual-estado" role="status" aria-live="polite" aria-busy="true">
24
+ <p>Cargando manual…</p>
25
+ </div>
26
26
  }
27
27
 
28
28
  @if (error) {
29
- <div class="manual-estado manual-error" role="alert">
30
- <p>
31
- No se pudo cargar el manual. Verifique que el archivo
32
- <code>Manual.md</code> esté en la carpeta <code>public/</code>.
33
- </p>
34
- </div>
29
+ <div class="manual-estado manual-error" role="alert">
30
+ <p>
31
+ No se pudo cargar el manual. Verifique que el archivo
32
+ <code>Manual.md</code> esté en la carpeta <code>public/</code>.
33
+ </p>
34
+ </div>
35
35
  }
36
36
 
37
37
  @if (!cargando && !error) {
38
- <article #contenidoManual class="manual-articulo" [innerHTML]="contenido"></article>
38
+ <article #contenidoManual class="manual-articulo" [innerHTML]="contenido" (click)="manejarClicEnContenido($event)">
39
+ </article>
39
40
  }
40
41
 
41
42
  </main>
42
43
 
43
- </div>
44
+ </div>
@@ -44,7 +44,7 @@ export class ManualComponent implements OnInit, OnDestroy {
44
44
 
45
45
  doc.querySelectorAll('h1, h2, h3').forEach((heading) => {
46
46
  const texto = heading.textContent ?? '';
47
- const id = 'sec-' + texto
47
+ const id = texto
48
48
  .toLowerCase()
49
49
  .normalize('NFD')
50
50
  .replace(DIACRITICOS, '')
@@ -72,6 +72,15 @@ export class ManualComponent implements OnInit, OnDestroy {
72
72
  }
73
73
 
74
74
  irA(id: string): void {
75
- this.contenidoManualRef?.nativeElement.querySelector('#' + id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
75
+ document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
76
+ }
77
+
78
+ manejarClicEnContenido(event: MouseEvent): void {
79
+ const anchor = (event.target as Element).closest('a');
80
+ if (!anchor) return;
81
+ const href = anchor.getAttribute('href') ?? '';
82
+ if (!href.startsWith('#')) return;
83
+ event.preventDefault();
84
+ this.irA(href.slice(1));
76
85
  }
77
86
  }
@@ -237,6 +237,8 @@ tr.example-element-row:hover {
237
237
  display: block !important;
238
238
  background: transparent !important;
239
239
  border: none !important;
240
+ width: 100% !important;
241
+ min-width: 0 !important;
240
242
  }
241
243
 
242
244
  /* Ocultar el encabezado por completo */
@@ -254,6 +256,8 @@ tr.example-element-row:hover {
254
256
  box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important;
255
257
  height: auto !important;
256
258
  border: 1px solid #eee !important;
259
+ width: 100% !important;
260
+ box-sizing: border-box !important;
257
261
  }
258
262
 
259
263
  /* 3. Convertir cada CELDA en una línea con espacio entre nombre y valor */
@@ -5,7 +5,7 @@
5
5
  .modulo-card {
6
6
  position: relative;
7
7
  width: 300px;
8
- /* Reduced from 380px */
8
+ max-width: calc(100% - 20px);
9
9
  height: 220px;
10
10
  border-radius: 25px;
11
11
  overflow: hidden;
@@ -199,12 +199,38 @@
199
199
  width: 100%;
200
200
  min-width: 0;
201
201
  }
202
+
203
+ .pie {
204
+ flex-wrap: wrap;
205
+ font-size: 13px;
206
+ gap: 4px;
207
+ }
208
+
209
+ .pie-col.izquierda {
210
+ font-size: 11px;
211
+ overflow: hidden;
212
+ text-overflow: ellipsis;
213
+ white-space: nowrap;
214
+ }
202
215
  }
203
216
 
204
217
  @media (max-width: 480px) {
205
218
  .encabezado {
206
219
  font-size: 18px;
207
220
  }
221
+
222
+ .pie-col.izquierda {
223
+ display: none;
224
+ }
225
+
226
+ .pie-col.centro {
227
+ flex: 1;
228
+ justify-content: center;
229
+ }
230
+
231
+ .pie-col.derecha {
232
+ flex: 0 1 auto;
233
+ }
208
234
  }
209
235
 
210
236
  /* Pie */