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.
- package/commands/frontend.js +38 -11
- package/package.json +1 -1
- package/templates/backend/package.json +1 -0
- package/templates/backend/servicios/Nucleo/LogoUTN.png +0 -0
- package/templates/backend/servicios/Nucleo/ReportePDF.js +456 -0
- package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.css +12 -0
package/commands/frontend.js
CHANGED
|
@@ -387,17 +387,44 @@ function actualizarArchivosConfiguracion(nombreClase, nombreRuta, titulo, descri
|
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
// --- Modificar contenedor-principal.component.
|
|
391
|
-
console.log('Actualizando contenedor-principal.component.
|
|
392
|
-
const
|
|
393
|
-
if (fs.existsSync(
|
|
394
|
-
let
|
|
395
|
-
if (!
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
Binary file
|
|
@@ -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(/ /g, ' ')
|
|
441
|
+
.replace(/</g, '<')
|
|
442
|
+
.replace(/>/g, '>')
|
|
443
|
+
.replace(/&/g, '&')
|
|
444
|
+
.replace(/"/g, '"')
|
|
445
|
+
.replace(/'/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;
|