utn-cli 2.0.86 → 2.0.88

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.
@@ -387,17 +387,44 @@ function actualizarArchivosConfiguracion(nombreClase, nombreRuta, titulo, descri
387
387
  }
388
388
  }
389
389
 
390
- // --- Modificar contenedor-principal.component.html ---
391
- console.log('Actualizando contenedor-principal.component.html...');
392
- const rutaHtml = path.join(process.cwd(), 'src', 'app', 'Paginas', 'contenedor-principal', 'contenedor-principal.component.html');
393
- if (fs.existsSync(rutaHtml)) {
394
- let contenidoHtml = fs.readFileSync(rutaHtml, 'utf-8');
395
- if (!contenidoHtml.includes(`rutaASeguir]="'${nombreRuta}'"`)) {
396
- const nuevaTarjeta = ` <app-tarjeta [rutaASeguir]="'${nombreRuta}'" titulo="${titulo}" descripcion="${descripcion}" icono="table_chart"></app-tarjeta>`;
397
- const ultimoDiv = contenidoHtml.lastIndexOf('</div>');
398
- if (ultimoDiv !== -1) {
399
- contenidoHtml = contenidoHtml.slice(0, ultimoDiv) + '\n' + nuevaTarjeta + '\n' + contenidoHtml.slice(ultimoDiv);
400
- fs.writeFileSync(rutaHtml, contenidoHtml);
390
+ // --- Modificar contenedor-principal.component.ts ---
391
+ console.log('Actualizando contenedor-principal.component.ts...');
392
+ const rutaTs = path.join(process.cwd(), 'src', 'app', 'Paginas', 'contenedor-principal', 'contenedor-principal.component.ts');
393
+ if (fs.existsSync(rutaTs)) {
394
+ let contenidoTs = fs.readFileSync(rutaTs, 'utf-8');
395
+ if (!contenidoTs.includes(`rutaASeguir: '${nombreRuta}'`)) {
396
+ // Encontrar el array baseTarjetas y su contenido
397
+ const regexBaseTarjetas = /let baseTarjetas: AnyTarjetaConfig\[\] = \[([\s\S]*?)\];/;
398
+ const match = contenidoTs.match(regexBaseTarjetas);
399
+
400
+ if (match) {
401
+ const contenidoArray = match[1];
402
+ // Buscar la última posición para incrementar
403
+ const regexPosiciones = /position: (\d+)/g;
404
+ let ultimaPosicion = 0;
405
+ let m;
406
+ while ((m = regexPosiciones.exec(contenidoArray)) !== null) {
407
+ const pos = parseInt(m[1]);
408
+ if (pos > ultimaPosicion) ultimaPosicion = pos;
409
+ }
410
+ const nuevaPosicion = ultimaPosicion + 10;
411
+
412
+ const nuevaTarjeta = `,
413
+ {
414
+ type: 'single',
415
+ position: ${nuevaPosicion},
416
+ rutaASeguir: '${nombreRuta}',
417
+ titulo: '${titulo}',
418
+ descripcion: '${descripcion}',
419
+ icono: 'table_chart'
420
+ }`;
421
+
422
+ // Insertar antes del cierre del array (el ]; que sigue al contenidoArray)
423
+ const indiceCierre = contenidoTs.indexOf('];', match.index);
424
+ if (indiceCierre !== -1) {
425
+ contenidoTs = contenidoTs.slice(0, indiceCierre).trimEnd() + nuevaTarjeta + '\n ' + contenidoTs.slice(indiceCierre);
426
+ fs.writeFileSync(rutaTs, contenidoTs);
427
+ }
401
428
  }
402
429
  }
403
430
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.0.86",
3
+ "version": "2.0.88",
4
4
  "description": "Herramienta CLI unificada para la gestión de plantillas en SIGU.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -22,6 +22,7 @@
22
22
  "cors": "^2.8.6",
23
23
  "express": "^4.22.1",
24
24
  "helmet": "^7.2.0",
25
+ "google-auth-library": "^9.15.1",
25
26
  "jsonwebtoken": "^9.0.3",
26
27
  "mysql2": "^3.20.0",
27
28
  "nodemailer": "^8.0.5",
@@ -0,0 +1,456 @@
1
+ const path = require("path");
2
+ const PDFDocument = require('pdfkit');
3
+ const crypto = require('crypto');
4
+ const { ejecutarConsultaSIGU } = require('./db.js');
5
+ const Miscelaneas = require('./Miscelaneas.js');
6
+
7
+ class ReportePDF {
8
+ constructor() {
9
+ this.config = {
10
+ margin: 50,
11
+ colorPrimario: "#1F2A63",
12
+ colorSecundario: "#666666",
13
+ colorFondoAlterno: "#F9F9F9",
14
+ colorLinea: "#DDD",
15
+ fuenteBase: "Helvetica",
16
+ fuenteNegrita: "Helvetica-Bold",
17
+ fuenteCursiva: "Helvetica-Oblique",
18
+ fuenteNegritaCursiva: "Helvetica-BoldOblique",
19
+ fontSizeTitulo: 14,
20
+ fontSizeSubtitulo: 10,
21
+ fontSizeCuerpo: 9,
22
+ fontSizePequeño: 8,
23
+ fontSizeMuyPequeño: 7
24
+ };
25
+ }
26
+
27
+ async verificarSiReporteYaExiste(Modulo, Titulo, Identificador) {
28
+ try {
29
+ const query = `SELECT COUNT(*) AS total FROM SIGU.SIGU_ReportesGenerados WHERE Modulo = ? AND Titulo = ? AND Identificador = ?`;
30
+ const resultado = await ejecutarConsultaSIGU(query, [Modulo, Titulo, Identificador]);
31
+ return resultado[0].total > 0;
32
+ } catch (error) {
33
+ console.error("Error al verificar existencia del reporte:", error);
34
+ return false;
35
+ }
36
+ }
37
+
38
+ async generarMetadatosReporte(doc, opciones) {
39
+ try {
40
+ const {
41
+ Modulo = Miscelaneas.getNombreDelRepositorioDelBackend(),
42
+ Titulo = 'Sin Título',
43
+ Identificador = 0,
44
+ Autor = 'Sistema',
45
+ Asunto = 'Reporte'
46
+ } = opciones;
47
+
48
+ const yaExiste = await this.verificarSiReporteYaExiste(Modulo, Titulo, Identificador);
49
+ if (yaExiste) {
50
+ return true;
51
+ }
52
+
53
+ // Generar UUID
54
+ const UUIDResult = await ejecutarConsultaSIGU("SELECT UUID() AS Dato");
55
+ const uuid = UUIDResult[0].Dato;
56
+
57
+ // Generar fecha y hora actual en formato MySQL
58
+ const now = new Date();
59
+ // Formatear al huso horario local de Costa Rica o general YYYY-MM-DD HH:mm:ss
60
+ // Una forma segura y simple en Node para obtener local sin T ni Z
61
+ const offsetMs = now.getTimezoneOffset() * 60 * 1000;
62
+ const localTime = new Date(now.getTime() - offsetMs);
63
+ const fechaYHora = localTime.toISOString().replace('T', ' ').substring(0, 19);
64
+
65
+ // Preparar Metadatos según estructura solicitada
66
+ const metadatos = {
67
+ "Título": Titulo,
68
+ "Autor": Autor,
69
+ "Asunto": Asunto,
70
+ "Palabras clave": {
71
+ "Módulo": Modulo,
72
+ "UUID": uuid
73
+ },
74
+ "Fecha de creación": fechaYHora
75
+ };
76
+
77
+ // Calcular cantidad de páginas
78
+ let totalPaginas = 1;
79
+ // Primero intentamos con bufferedPageRange si se inicializó con { bufferPages: true }
80
+ if (typeof doc.bufferedPageRange === 'function') {
81
+ try {
82
+ const range = doc.bufferedPageRange();
83
+ if (range && range.count > 0) {
84
+ totalPaginas = range.count;
85
+ }
86
+ } catch (e) {
87
+ // Si bufferPages no está activo, PDFKit a veces lanza un error al llamar a la función
88
+ }
89
+ }
90
+
91
+ // Si falló el buffer, intentamos recuperar el número de la página en la que quedó el cursor.
92
+ if (totalPaginas === 1 && doc._pageBuffer && doc._pageBuffer.length > 0) {
93
+ totalPaginas = doc._pageBuffer.length;
94
+ }
95
+
96
+ // Suma de comprobación usando crypto sha256
97
+ const hashStr = `${uuid}-${Modulo}-${fechaYHora}`;
98
+ const sumaDeComprobacion = crypto.createHash('sha256').update(hashStr).digest('hex');
99
+
100
+ // Preparar MetadatosExtra
101
+ const metadatosExtra = {
102
+ "CantidadDePáginas": totalPaginas,
103
+ "ValidaFechaDeModificación": "TRUE",
104
+ "SumaDeComprobacion": sumaDeComprobacion
105
+ };
106
+
107
+ // Adjuntar metadatos al documento PDF
108
+ doc.info['Title'] = metadatos['Título'];
109
+ doc.info['Author'] = metadatos['Autor'];
110
+ doc.info['Subject'] = metadatos['Asunto'];
111
+ doc.info['Keywords'] = JSON.stringify(metadatos['Palabras clave']);
112
+ // Añadir la cadena JSON explícitamente en el PDF si se requiere para validaciones futuras
113
+ doc.info['Metadatos'] = JSON.stringify(metadatos);
114
+ doc.info['MetadatosExtra'] = JSON.stringify(metadatosExtra);
115
+
116
+ // Insertar el registro en la base de datos
117
+ await ejecutarConsultaSIGU(
118
+ `INSERT INTO SIGU.SIGU_ReportesGenerados
119
+ (IdentificadorDelReporte, Modulo, Titulo, Identificador, Metadatos, MetadatosExtra, FechaYHora)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
121
+ [
122
+ uuid,
123
+ Modulo,
124
+ Titulo,
125
+ Identificador,
126
+ JSON.stringify(metadatos),
127
+ JSON.stringify(metadatosExtra),
128
+ fechaYHora
129
+ ]
130
+ );
131
+
132
+ return true;
133
+ } catch (error) {
134
+ console.error("Error al generar metadatos del reporte PDF:", error);
135
+ return false;
136
+ }
137
+ }
138
+
139
+ finalizarReporte(doc) {
140
+ doc.end();
141
+ }
142
+
143
+ generarEncabezado(Titulos, respuesta) {
144
+ respuesta.setHeader("Content-Type", "application/pdf");
145
+ respuesta.setHeader("Content-Disposition", `inline; filename=Reporte.pdf`);
146
+ const doc = new PDFDocument({ size: 'Letter', margin: this.config.margin, bufferPages: true });
147
+ doc.pipe(respuesta);
148
+
149
+ // Función interna para dibujar el encabezado
150
+ const dibujarEncabezado = () => {
151
+ const margin = this.config.margin;
152
+ const logoWidth = 70;
153
+ const pageWidth = doc.page.width;
154
+
155
+ // Logo UTN
156
+ try {
157
+ const rutaLogo = path.join(__dirname, "./LogoUTN.png");
158
+ // Subimos el logo ligeramente (Y=35 en vez de 40)
159
+ doc.image(rutaLogo, margin, 35, { width: logoWidth });
160
+ } catch (e) { console.error("Logo no encontrado", e); }
161
+
162
+ doc.fillColor(this.config.colorPrimario).fontSize(11).font(this.config.fuenteNegrita);
163
+ doc.text("Universidad Técnica Nacional", margin + logoWidth + 15, 45);
164
+ doc.fontSize(9).font(this.config.fuenteBase);
165
+ doc.text(Titulos.Direccion, margin + logoWidth + 15, 58);
166
+ doc.text(Titulos.Area, margin + logoWidth + 15, 70);
167
+
168
+ // Bloque derecho con información del documento
169
+ const now = new Date();
170
+ const fechaActual = now.toLocaleDateString('es-CR');
171
+ const horaActual = now.toLocaleTimeString('es-CR', { hour: '2-digit', minute: '2-digit', hour12: false });
172
+ doc.fontSize(this.config.fontSizePequeño).fillColor(this.config.colorPrimario);
173
+ doc.text(`Fecha: ${fechaActual}`, pageWidth - margin - 150, 45, { align: 'right', width: 150 });
174
+ doc.text(`Hora: ${horaActual}`, pageWidth - margin - 150, 55, { align: 'right', width: 150 });
175
+ doc.text(`Usuario: ${Titulos.Usuario}`, pageWidth - margin - 150, 65, { align: 'right', width: 150 });
176
+ // Línea separadora
177
+ doc.moveTo(margin, 90).lineTo(pageWidth - margin, 90).strokeColor(this.config.colorPrimario).lineWidth(2).stroke();
178
+ doc.moveDown(4);
179
+ };
180
+ // Evento para añadir el encabezado institucional automáticamente en CADA NUEVA PÁGINA
181
+ doc.on('pageAdded', dibujarEncabezado);
182
+ // Dibujar el encabezado en la PRIMERA PÁGINA (que ya fue creada al instanciar PDFDocument)
183
+ dibujarEncabezado();
184
+ return doc;
185
+ }
186
+
187
+ generarTituloDePagina(doc, Titulo, Subtitulo1 = undefined, Subtitulo2 = undefined) {
188
+ const margin = this.config.margin;
189
+ const pageWidth = doc.page.width;
190
+
191
+ // Alinear el cursor al margen izquierdo antes de imprimir
192
+ doc.x = margin;
193
+
194
+ // Título y subtítulos
195
+ doc.fillColor("black").fontSize(this.config.fontSizeTitulo).font(this.config.fuenteNegrita);
196
+
197
+ // Al no pasar coordenadas X e Y fijas, PDFKit renderiza el texto en la
198
+ // posición actual de doc.y fluyendo naturalmente hacia abajo.
199
+ doc.text(Titulo, { align: 'center', width: pageWidth - 2 * margin });
200
+
201
+ doc.fontSize(this.config.fontSizeSubtitulo).font(this.config.fuenteBase);
202
+
203
+ if (Subtitulo1) {
204
+ doc.moveDown(0.5);
205
+ doc.text(Subtitulo1, { align: 'center', width: pageWidth - 2 * margin });
206
+ }
207
+
208
+ if (Subtitulo2) {
209
+ doc.moveDown(0.5);
210
+ doc.text(Subtitulo2, { align: 'center', width: pageWidth - 2 * margin });
211
+ }
212
+
213
+ // Mover el cursor un poco hacia abajo para el siguiente contenido
214
+ doc.moveDown(2);
215
+ }
216
+
217
+ generarPie(doc, PieDePagina = undefined) {
218
+ const range = doc.bufferedPageRange();
219
+ const textoPieDePagina = PieDePagina || '';
220
+ for (let i = range.start; i < range.start + range.count; i++) {
221
+ doc.switchToPage(i);
222
+ const indexPagina = i + 1;
223
+ const totalPaginas = range.count;
224
+ const margin = this.config.margin;
225
+ const pageWidth = doc.page.width;
226
+ const footerY = doc.page.height - 50;
227
+
228
+ // Desactivar temporalmente el margen inferior para evitar saltos de página accidentales
229
+ const bottomMargin = doc.page.margins.bottom;
230
+ doc.page.margins.bottom = 0;
231
+
232
+ // Raya horizontal superior del pie
233
+ doc.moveTo(margin, footerY).lineTo(pageWidth - margin, footerY).strokeColor(this.config.colorPrimario).lineWidth(1).stroke();
234
+
235
+ // Raya vertical en el centro como separador
236
+ const centerX = pageWidth / 2;
237
+
238
+ doc.fillColor(this.config.colorPrimario).fontSize(this.config.fontSizeMuyPequeño).font(this.config.fuenteBase);
239
+
240
+ // Texto a la izquierda
241
+ doc.text(textoPieDePagina || "", margin, footerY + 10, { width: (pageWidth / 2) - margin - 15, align: 'left' });
242
+
243
+ // Paginación a la derecha (X de Y)
244
+ const paginacion = `Página ${indexPagina} de ${totalPaginas}`;
245
+ // Como el texto de la izquierda tiene 2 líneas, alineamos la paginación un poco más abajo para que quede centrado
246
+ doc.text(paginacion, centerX + 15, footerY + 14, { width: (pageWidth / 2) - margin - 15, align: 'right' });
247
+
248
+ // Restaurar el margen inferior original
249
+ doc.page.margins.bottom = bottomMargin;
250
+ }
251
+ // doc.end();
252
+ }
253
+
254
+ generarTabla(doc, datos, metadatos) {
255
+ if (!datos || datos.length === 0) return;
256
+
257
+ const margin = this.config.margin;
258
+ const pageWidth = doc.page.width;
259
+ const availableWidth = pageWidth - 2 * margin;
260
+
261
+ // Dibujar encabezado de la tabla
262
+ const dibujarHeaderTabla = (y) => {
263
+ doc.rect(margin, y, availableWidth, 22).fill(this.config.colorPrimario);
264
+ doc.fillColor("white").font(this.config.fuenteNegrita).fontSize(this.config.fontSizeCuerpo);
265
+ let cx = margin;
266
+ metadatos.forEach(col => {
267
+ doc.text(col.titulo, cx + 5, y + 7, { width: col.ancho - 10, align: 'left' });
268
+ cx += col.ancho;
269
+ });
270
+ return y + 22;
271
+ };
272
+
273
+ let currentY = doc.y;
274
+
275
+ // Si estamos muy cerca del final, empezamos en página nueva
276
+ if (currentY > doc.page.height - 100) {
277
+ doc.addPage();
278
+ currentY = doc.y; // doc.y ya viene ajustado por el encabezado institucional
279
+ }
280
+
281
+ currentY = dibujarHeaderTabla(currentY);
282
+ doc.font(this.config.fuenteBase).fontSize(this.config.fontSizePequeño).fillColor("black");
283
+
284
+ datos.forEach((fila, index) => {
285
+ const rowHeight = 20;
286
+
287
+ // Salto de página automático (dejando margen para el pie)
288
+ if (currentY + rowHeight > doc.page.height - 80) {
289
+ doc.addPage();
290
+ currentY = dibujarHeaderTabla(doc.y);
291
+ doc.font(this.config.fuenteBase).fontSize(this.config.fontSizePequeño).fillColor("black");
292
+ }
293
+
294
+ // Fondo alterno para las filas
295
+ if (index % 2 === 0) {
296
+ doc.rect(margin, currentY, availableWidth, rowHeight).fill(this.config.colorFondoAlterno);
297
+ }
298
+
299
+ doc.fillColor("black");
300
+ let cx = margin;
301
+ metadatos.forEach(col => {
302
+ let valor = fila[col.llave];
303
+ // Formatear si es salario o número de días
304
+ if (col.llave === 'Salario' && valor) {
305
+ valor = Number(valor).toLocaleString('es-CR', { style: 'currency', currency: 'CRC' });
306
+ } else if (col.llave === 'DiasOtorgados' && valor) {
307
+ valor = Number(valor).toFixed(1);
308
+ }
309
+
310
+ doc.text(String(valor || ""), cx + 5, currentY + 6, { width: col.ancho - 10, align: 'left' });
311
+ cx += col.ancho;
312
+ });
313
+
314
+ // Línea de separación de fila
315
+ doc.moveTo(margin, currentY + rowHeight).lineTo(pageWidth - margin, currentY + rowHeight).strokeColor(this.config.colorLinea).lineWidth(0.5).stroke();
316
+
317
+ currentY += rowHeight;
318
+ // Actualizar la posición global del cursor Y del documento
319
+ doc.y = currentY;
320
+ });
321
+
322
+ doc.moveDown(1);
323
+ }
324
+
325
+ generarSaltoDePagina(doc) {
326
+ // PDFKit generará automáticamente el encabezado de la nueva página debido
327
+ // al evento doc.on('pageAdded') que configuramos en crearPDFDinamico.
328
+ doc.addPage();
329
+ }
330
+
331
+ generarParrafoHTML(doc, html, opciones = {}) {
332
+ const margin = this.config.margin;
333
+ const pageWidth = doc.page.width;
334
+ const availableWidth = opciones.ancho || (pageWidth - 2 * margin);
335
+
336
+ // Mejorar la expresión regular para que mantenga los espacios en blanco
337
+ // y capture los tags sin romper las palabras
338
+ const tokens = String(html || "")
339
+ .replace(/\n/g, ' ') // Eliminar saltos de línea para que fluya como un párrafo
340
+ .split(/(<[a-z0-9/]+[^>]*>)/gi);
341
+
342
+ let isBold = false;
343
+ let isItalic = false;
344
+ let isUnderline = false;
345
+
346
+ const fontSize = opciones.fontSize || this.config.fontSizeSubtitulo;
347
+ const align = opciones.align || 'justify';
348
+
349
+ // Determinar el último token de texto que no está vacío
350
+ let lastTextIndex = -1;
351
+ for (let i = tokens.length - 1; i >= 0; i--) {
352
+ const token = tokens[i];
353
+ if (!token.match(/^<.*>$/) && token.trim() !== "") {
354
+ lastTextIndex = i;
355
+ break;
356
+ }
357
+ }
358
+
359
+ doc.fillColor('black');
360
+
361
+ // Como pdfkit podría venir de un texto dibujado con coordenadas X y Y absolutas (que rompe el flujo interno de texto),
362
+ // definimos la posición X al margen izquierdo para iniciar el párrafo, manteniendo la coordenada Y actual.
363
+ doc.x = margin;
364
+
365
+ // Función auxiliar para aplicar el formato y escribir el buffer
366
+ const flushText = (token, isLast) => {
367
+ if (token === "") return;
368
+
369
+ let fontName = this.config.fuenteBase;
370
+ if (isBold && isItalic) fontName = this.config.fuenteNegritaCursiva;
371
+ else if (isBold) fontName = this.config.fuenteNegrita;
372
+ else if (isItalic) fontName = this.config.fuenteCursiva;
373
+
374
+ doc.font(fontName).fontSize(fontSize);
375
+
376
+ // Decidimos si usar 'continued'
377
+ // Es 'continued' a menos que sea el último fragmento de texto
378
+ const isContinued = !isLast;
379
+
380
+ // Para evitar que las palabras con diferente formato se peguen,
381
+ // añadimos explícitamente un espacio en blanco al final de cada fragmento
382
+ // si este no es el último y si el token en sí no termina ya en un espacio.
383
+ let textoParaImprimir = token;
384
+ if (isContinued && !textoParaImprimir.endsWith(' ')) {
385
+ textoParaImprimir += ' ';
386
+ }
387
+
388
+ doc.text(textoParaImprimir, {
389
+ width: availableWidth,
390
+ continued: isContinued,
391
+ underline: isUnderline,
392
+ align: align
393
+ });
394
+ };
395
+
396
+ for (let index = 0; index < tokens.length; index++) {
397
+ let token = tokens[index];
398
+
399
+ if (!token) continue;
400
+
401
+ const lowerToken = token.toLowerCase().trim();
402
+
403
+ // Tags admitidos
404
+ if (lowerToken === '<b>' || lowerToken === '<strong>') {
405
+ isBold = true;
406
+ continue;
407
+ }
408
+ if (lowerToken === '</b>' || lowerToken === '</strong>') {
409
+ isBold = false;
410
+ continue;
411
+ }
412
+ if (lowerToken === '<i>' || lowerToken === '<em>') {
413
+ isItalic = true;
414
+ continue;
415
+ }
416
+ if (lowerToken === '</i>' || lowerToken === '</em>') {
417
+ isItalic = false;
418
+ continue;
419
+ }
420
+ if (lowerToken === '<u>') {
421
+ isUnderline = true;
422
+ continue;
423
+ }
424
+ if (lowerToken === '</u>') {
425
+ isUnderline = false;
426
+ continue;
427
+ }
428
+ if (lowerToken === '<br>' || lowerToken === '<br/>' || lowerToken === '<br />') {
429
+ // Si hay un salto de línea, rompemos el continued escribiendo un espacio final
430
+ doc.text(' ', { continued: false });
431
+ doc.moveDown(1);
432
+ continue;
433
+ }
434
+
435
+ // Si no es un tag, entonces es texto. Lo mandamos a imprimir.
436
+ if (!token.match(/^<.*>$/)) {
437
+ // En PDFKit el continued es complicado con espacios en los bordes de los tags
438
+ // Decodificamos entidades HTML comunes
439
+ token = token
440
+ .replace(/&nbsp;/g, ' ')
441
+ .replace(/&lt;/g, '<')
442
+ .replace(/&gt;/g, '>')
443
+ .replace(/&amp;/g, '&')
444
+ .replace(/&quot;/g, '"')
445
+ .replace(/&#39;/g, "'");
446
+
447
+ const isLast = index === lastTextIndex || index > lastTextIndex;
448
+ flushText(token, isLast);
449
+ }
450
+ }
451
+
452
+ doc.moveDown(1);
453
+ }
454
+ }
455
+
456
+ module.exports = new ReportePDF();
@@ -102,6 +102,18 @@
102
102
  display: flex;
103
103
  }
104
104
 
105
+ @media (max-width: 768px) {
106
+ .encabezado {
107
+ font-size: 18px;
108
+ }
109
+ }
110
+
111
+ @media (max-width: 480px) {
112
+ .encabezado {
113
+ font-size: 18px;
114
+ }
115
+ }
116
+
105
117
  /* Pie */
106
118
  .pie {
107
119
  display: flex;