utn-cli 2.0.88 → 2.0.90
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/package.json +1 -1
- package/templates/backend/servicios/Nucleo/Miscelaneas.js +19 -309
- package/templates/backend/servicios/Nucleo/ReporteHTML.js +235 -0
- package/templates/backend/servicios/Nucleo/ReportePDF.js +367 -7
- package/templates/backend/servicios/Servicio1.js +0 -1
- package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-reporte/tarjeta-reporte.component.html +2 -2
- package/templates/frontend/src/app/Paginas/gestion-de-reportes/gestion-de-reportes.component.css +50 -0
- package/templates/frontend/src/app/Paginas/gestion-tabla-XYZ/gestion-tabla-XYZ.component.ts +12 -0
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { ejecutarConsulta, ejecutarConsultaSIGU,
|
|
1
|
+
const { ejecutarConsulta, ejecutarConsultaSIGU, crearObjetoConexionSIGU } = require('./db.js');
|
|
2
2
|
const ManejadorDeErrores = require('./ManejadorDeErrores.js');
|
|
3
3
|
const InformacionDelModulo = require('../InformacionDelModulo.js');
|
|
4
4
|
const { envioDeCorreo } = require('./EnvioDeCorreos.js');
|
|
@@ -103,325 +103,35 @@ class Miscelaneo {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
//REPORTE INICIA AQUÍ
|
|
107
|
-
|
|
108
106
|
async generarFirmaHTML(Identificador, FechaDeLaFirma) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
if (!persona) return '';
|
|
112
|
-
|
|
113
|
-
const nombre = persona.NombreCompleto;
|
|
114
|
-
const identificacion = persona.Identificacion;
|
|
115
|
-
const instancia = await (async () => {
|
|
116
|
-
try {
|
|
117
|
-
const result = await ejecutarConsultaSIGU(`
|
|
118
|
-
SELECT
|
|
119
|
-
CASE
|
|
120
|
-
WHEN i.TipoDeResponsabilidad = 'Ninguna' THEN sup.Nombre
|
|
121
|
-
ELSE i.Nombre
|
|
122
|
-
END AS Nombre
|
|
123
|
-
FROM SIGU.EstructuraOrganizacional_Instancias i
|
|
124
|
-
LEFT JOIN SIGU.EstructuraOrganizacional_Instancias sup
|
|
125
|
-
ON i.IdentificadorDeInstanciaSuperior = sup.IdentificadorDeInstancia
|
|
126
|
-
WHERE i.Identificador = ?
|
|
127
|
-
`, [Identificador]);
|
|
128
|
-
return result?.[0]?.Nombre || 'N/A';
|
|
129
|
-
} catch {
|
|
130
|
-
return 'N/A';
|
|
131
|
-
}
|
|
132
|
-
})();
|
|
133
|
-
|
|
134
|
-
return ` <div style="
|
|
135
|
-
font-size: 9px;
|
|
136
|
-
border: 1px solid #eee;
|
|
137
|
-
padding: 6px;
|
|
138
|
-
display: inline-flex;
|
|
139
|
-
align-items: center;
|
|
140
|
-
background-color: #fcfcfc;
|
|
141
|
-
gap: 8px;
|
|
142
|
-
">
|
|
143
|
-
<div class="logo" style="display:flex; align-items:center;">
|
|
144
|
-
<img src="https://storage.sigu.utn.ac.cr/images/cards/LogoUTN.svg"
|
|
145
|
-
alt="Logo UTN"
|
|
146
|
-
style="height:25px;">
|
|
147
|
-
</div>
|
|
148
|
-
<div style="
|
|
149
|
-
width: 1px;
|
|
150
|
-
height: 50px;
|
|
151
|
-
background-color: #ccc;
|
|
152
|
-
"></div>
|
|
153
|
-
<div style="text-align: left;">
|
|
154
|
-
<strong>FIRMADO DIGITALMENTE POR:</strong><br>
|
|
155
|
-
${nombre}<br>
|
|
156
|
-
Cédula: ${identificacion}<br>
|
|
157
|
-
Instancia: ${instancia}<br>
|
|
158
|
-
Fecha: ${FechaDeLaFirma}
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
|
-
</div>`;
|
|
107
|
+
const ReporteHTML = require('./ReporteHTML.js');
|
|
108
|
+
return await ReporteHTML.generarFirmaHTML(Identificador, FechaDeLaFirma);
|
|
162
109
|
}
|
|
163
110
|
|
|
164
111
|
GenerarReporteHTMLRegistrosVerticales(ElementosParaLaTabla, ParametrosExcluidos = [], ParametrosExtra = [], MostrarEncabezado = true) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// {
|
|
168
|
-
// "Fecha del traslado": "16-03-2026",
|
|
169
|
-
// "Justificacion": "Traslado por mantenimiento",
|
|
170
|
-
// "Dependencia que entrega": "Juan Pérez",
|
|
171
|
-
// "Dependencia que recibe": "María Gómez",
|
|
172
|
-
// Placa: "ABC123",
|
|
173
|
-
// "Descripción": "Laptop Dell",
|
|
174
|
-
// "Código de activo": "ACT-001",
|
|
175
|
-
// "N° de serie": "SN123456",
|
|
176
|
-
// Marca: "Dell",
|
|
177
|
-
// Modelo: "Latitude 5420",
|
|
178
|
-
// Estado: "Bueno",
|
|
179
|
-
// IdentificadorOrigen: 101,
|
|
180
|
-
// IdentificadorDestino: 202
|
|
181
|
-
// },
|
|
182
|
-
// {
|
|
183
|
-
// "Fecha del traslado": "16-03-2026",
|
|
184
|
-
// "Justificacion": "Traslado por mantenimiento",
|
|
185
|
-
// "Dependencia que entrega": "Juan Pérez",
|
|
186
|
-
// "Dependencia que recibe": "María Gómez",
|
|
187
|
-
// Placa: "XYZ789",
|
|
188
|
-
// "Descripción": "Monitor Samsung",
|
|
189
|
-
// "Código de activo": "ACT-002",
|
|
190
|
-
// "N° de serie": "SN654321",
|
|
191
|
-
// Marca: "Samsung",
|
|
192
|
-
// Modelo: "S24F350",
|
|
193
|
-
// Estado: "Excelente",
|
|
194
|
-
// IdentificadorOrigen: 101,
|
|
195
|
-
// IdentificadorDestino: 202
|
|
196
|
-
// ]
|
|
197
|
-
// Espera un Array de objetos.
|
|
198
|
-
//
|
|
199
|
-
// Ejemplo ParametrosExcluidos:
|
|
200
|
-
// const ParametrosExcluidos = ['IdentificadorOrigen', 'Justificacion', 'IdentificadorDestino'];
|
|
201
|
-
// Valores devueltos por el QUERY que no sean necesarios mostrar en la tabla.
|
|
202
|
-
//
|
|
203
|
-
// Ejemplo ParametrosExtra:
|
|
204
|
-
// const ParametrosExtra = ['Toma física'];
|
|
205
|
-
// En caso de necesitar un espacio extra en la tabla que no se encuentra en la lista principal.
|
|
206
|
-
//
|
|
207
|
-
// Ejemplo MostrarEncabezado:
|
|
208
|
-
// MostrarEncabezado = true (Valor por defecto, muestra la barra azul "Registro N° 1")
|
|
209
|
-
// MostrarEncabezado = false (Oculta la barra azul, ideal para usar la función con un único registro)
|
|
210
|
-
|
|
211
|
-
if (!ElementosParaLaTabla?.length) return '<p>No hay datos para mostrar.</p>';
|
|
212
|
-
|
|
213
|
-
const baseColumnas = Object.keys(ElementosParaLaTabla[0]).filter(col => !ParametrosExcluidos.includes(col));
|
|
214
|
-
const columnas = [...baseColumnas, ...ParametrosExtra.filter(p => !baseColumnas.includes(p))];
|
|
215
|
-
|
|
216
|
-
let htmlFinal = '';
|
|
217
|
-
|
|
218
|
-
ElementosParaLaTabla.forEach((fila, index) => {
|
|
219
|
-
|
|
220
|
-
const filasHTML = columnas.map(col => {
|
|
221
|
-
const valor = fila[col] ?? '-';
|
|
222
|
-
return `
|
|
223
|
-
<tr>
|
|
224
|
-
<th style="width: 20%;">${col}</th>
|
|
225
|
-
<td style="width: 80%;">${valor}</td>
|
|
226
|
-
</tr>
|
|
227
|
-
`;
|
|
228
|
-
}).join('');
|
|
229
|
-
|
|
230
|
-
const encabezadoHTML = MostrarEncabezado ? `
|
|
231
|
-
<div style="background-color: #002f6b; color: white; padding: 6px 10px; font-weight: bold; font-size: 14px;">
|
|
232
|
-
Registro N° ${index + 1}
|
|
233
|
-
</div>
|
|
234
|
-
` : '';
|
|
235
|
-
|
|
236
|
-
htmlFinal += `
|
|
237
|
-
<div style="margin-top: 20px; page-break-inside: avoid; border: 1px solid #ccc; border-radius: 8px; overflow: hidden;">
|
|
238
|
-
|
|
239
|
-
${encabezadoHTML}
|
|
240
|
-
|
|
241
|
-
<table style="border: none; border-radius: 0; margin-top: 0;">
|
|
242
|
-
<tbody>
|
|
243
|
-
${filasHTML}
|
|
244
|
-
</tbody>
|
|
245
|
-
</table>
|
|
246
|
-
|
|
247
|
-
</div>`;
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
return htmlFinal;
|
|
112
|
+
const ReporteHTML = require('./ReporteHTML.js');
|
|
113
|
+
return ReporteHTML.GenerarReporteHTMLRegistrosVerticales(ElementosParaLaTabla, ParametrosExcluidos, ParametrosExtra, MostrarEncabezado);
|
|
251
114
|
}
|
|
252
115
|
|
|
253
116
|
GenerarReporteHTMLEncabezado(InformacionDeLaDerecha, titulares, marcaDeAgua = '') {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return ` <!DOCTYPE html>
|
|
258
|
-
<html lang="es">
|
|
259
|
-
<head>
|
|
260
|
-
<meta charset="UTF-8">
|
|
261
|
-
<title>${titulares.titulo}</title>
|
|
262
|
-
|
|
263
|
-
<style>
|
|
264
|
-
@media print {
|
|
265
|
-
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
266
|
-
}
|
|
267
|
-
body { font-family: Roboto, "Helvetica Neue", sans-serif; margin: 8px; }
|
|
268
|
-
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; min-height: 70px; }
|
|
269
|
-
.header > div { flex: 1; }
|
|
270
|
-
.logo { flex-shrink: 0; }
|
|
271
|
-
.title-container { text-align: center; flex-grow: 1; }
|
|
272
|
-
.title { font-size: 16px; font-weight: bold; white-space: nowrap; flex-shrink: 0; }
|
|
273
|
-
.subtitle { font-size: 13px; font-weight: bold; margin-top: 4px; }
|
|
274
|
-
.info { text-align: right; font-size: 11px; margin-left: 10px; flex-shrink: 0; }
|
|
275
|
-
table { width: 100%; border-collapse: separate; border-spacing: 0; border-radius: 10px; overflow: hidden; }
|
|
276
|
-
th, td { border: 1px solid #ccc; padding: 4px; text-align: left; font-size: 12px; }
|
|
277
|
-
th { background-color: #002f6b; color: white; }
|
|
278
|
-
tr:nth-child(even) { background-color: #f9f9f9; }
|
|
279
|
-
td:last-child { width: 100px; }
|
|
280
|
-
hr { border: none; border-top: 2px solid #838383; margin: 8px 0; }
|
|
281
|
-
.watermark { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-45deg); font-size: 80px; color: rgba(100, 100, 100, 0.09); font-weight: bold; z-index: 999; white-space: nowrap; pointer-events: none; }
|
|
282
|
-
</style>
|
|
283
|
-
|
|
284
|
-
</head>
|
|
285
|
-
<body>
|
|
286
|
-
<div class="watermark">${marcaDeAgua}</div>
|
|
287
|
-
<div class="header">
|
|
288
|
-
<div class="logo">
|
|
289
|
-
<img src="https://storage.sigu.utn.ac.cr/images/cards/LogoUTN.svg" alt="Logo UTN" style="height:50px;">
|
|
290
|
-
</div>
|
|
291
|
-
<div class="title-container">
|
|
292
|
-
<div class="title">Universidad Técnica Nacional</div>
|
|
293
|
-
<div class="subtitle">${titulares.primerSubtitulo}</div>
|
|
294
|
-
<div class="subtitle">${titulares.segundoSubtitulo}</div>
|
|
295
|
-
</div>
|
|
296
|
-
<div class="info">
|
|
297
|
-
${InformacionDeLaDerecha}
|
|
298
|
-
</div>
|
|
299
|
-
|
|
300
|
-
</div> `;
|
|
117
|
+
const ReporteHTML = require('./ReporteHTML.js');
|
|
118
|
+
return ReporteHTML.GenerarReporteHTMLEncabezado(InformacionDeLaDerecha, titulares, marcaDeAgua);
|
|
301
119
|
}
|
|
302
120
|
|
|
303
121
|
GenerarReporteHTMLFecha() {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
const day = now.getDate().toString().padStart(2, '0');
|
|
307
|
-
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
|
308
|
-
const year = now.getFullYear();
|
|
309
|
-
|
|
310
|
-
let hours = now.getHours();
|
|
311
|
-
const minutes = now.getMinutes().toString().padStart(2, '0');
|
|
312
|
-
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
313
|
-
|
|
314
|
-
hours = hours % 12;
|
|
315
|
-
hours = hours ? hours : 12;
|
|
316
|
-
hours = hours.toString().padStart(2, '0');
|
|
317
|
-
|
|
318
|
-
const fechaFormateada = `${day}-${month}-${year} ${hours}:${minutes} ${ampm}`;
|
|
319
|
-
|
|
320
|
-
return ` <div style="margin-left: 0; font-size: 11px; margin-top:4px;">
|
|
321
|
-
<strong>Fecha:</strong>
|
|
322
|
-
<span style="
|
|
323
|
-
border-bottom: 1px solid #000;
|
|
324
|
-
margin-left: 10px;
|
|
325
|
-
display: inline-block;
|
|
326
|
-
padding: 0 4px 2px 4px;
|
|
327
|
-
vertical-align: middle;
|
|
328
|
-
text-align: center;
|
|
329
|
-
min-width: 120px;
|
|
330
|
-
">${fechaFormateada}
|
|
331
|
-
</span>
|
|
332
|
-
</div>
|
|
333
|
-
`
|
|
122
|
+
const ReporteHTML = require('./ReporteHTML.js');
|
|
123
|
+
return ReporteHTML.GenerarReporteHTMLFecha();
|
|
334
124
|
}
|
|
335
125
|
|
|
336
126
|
GenerarReporteHTMLTablas(ElementosParaLaTabla, ParametrosExcluidos = [], ParametrosExtra = []) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// {
|
|
340
|
-
// "Fecha del traslado": "16-03-2026",
|
|
341
|
-
// "Justificacion": "Traslado por mantenimiento",
|
|
342
|
-
// "Dependencia que entrega": "Juan Pérez",
|
|
343
|
-
// "Dependencia que recibe": "María Gómez",
|
|
344
|
-
// Placa: "ABC123",
|
|
345
|
-
// "Descripción": "Laptop Dell",
|
|
346
|
-
// "Código de activo": "ACT-001",
|
|
347
|
-
// "N° de serie": "SN123456",
|
|
348
|
-
// Marca: "Dell",
|
|
349
|
-
// Modelo: "Latitude 5420",
|
|
350
|
-
// Estado: "Bueno",
|
|
351
|
-
// IdentificadorOrigen: 101,
|
|
352
|
-
// IdentificadorDestino: 202
|
|
353
|
-
// },
|
|
354
|
-
// {
|
|
355
|
-
// "Fecha del traslado": "16-03-2026",
|
|
356
|
-
// "Justificacion": "Traslado por mantenimiento",
|
|
357
|
-
// "Dependencia que entrega": "Juan Pérez",
|
|
358
|
-
// "Dependencia que recibe": "María Gómez",
|
|
359
|
-
// Placa: "XYZ789",
|
|
360
|
-
// "Descripción": "Monitor Samsung",
|
|
361
|
-
// "Código de activo": "ACT-002",
|
|
362
|
-
// "N° de serie": "SN654321",
|
|
363
|
-
// Marca: "Samsung",
|
|
364
|
-
// Modelo: "S24F350",
|
|
365
|
-
// Estado: "Excelente",
|
|
366
|
-
// IdentificadorOrigen: 101,
|
|
367
|
-
// IdentificadorDestino: 202
|
|
368
|
-
// }
|
|
369
|
-
// ]
|
|
370
|
-
// Espera un Array
|
|
371
|
-
//Ejemplo ParametrosExcluidos:
|
|
372
|
-
//const ParametrosExcluidos = ['IdentificadorOrigen', 'Justificacion', 'IdentificadorDestino'];
|
|
373
|
-
//Valores devueltos por el QUERY que no sean necesarios mostrar en la tabla.
|
|
374
|
-
|
|
375
|
-
//Ejemplo ParametrosExtra:
|
|
376
|
-
//const ParametrosExtra = ['Toma física'];
|
|
377
|
-
//En caso de necesitar un espacio extra en la tabla que no se encuentra en la lista principal.
|
|
378
|
-
|
|
379
|
-
if (!ElementosParaLaTabla?.length) return '';
|
|
380
|
-
|
|
381
|
-
const baseColumnas = Object.keys(ElementosParaLaTabla[0]).filter(col => !ParametrosExcluidos.includes(col));
|
|
382
|
-
const columnas = [...baseColumnas, ...ParametrosExtra.filter(p => !baseColumnas.includes(p))];
|
|
383
|
-
|
|
384
|
-
const tableHeaders = `<th>N°</th>` + columnas.map(col => `<th>${col}</th>`).join('');
|
|
385
|
-
|
|
386
|
-
const rows = ElementosParaLaTabla.map((fila, index) => {
|
|
387
|
-
const rowCells = columnas.map(col => {
|
|
388
|
-
const valor = fila[col] ?? '';
|
|
389
|
-
return `<td>${valor}</td>`;
|
|
390
|
-
}).join('');
|
|
391
|
-
return `<tr><td><strong>${index + 1}</strong></td>${rowCells}</tr>`;
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const colCount = columnas.length + 1;
|
|
395
|
-
const minRows = 1;
|
|
396
|
-
let currentIndex = rows.length;
|
|
397
|
-
while (rows.length < minRows) {
|
|
398
|
-
let emptyCells = `<td><strong>${currentIndex + 1}</strong></td>`;
|
|
399
|
-
for (let i = 0; i < colCount - 1; i++) {
|
|
400
|
-
emptyCells += '<td></td>';
|
|
401
|
-
}
|
|
402
|
-
rows.push(`<tr style="height:40px">${emptyCells}</tr>`);
|
|
403
|
-
currentIndex++;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const tableRows = rows.join('');
|
|
407
|
-
return `
|
|
408
|
-
<table>
|
|
409
|
-
<thead>
|
|
410
|
-
<tr>${tableHeaders}</tr>
|
|
411
|
-
</thead>
|
|
412
|
-
<tbody>
|
|
413
|
-
${tableRows}
|
|
414
|
-
</tbody>
|
|
415
|
-
</table>`;
|
|
127
|
+
const ReporteHTML = require('./ReporteHTML.js');
|
|
128
|
+
return ReporteHTML.GenerarReporteHTMLTablas(ElementosParaLaTabla, ParametrosExcluidos, ParametrosExtra);
|
|
416
129
|
}
|
|
417
130
|
|
|
418
131
|
GenerarReporteHTMLPie() {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
</html>`;
|
|
132
|
+
const ReporteHTML = require('./ReporteHTML.js');
|
|
133
|
+
return ReporteHTML.GenerarReporteHTMLPie();
|
|
422
134
|
}
|
|
423
|
-
//la información adicional se concatena desde la llamada
|
|
424
|
-
//REPORTE TERMINA AQUÍ
|
|
425
135
|
|
|
426
136
|
obtenerNombreLaBaseDeDatos() {
|
|
427
137
|
return this.NombreDelRepositorioDeLaBaseDeDatos.slice(0, -3);
|
|
@@ -1114,7 +824,7 @@ class Miscelaneo {
|
|
|
1114
824
|
`a`.`Tipo`, IF(`a`.`Icono` <> '', CONCAT('https://storage.sigu.utn.ac.cr/images/cards/', `a`.`Icono`), '') AS `Icono`, `a`.`Color`, `a`.`Correo`,\
|
|
1115
825
|
`a`.`Version`, `a`.`FechaDePublicacion`, `a`.`AcuerdoDeNivelDeServicio`, `a`.`DiccionarioDeDatos`, `a`.`Repositorios`,\
|
|
1116
826
|
`a`.`EnlaceDelVideo`,`a`.`EnlaceDelManual`\
|
|
1117
|
-
, `a`.`Estado`, REGEXP_SUBSTR(
|
|
827
|
+
, `a`.`Estado`, REGEXP_SUBSTR(`a`.`Repositorios`, '[^,]*front[^,]*') AS `Frontend`\
|
|
1118
828
|
FROM `SIGU`.`SIGU_ModulosV2` `a`\
|
|
1119
829
|
JOIN `SIGU`.`SIGU_PermisosV2` `b` ON (`a`.`Nombre` = `b`.`Modulo`)\
|
|
1120
830
|
WHERE `a`.`Estado` = 'Activo' AND `a`.`Padre` = ? AND `b`.`PermisoId` IN (?)\
|
|
@@ -1544,7 +1254,7 @@ class Miscelaneo {
|
|
|
1544
1254
|
}
|
|
1545
1255
|
|
|
1546
1256
|
versionDelNucleo() {
|
|
1547
|
-
return "
|
|
1257
|
+
return "2.0.82" + " " + this.NombreCanonicoDelModulo;
|
|
1548
1258
|
}
|
|
1549
1259
|
|
|
1550
1260
|
async destinatarioDeInformesDeError() {
|
|
@@ -1616,16 +1326,16 @@ class Miscelaneo {
|
|
|
1616
1326
|
, [Resultado.uid]);
|
|
1617
1327
|
}
|
|
1618
1328
|
|
|
1619
|
-
obtenerDatosDeLaPersonaPorIdentificacion(Identificacion) {
|
|
1620
|
-
return ejecutarConsultaSIGU("SELECT `a`.`Identificador`, `a`.`Identificacion`\
|
|
1329
|
+
async obtenerDatosDeLaPersonaPorIdentificacion(Identificacion) {
|
|
1330
|
+
return await ejecutarConsultaSIGU("SELECT `a`.`Identificador`, `a`.`Identificacion`\
|
|
1621
1331
|
, `a`.`Nombre`, `a`.`PrimerApellido`, `a`.`SegundoApellido`\
|
|
1622
1332
|
, (SELECT GROUP_CONCAT(`b`.`CuentaIBAN`) FROM `SIGU`.`SIGU_CuentasBancariasPersonas` `b` WHERE `b`.`Estado` = TRUE AND `a`.`Identificador` = `b`.`Identificador`) AS `CuentasIBAN`\
|
|
1623
1333
|
FROM `SIGU`.`SIGU_Personas` `a` WHERE `a`.`Identificacion` = ?"
|
|
1624
1334
|
, [Identificacion]);
|
|
1625
1335
|
}
|
|
1626
1336
|
|
|
1627
|
-
obtenerEstudiantes() {
|
|
1628
|
-
return ejecutarConsultaSIGU("SELECT `Identificador`, `Identificacion`, `Nombre`, `PrimerApellido`,\
|
|
1337
|
+
async obtenerEstudiantes() {
|
|
1338
|
+
return await ejecutarConsultaSIGU("SELECT `Identificador`, `Identificacion`, `Nombre`, `PrimerApellido`,\
|
|
1629
1339
|
`SegundoApellido` FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` IN\
|
|
1630
1340
|
(SELECT `Identificador` FROM `SIGU`.`SIGU_RolesPersonas` WHERE `PerfilGeneralId` = 1 /*El perfil 1 parece ser para Estudiantes*/)");
|
|
1631
1341
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const { ejecutarConsultaSIGU } = require('./db.js');
|
|
2
|
+
|
|
3
|
+
class ReporteHTML {
|
|
4
|
+
constructor() {}
|
|
5
|
+
|
|
6
|
+
async generarFirmaHTML(Identificador, FechaDeLaFirma) {
|
|
7
|
+
const [persona] = await ejecutarConsultaSIGU("SELECT Identificacion, CONCAT(Nombre, ' ', PrimerApellido, ' ', SegundoApellido) AS NombreCompleto FROM SIGU.SIGU_Personas WHERE Identificador = ?", [Identificador]);
|
|
8
|
+
|
|
9
|
+
if (!persona) return '';
|
|
10
|
+
|
|
11
|
+
const nombre = persona.NombreCompleto;
|
|
12
|
+
const identificacion = persona.Identificacion;
|
|
13
|
+
const instancia = await (async () => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await ejecutarConsultaSIGU(`
|
|
16
|
+
SELECT
|
|
17
|
+
CASE
|
|
18
|
+
WHEN i.TipoDeResponsabilidad = 'Ninguna' THEN sup.Nombre
|
|
19
|
+
ELSE i.Nombre
|
|
20
|
+
END AS Nombre
|
|
21
|
+
FROM SIGU.EstructuraOrganizacional_Instancias i
|
|
22
|
+
LEFT JOIN SIGU.EstructuraOrganizacional_Instancias sup
|
|
23
|
+
ON i.IdentificadorDeInstanciaSuperior = sup.IdentificadorDeInstancia
|
|
24
|
+
WHERE i.Identificador = ?
|
|
25
|
+
`, [Identificador]);
|
|
26
|
+
return result?.[0]?.Nombre || 'N/A';
|
|
27
|
+
} catch {
|
|
28
|
+
return 'N/A';
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
|
|
32
|
+
return ` <div style="
|
|
33
|
+
font-size: 9px;
|
|
34
|
+
border: 1px solid #eee;
|
|
35
|
+
padding: 6px;
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
background-color: #fcfcfc;
|
|
39
|
+
gap: 8px;
|
|
40
|
+
">
|
|
41
|
+
<div class="logo" style="display:flex; align-items:center;">
|
|
42
|
+
<img src="https://storage.sigu.utn.ac.cr/images/cards/LogoUTN.svg"
|
|
43
|
+
alt="Logo UTN"
|
|
44
|
+
style="height:25px;">
|
|
45
|
+
</div>
|
|
46
|
+
<div style="
|
|
47
|
+
width: 1px;
|
|
48
|
+
height: 50px;
|
|
49
|
+
background-color: #ccc;
|
|
50
|
+
"></div>
|
|
51
|
+
<div style="text-align: left;">
|
|
52
|
+
<strong>Firma electrónica por:</strong><br>
|
|
53
|
+
${nombre}<br>
|
|
54
|
+
Cédula: ${identificacion}<br>
|
|
55
|
+
Instancia: ${instancia}<br>
|
|
56
|
+
Fecha: ${FechaDeLaFirma}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
</div>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
GenerarReporteHTMLRegistrosVerticales(ElementosParaLaTabla, ParametrosExcluidos = [], ParametrosExtra = [], MostrarEncabezado = true) {
|
|
63
|
+
if (!ElementosParaLaTabla?.length) return '<p>No hay datos para mostrar.</p>';
|
|
64
|
+
|
|
65
|
+
const baseColumnas = Object.keys(ElementosParaLaTabla[0]).filter(col => !ParametrosExcluidos.includes(col));
|
|
66
|
+
const columnas = [...baseColumnas, ...ParametrosExtra.filter(p => !baseColumnas.includes(p))];
|
|
67
|
+
|
|
68
|
+
let htmlFinal = '';
|
|
69
|
+
|
|
70
|
+
ElementosParaLaTabla.forEach((fila, index) => {
|
|
71
|
+
|
|
72
|
+
const filasHTML = columnas.map(col => {
|
|
73
|
+
const valor = fila[col] ?? '-';
|
|
74
|
+
return `
|
|
75
|
+
<tr>
|
|
76
|
+
<th style="width: 20%;">${col}</th>
|
|
77
|
+
<td style="width: 80%;">${valor}</td>
|
|
78
|
+
</tr>
|
|
79
|
+
`;
|
|
80
|
+
}).join('');
|
|
81
|
+
|
|
82
|
+
const encabezadoHTML = MostrarEncabezado ? `
|
|
83
|
+
<div style="background-color: #002f6b; color: white; padding: 6px 10px; font-weight: bold; font-size: 14px;">
|
|
84
|
+
Registro N° ${index + 1}
|
|
85
|
+
</div>
|
|
86
|
+
` : '';
|
|
87
|
+
|
|
88
|
+
htmlFinal += `
|
|
89
|
+
<div style="margin-top: 20px; page-break-inside: avoid; border: 1px solid #ccc; border-radius: 8px; overflow: hidden;">
|
|
90
|
+
|
|
91
|
+
${encabezadoHTML}
|
|
92
|
+
|
|
93
|
+
<table style="border: none; border-radius: 0; margin-top: 0;">
|
|
94
|
+
<tbody>
|
|
95
|
+
${filasHTML}
|
|
96
|
+
</tbody>
|
|
97
|
+
</table>
|
|
98
|
+
|
|
99
|
+
</div>`;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return htmlFinal;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
GenerarReporteHTMLEncabezado(InformacionDeLaDerecha, titulares, marcaDeAgua = '') {
|
|
106
|
+
const date = new Date();
|
|
107
|
+
const year = date.getFullYear();
|
|
108
|
+
|
|
109
|
+
return ` <!DOCTYPE html>
|
|
110
|
+
<html lang="es">
|
|
111
|
+
<head>
|
|
112
|
+
<meta charset="UTF-8">
|
|
113
|
+
<title>${titulares.titulo}</title>
|
|
114
|
+
|
|
115
|
+
<style>
|
|
116
|
+
@media print {
|
|
117
|
+
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
118
|
+
}
|
|
119
|
+
body { font-family: Roboto, "Helvetica Neue", sans-serif; margin: 8px; }
|
|
120
|
+
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; min-height: 70px; }
|
|
121
|
+
.header > div { flex: 1; }
|
|
122
|
+
.logo { flex-shrink: 0; }
|
|
123
|
+
.title-container { text-align: center; flex-grow: 1; }
|
|
124
|
+
.title { font-size: 16px; font-weight: bold; white-space: nowrap; flex-shrink: 0; }
|
|
125
|
+
.subtitle { font-size: 13px; font-weight: bold; margin-top: 4px; }
|
|
126
|
+
.info { text-align: right; font-size: 11px; margin-left: 10px; flex-shrink: 0; }
|
|
127
|
+
table { width: 100%; border-collapse: separate; border-spacing: 0; border-radius: 10px; overflow: hidden; }
|
|
128
|
+
th, td { border: 1px solid #ccc; padding: 4px; text-align: left; font-size: 12px; }
|
|
129
|
+
th { background-color: #002f6b; color: white; }
|
|
130
|
+
tr:nth-child(even) { background-color: #f9f9f9; }
|
|
131
|
+
td:last-child { width: 100px; }
|
|
132
|
+
hr { border: none; border-top: 2px solid #838383; margin: 8px 0; }
|
|
133
|
+
.watermark { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-45deg); font-size: 80px; color: rgba(100, 100, 100, 0.09); font-weight: bold; z-index: 999; white-space: nowrap; pointer-events: none; }
|
|
134
|
+
</style>
|
|
135
|
+
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<div class="watermark">${marcaDeAgua}</div>
|
|
139
|
+
<div class="header">
|
|
140
|
+
<div class="logo">
|
|
141
|
+
<img src="https://storage.sigu.utn.ac.cr/images/cards/LogoUTN.svg" alt="Logo UTN" style="height:50px;">
|
|
142
|
+
</div>
|
|
143
|
+
<div class="title-container">
|
|
144
|
+
<div class="title">Universidad Técnica Nacional</div>
|
|
145
|
+
<div class="subtitle">${titulares.primerSubtitulo}</div>
|
|
146
|
+
<div class="subtitle">${titulares.segundoSubtitulo}</div>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="info">
|
|
149
|
+
${InformacionDeLaDerecha}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
</div> `;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
GenerarReporteHTMLFecha() {
|
|
156
|
+
const now = new Date();
|
|
157
|
+
|
|
158
|
+
const day = now.getDate().toString().padStart(2, '0');
|
|
159
|
+
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
|
160
|
+
const year = now.getFullYear();
|
|
161
|
+
|
|
162
|
+
let hours = now.getHours();
|
|
163
|
+
const minutes = now.getMinutes().toString().padStart(2, '0');
|
|
164
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
165
|
+
|
|
166
|
+
hours = hours % 12;
|
|
167
|
+
hours = hours ? hours : 12;
|
|
168
|
+
hours = hours.toString().padStart(2, '0');
|
|
169
|
+
|
|
170
|
+
const fechaFormateada = `\${day}-\${month}-\${year} \${hours}:\${minutes} \${ampm}\`;
|
|
171
|
+
|
|
172
|
+
return \` <div style="margin-left: 0; font-size: 11px; margin-top:4px;">
|
|
173
|
+
<strong>Fecha:</strong>
|
|
174
|
+
<span style="
|
|
175
|
+
border-bottom: 1px solid #000;
|
|
176
|
+
margin-left: 10px;
|
|
177
|
+
display: inline-block;
|
|
178
|
+
padding: 0 4px 2px 4px;
|
|
179
|
+
vertical-align: middle;
|
|
180
|
+
text-align: center;
|
|
181
|
+
min-width: 120px;
|
|
182
|
+
">\${fechaFormateada}
|
|
183
|
+
</span>
|
|
184
|
+
</div>
|
|
185
|
+
`
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
GenerarReporteHTMLTablas(ElementosParaLaTabla, ParametrosExcluidos = [], ParametrosExtra = []) {
|
|
189
|
+
if (!ElementosParaLaTabla?.length) return '';
|
|
190
|
+
|
|
191
|
+
const baseColumnas = Object.keys(ElementosParaLaTabla[0]).filter(col => !ParametrosExcluidos.includes(col));
|
|
192
|
+
const columnas = [...baseColumnas, ...ParametrosExtra.filter(p => !baseColumnas.includes(p))];
|
|
193
|
+
|
|
194
|
+
const tableHeaders = `<th>N°</th>\` + columnas.map(col => \`<th>\${col}</th>\`).join('');
|
|
195
|
+
|
|
196
|
+
const rows = ElementosParaLaTabla.map((fila, index) => {
|
|
197
|
+
const rowCells = columnas.map(col => {
|
|
198
|
+
const valor = fila[col] ?? '';
|
|
199
|
+
return \`<td>\${valor}</td>\`;
|
|
200
|
+
}).join('');
|
|
201
|
+
return \`<tr><td><strong>\${index + 1}</strong></td>\${rowCells}</tr>\`;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const colCount = columnas.length + 1;
|
|
205
|
+
const minRows = 1;
|
|
206
|
+
let currentIndex = rows.length;
|
|
207
|
+
while (rows.length < minRows) {
|
|
208
|
+
let emptyCells = \`<td><strong>\${currentIndex + 1}</strong></td>\`;
|
|
209
|
+
for (let i = 0; i < colCount - 1; i++) {
|
|
210
|
+
emptyCells += '<td></td>';
|
|
211
|
+
}
|
|
212
|
+
rows.push(\`<tr style="height:40px">\${emptyCells}</tr>\`);
|
|
213
|
+
currentIndex++;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const tableRows = rows.join('');
|
|
217
|
+
return \`
|
|
218
|
+
<table>
|
|
219
|
+
<thead>
|
|
220
|
+
<tr>\${tableHeaders}</tr>
|
|
221
|
+
</thead>
|
|
222
|
+
<tbody>
|
|
223
|
+
\${tableRows}
|
|
224
|
+
</tbody>
|
|
225
|
+
</table>`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
GenerarReporteHTMLPie() {
|
|
229
|
+
return `
|
|
230
|
+
</body>
|
|
231
|
+
</html>`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = new ReporteHTML();
|
|
@@ -23,7 +23,127 @@ class ReportePDF {
|
|
|
23
23
|
fontSizeMuyPequeño: 7
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
// const datosUsuario = await Miscelaneo.obtenerDatosDelUsuario(solicitud.headers.authorization);
|
|
28
|
+
// const Fecha = solicitud.params.Fecha || solicitud.query.Fecha;
|
|
29
|
+
// const titulos = {
|
|
30
|
+
// Principal: "Histórico de asignación de periodos",
|
|
31
|
+
// Subtitulo1: `Fecha de proceso: ${Fecha}`,
|
|
32
|
+
// Subtitulo2: "Reporte generado por el Área de Administración de Servicios",
|
|
33
|
+
// Direccion: "Dirección de Gestión de Desarrollo Humano",
|
|
34
|
+
// Area: "Área de Administración de Servicios AAS",
|
|
35
|
+
// Usuario: datosUsuario?.Identificacion || "Sistema",
|
|
36
|
+
// pieDePagina: "Copia digital del documento original.\nPara fines informáticos únicamente.",
|
|
37
|
+
// Acceso: "Privado",
|
|
38
|
+
// Uso: "Interno"
|
|
39
|
+
// };
|
|
40
|
+
// const metadatos = [
|
|
41
|
+
// { llave: 'Identificacion', titulo: 'Cédula', ancho: 80 },
|
|
42
|
+
// { llave: 'Nombre', titulo: 'Nombre Completo', ancho: 180 },
|
|
43
|
+
// { llave: 'Periodo', titulo: 'Período', ancho: 70 },
|
|
44
|
+
// { llave: 'Estamento', titulo: 'Estamento', ancho: 80 },
|
|
45
|
+
// { llave: 'DiasOtorgados', titulo: 'Días', ancho: 40 },
|
|
46
|
+
// { llave: 'Salario', titulo: 'Salario', ancho: 45 }
|
|
47
|
+
// ];
|
|
48
|
+
// const doc = ReportePDF.generarEncabezado(titulos, respuesta);
|
|
49
|
+
// const parrafo = 'Para más información, visite el <a href="https://www.utn.ac.cr">sitio web de la UTN</a>.';
|
|
50
|
+
// ReportePDF.generarParrafoHTML(doc, parrafo);
|
|
51
|
+
// ReportePDF.generarTituloDePagina(doc, titulos.Principal, titulos.Subtitulo1, titulos.Subtitulo2);
|
|
52
|
+
// ReportePDF.generarTabla(doc, datos, metadatos);
|
|
53
|
+
// let firmas = [
|
|
54
|
+
// {
|
|
55
|
+
// Nombre: "DAVID VILLALOBOS CAMBRONERO",
|
|
56
|
+
// Identificacion: "111050570",
|
|
57
|
+
// Instancia: "Dirección de TI",
|
|
58
|
+
// Fecha: "17-04-2026 14:30",
|
|
59
|
+
// Motivo: "Aprobación de vacaciones"
|
|
60
|
+
// },
|
|
61
|
+
// {
|
|
62
|
+
// Nombre: "JUAN PEREZ",
|
|
63
|
+
// Identificacion: "123456789",
|
|
64
|
+
// Instancia: "Recursos Humanos",
|
|
65
|
+
// Fecha: "17-04-2026 14:35",
|
|
66
|
+
// Motivo: "Validación de saldos"
|
|
67
|
+
// },
|
|
68
|
+
// {
|
|
69
|
+
// Nombre: "JUAN PEREZ",
|
|
70
|
+
// Identificacion: "123456789",
|
|
71
|
+
// Instancia: "Recursos Humanos",
|
|
72
|
+
// Fecha: "17-04-2026 14:35",
|
|
73
|
+
// Motivo: "Revisión técnica"
|
|
74
|
+
// },
|
|
75
|
+
// {
|
|
76
|
+
// Nombre: "JUAN PEREZ",
|
|
77
|
+
// Identificacion: "123456789",
|
|
78
|
+
// Instancia: "Recursos Humanos",
|
|
79
|
+
// Fecha: "17-04-2026 14:35",
|
|
80
|
+
// Motivo: "Revisión técnica"
|
|
81
|
+
// },
|
|
82
|
+
// {
|
|
83
|
+
// Nombre: "JUAN PEREZ",
|
|
84
|
+
// Identificacion: "123456789",
|
|
85
|
+
// Instancia: "Recursos Humanos",
|
|
86
|
+
// Fecha: "17-04-2026 14:35",
|
|
87
|
+
// Motivo: "Revisión técnica"
|
|
88
|
+
// },
|
|
89
|
+
// {
|
|
90
|
+
// Nombre: "JUAN PEREZ",
|
|
91
|
+
// Identificacion: "123456789",
|
|
92
|
+
// Instancia: "Recursos Humanos",
|
|
93
|
+
// Fecha: "17-04-2026 14:35",
|
|
94
|
+
// Motivo: "Revisión técnica"
|
|
95
|
+
// }
|
|
96
|
+
// ];
|
|
97
|
+
// ReportePDF.generarFirmas(doc, firmas);
|
|
98
|
+
// firmas = [
|
|
99
|
+
// {
|
|
100
|
+
// Nombre: "DAVID VILLALOBOS CAMBRONERO",
|
|
101
|
+
// Identificacion: "111050570",
|
|
102
|
+
// Instancia: "Dirección de TI",
|
|
103
|
+
// Fecha: "17-04-2026 14:30",
|
|
104
|
+
// Motivo: "Aprobación de vacaciones"
|
|
105
|
+
// }
|
|
106
|
+
// ]
|
|
107
|
+
// ReportePDF.generarFirmas(doc, firmas);
|
|
108
|
+
// firmas = [
|
|
109
|
+
// {
|
|
110
|
+
// Nombre: "DAVID VILLALOBOS CAMBRONERO",
|
|
111
|
+
// Identificacion: "111050570",
|
|
112
|
+
// Instancia: "Dirección de TI",
|
|
113
|
+
// Fecha: "17-04-2026 14:30",
|
|
114
|
+
// Motivo: "Aprobación de vacaciones"
|
|
115
|
+
// },
|
|
116
|
+
// {
|
|
117
|
+
// Nombre: "DAVID VILLALOBOS CAMBRONERO",
|
|
118
|
+
// Identificacion: "111050570",
|
|
119
|
+
// Instancia: "Dirección de TI",
|
|
120
|
+
// Fecha: "17-04-2026 14:30",
|
|
121
|
+
// Motivo: "Aprobación de vacaciones"
|
|
122
|
+
// }
|
|
123
|
+
// ]
|
|
124
|
+
// ReportePDF.generarFirmas(doc, firmas);
|
|
125
|
+
// ReportePDF.generarTituloDePagina(doc, titulos.Principal, titulos.Subtitulo1, titulos.Subtitulo2);
|
|
126
|
+
// ReportePDF.generarTituloDePagina(doc, titulos.Principal, titulos.Subtitulo1, titulos.Subtitulo2);
|
|
127
|
+
// ReportePDF.generarTabla(doc, datos, metadatos);
|
|
128
|
+
// ReportePDF.generarTabla(doc, datos, metadatos);
|
|
129
|
+
// const parrafoIntroductorio = `Este <b>reporte</b> detalla la asignación <u>automática</u> de periodos de vacaciones realizada en la fecha seleccionada. <i>Los datos mostrados</i> corresponden al estado final de cada registro tras el proceso de cálculo.`;
|
|
130
|
+
// ReportePDF.generarParrafoHTML(doc, parrafoIntroductorio);
|
|
131
|
+
// ReportePDF.generarTabla(doc, datos, metadatos);
|
|
132
|
+
// ReportePDF.generarSaltoDePagina(doc);
|
|
133
|
+
// ReportePDF.generarTabla(doc, datos, metadatos);
|
|
134
|
+
// ReportePDF.generarParrafoHTML(doc, parrafo);
|
|
135
|
+
// ReportePDF.generarMarcaDeAgua(doc, "TEXTO DEL SELLO", "#FF0000");
|
|
136
|
+
// ReportePDF.generarFoliado(doc, 100);
|
|
137
|
+
// ReportePDF.generarPie(doc, "Copia digital del documento original.\nPara fines informáticos únicamente.");
|
|
138
|
+
// const MetaDatos = {
|
|
139
|
+
// Titulo: "UTN-1-2025-10841",
|
|
140
|
+
// Identificador: 12,
|
|
141
|
+
// Autor: "VALLADARES BONILLA TONNY MAURICIO 603230047",
|
|
142
|
+
// Asunto: "Boleta de Vacaciones"
|
|
143
|
+
// };
|
|
144
|
+
// await ReportePDF.generarMetadatosReporte(doc, MetaDatos);
|
|
145
|
+
// ReportePDF.finalizarReporte(doc);
|
|
146
|
+
|
|
27
147
|
async verificarSiReporteYaExiste(Modulo, Titulo, Identificador) {
|
|
28
148
|
try {
|
|
29
149
|
const query = `SELECT COUNT(*) AS total FROM SIGU.SIGU_ReportesGenerados WHERE Modulo = ? AND Titulo = ? AND Identificador = ?`;
|
|
@@ -143,7 +263,11 @@ class ReportePDF {
|
|
|
143
263
|
generarEncabezado(Titulos, respuesta) {
|
|
144
264
|
respuesta.setHeader("Content-Type", "application/pdf");
|
|
145
265
|
respuesta.setHeader("Content-Disposition", `inline; filename=Reporte.pdf`);
|
|
146
|
-
const doc = new PDFDocument({
|
|
266
|
+
const doc = new PDFDocument({
|
|
267
|
+
size: 'Letter',
|
|
268
|
+
margins: { top: 105, left: this.config.margin, right: this.config.margin, bottom: this.config.margin },
|
|
269
|
+
bufferPages: true
|
|
270
|
+
});
|
|
147
271
|
doc.pipe(respuesta);
|
|
148
272
|
|
|
149
273
|
// Función interna para dibujar el encabezado
|
|
@@ -170,12 +294,20 @@ class ReportePDF {
|
|
|
170
294
|
const fechaActual = now.toLocaleDateString('es-CR');
|
|
171
295
|
const horaActual = now.toLocaleTimeString('es-CR', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
172
296
|
doc.fontSize(this.config.fontSizePequeño).fillColor(this.config.colorPrimario);
|
|
173
|
-
doc.text(`Fecha: ${fechaActual}`, pageWidth - margin -
|
|
174
|
-
doc.text(`Hora: ${horaActual}`, pageWidth - margin -
|
|
175
|
-
doc.text(`Usuario: ${Titulos.Usuario}`, pageWidth - margin -
|
|
297
|
+
doc.text(`Fecha: ${fechaActual}`, pageWidth - margin - 200, 45, { align: 'right', width: 200 });
|
|
298
|
+
doc.text(`Hora: ${horaActual}`, pageWidth - margin - 200, 55, { align: 'right', width: 200 });
|
|
299
|
+
doc.text(`Usuario: ${Titulos.Usuario}`, pageWidth - margin - 200, 65, { align: 'right', width: 200 });
|
|
300
|
+
|
|
301
|
+
const acceso = Titulos.Acceso || 'Interno';
|
|
302
|
+
const uso = Titulos.Uso || 'Público';
|
|
303
|
+
doc.text(`Acceso: ${acceso}, Uso: ${uso}`, pageWidth - margin - 200, 75, { align: 'right', width: 200 });
|
|
304
|
+
|
|
176
305
|
// Línea separadora
|
|
177
306
|
doc.moveTo(margin, 90).lineTo(pageWidth - margin, 90).strokeColor(this.config.colorPrimario).lineWidth(2).stroke();
|
|
178
|
-
|
|
307
|
+
|
|
308
|
+
// Asegurar que el cursor quede posicionado debajo del encabezado
|
|
309
|
+
doc.x = margin;
|
|
310
|
+
doc.y = 105;
|
|
179
311
|
};
|
|
180
312
|
// Evento para añadir el encabezado institucional automáticamente en CADA NUEVA PÁGINA
|
|
181
313
|
doc.on('pageAdded', dibujarEncabezado);
|
|
@@ -245,12 +377,89 @@ class ReportePDF {
|
|
|
245
377
|
// Como el texto de la izquierda tiene 2 líneas, alineamos la paginación un poco más abajo para que quede centrado
|
|
246
378
|
doc.text(paginacion, centerX + 15, footerY + 14, { width: (pageWidth / 2) - margin - 15, align: 'right' });
|
|
247
379
|
|
|
380
|
+
// Si el documento tiene firmas y estamos en la última página, agregar la leyenda de seguridad
|
|
381
|
+
if (doc._tieneFirmas && i === range.start + range.count - 1) {
|
|
382
|
+
const leyenda = "Documento firmado electrónicamente con firma digital interna de la Universidad Técnica Nacional. Incluye un código de verificación único validable en el sistema SIGU. Cualquier modificación invalida su autenticidad.";
|
|
383
|
+
doc.fillColor("black").fontSize(6).font(this.config.fuenteBase);
|
|
384
|
+
|
|
385
|
+
// Calculamos la posición Y para que esté inmediatamente por encima de la línea del footer (footerY)
|
|
386
|
+
// Estimamos un alto aproximado dependiendo de cuantas líneas abarque el texto,
|
|
387
|
+
// pdfkit calculará la altura si usamos heightOfString, pero podemos darle un espacio fijo.
|
|
388
|
+
const alturaLeyenda = doc.heightOfString(leyenda, { width: pageWidth - 2 * margin, align: 'center' });
|
|
389
|
+
const yPosLeyenda = footerY - alturaLeyenda - 5;
|
|
390
|
+
|
|
391
|
+
doc.text(leyenda, margin, yPosLeyenda, {
|
|
392
|
+
width: pageWidth - 2 * margin,
|
|
393
|
+
align: 'center'
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
248
397
|
// Restaurar el margen inferior original
|
|
249
398
|
doc.page.margins.bottom = bottomMargin;
|
|
250
399
|
}
|
|
251
400
|
// doc.end();
|
|
252
401
|
}
|
|
253
402
|
|
|
403
|
+
generarFoliado(doc, folioInicial) {
|
|
404
|
+
if (!doc || folioInicial === undefined) return;
|
|
405
|
+
|
|
406
|
+
const range = doc.bufferedPageRange();
|
|
407
|
+
let folioActual = parseInt(folioInicial);
|
|
408
|
+
|
|
409
|
+
for (let i = range.start; i < range.start + range.count; i++) {
|
|
410
|
+
doc.switchToPage(i);
|
|
411
|
+
|
|
412
|
+
const margin = this.config.margin;
|
|
413
|
+
const pageWidth = doc.page.width;
|
|
414
|
+
const yPos = 30; // Posición en la parte superior, fuera del área del encabezado principal
|
|
415
|
+
|
|
416
|
+
doc.save();
|
|
417
|
+
doc.fillColor("black")
|
|
418
|
+
.fontSize(this.config.fontSizeSubtitulo)
|
|
419
|
+
.font(this.config.fuenteNegrita);
|
|
420
|
+
|
|
421
|
+
doc.text(String(folioActual), pageWidth - margin - 100, yPos, {
|
|
422
|
+
width: 100,
|
|
423
|
+
align: 'right'
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
doc.restore();
|
|
427
|
+
folioActual++;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
generarMarcaDeAgua(doc, texto, color = "#CCCCCC") {
|
|
432
|
+
if (!doc || !texto) return;
|
|
433
|
+
|
|
434
|
+
const range = doc.bufferedPageRange();
|
|
435
|
+
for (let i = range.start; i < range.start + range.count; i++) {
|
|
436
|
+
doc.switchToPage(i);
|
|
437
|
+
|
|
438
|
+
const pageWidth = doc.page.width;
|
|
439
|
+
const pageHeight = doc.page.height;
|
|
440
|
+
|
|
441
|
+
doc.save();
|
|
442
|
+
|
|
443
|
+
// Configuración de la marca de agua
|
|
444
|
+
doc.fillColor(color);
|
|
445
|
+
doc.fillOpacity(0.15); // Transparencia para que no tape el contenido
|
|
446
|
+
doc.fontSize(60);
|
|
447
|
+
doc.font(this.config.fuenteNegrita);
|
|
448
|
+
|
|
449
|
+
// Posicionamiento central con rotación
|
|
450
|
+
doc.translate(pageWidth / 2, pageHeight / 2);
|
|
451
|
+
doc.rotate(-45);
|
|
452
|
+
|
|
453
|
+
// Dibujar el texto centrado en el eje rotado
|
|
454
|
+
doc.text(texto, -pageWidth / 2, -30, {
|
|
455
|
+
width: pageWidth,
|
|
456
|
+
align: 'center'
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
doc.restore();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
254
463
|
generarTabla(doc, datos, metadatos) {
|
|
255
464
|
if (!datos || datos.length === 0) return;
|
|
256
465
|
|
|
@@ -342,6 +551,7 @@ class ReportePDF {
|
|
|
342
551
|
let isBold = false;
|
|
343
552
|
let isItalic = false;
|
|
344
553
|
let isUnderline = false;
|
|
554
|
+
let currentLink = null;
|
|
345
555
|
|
|
346
556
|
const fontSize = opciones.fontSize || this.config.fontSizeSubtitulo;
|
|
347
557
|
const align = opciones.align || 'justify';
|
|
@@ -358,6 +568,13 @@ class ReportePDF {
|
|
|
358
568
|
|
|
359
569
|
doc.fillColor('black');
|
|
360
570
|
|
|
571
|
+
// Comprobamos si hay espacio suficiente en la página antes de empezar a dibujar
|
|
572
|
+
// Estimamos un alto de línea basado en el tamaño de fuente
|
|
573
|
+
const estimatedHeight = fontSize * 1.5;
|
|
574
|
+
if (doc.y + estimatedHeight > doc.page.height - doc.page.margins.bottom - 40) {
|
|
575
|
+
doc.addPage();
|
|
576
|
+
}
|
|
577
|
+
|
|
361
578
|
// Como pdfkit podría venir de un texto dibujado con coordenadas X y Y absolutas (que rompe el flujo interno de texto),
|
|
362
579
|
// definimos la posición X al margen izquierdo para iniciar el párrafo, manteniendo la coordenada Y actual.
|
|
363
580
|
doc.x = margin;
|
|
@@ -385,10 +602,26 @@ class ReportePDF {
|
|
|
385
602
|
textoParaImprimir += ' ';
|
|
386
603
|
}
|
|
387
604
|
|
|
605
|
+
// Comprobar espacio por cada fragmento para prevenir que `continued` desborde mal
|
|
606
|
+
if (doc.y + fontSize > doc.page.height - doc.page.margins.bottom - 20) {
|
|
607
|
+
// En PDFKit, hacer addPage() mientras hay 'continued' true a veces falla, pero ayuda forzarlo antes.
|
|
608
|
+
doc.text(' ', { continued: false }); // forzar fin de linea
|
|
609
|
+
doc.addPage();
|
|
610
|
+
doc.x = margin; // reposicionar en nueva pagina
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Si hay un link activo, usamos color azul y subrayado por defecto si no se especificó lo contrario
|
|
614
|
+
if (currentLink) {
|
|
615
|
+
doc.fillColor('blue');
|
|
616
|
+
} else {
|
|
617
|
+
doc.fillColor('black');
|
|
618
|
+
}
|
|
619
|
+
|
|
388
620
|
doc.text(textoParaImprimir, {
|
|
389
621
|
width: availableWidth,
|
|
390
622
|
continued: isContinued,
|
|
391
|
-
underline: isUnderline,
|
|
623
|
+
underline: isUnderline || (currentLink ? true : false),
|
|
624
|
+
link: currentLink,
|
|
392
625
|
align: align
|
|
393
626
|
});
|
|
394
627
|
};
|
|
@@ -425,6 +658,17 @@ class ReportePDF {
|
|
|
425
658
|
isUnderline = false;
|
|
426
659
|
continue;
|
|
427
660
|
}
|
|
661
|
+
if (lowerToken.startsWith('<a')) {
|
|
662
|
+
const hrefMatch = token.match(/href=["']([^"']*)["']/i);
|
|
663
|
+
if (hrefMatch) {
|
|
664
|
+
currentLink = hrefMatch[1];
|
|
665
|
+
}
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (lowerToken === '</a>') {
|
|
669
|
+
currentLink = null;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
428
672
|
if (lowerToken === '<br>' || lowerToken === '<br/>' || lowerToken === '<br />') {
|
|
429
673
|
// Si hay un salto de línea, rompemos el continued escribiendo un espacio final
|
|
430
674
|
doc.text(' ', { continued: false });
|
|
@@ -451,6 +695,122 @@ class ReportePDF {
|
|
|
451
695
|
|
|
452
696
|
doc.moveDown(1);
|
|
453
697
|
}
|
|
698
|
+
|
|
699
|
+
generarFirmas(doc, firmas) {
|
|
700
|
+
if (!firmas || !Array.isArray(firmas) || firmas.length === 0) return;
|
|
701
|
+
|
|
702
|
+
doc._tieneFirmas = true; // Marcar el documento como firmado para el pie de página
|
|
703
|
+
|
|
704
|
+
const margin = this.config.margin;
|
|
705
|
+
const pageWidth = doc.page.width;
|
|
706
|
+
const availableWidth = pageWidth - 2 * margin;
|
|
707
|
+
const signatureHeight = 75;
|
|
708
|
+
const gap = 10;
|
|
709
|
+
const logoWidth = 35;
|
|
710
|
+
const padding = 6;
|
|
711
|
+
|
|
712
|
+
// Dividimos las firmas en grupos de 3 (máximo por fila)
|
|
713
|
+
for (let i = 0; i < firmas.length; i += 3) {
|
|
714
|
+
const filaDeFirmas = firmas.slice(i, i + 3);
|
|
715
|
+
const cantidadEnFila = filaDeFirmas.length;
|
|
716
|
+
|
|
717
|
+
// Ancho total por bloque de firma según la cantidad en esta fila
|
|
718
|
+
const blockWidth = availableWidth / cantidadEnFila;
|
|
719
|
+
|
|
720
|
+
// Verificar si hay espacio en la página actual para esta fila
|
|
721
|
+
if (doc.y + signatureHeight > doc.page.height - 80) {
|
|
722
|
+
doc.addPage();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const startY = doc.y + 10;
|
|
726
|
+
|
|
727
|
+
filaDeFirmas.forEach((firma, index) => {
|
|
728
|
+
const blockX = margin + (index * blockWidth);
|
|
729
|
+
|
|
730
|
+
let innerWidth = blockWidth - gap;
|
|
731
|
+
let innerX = blockX + (gap / 2);
|
|
732
|
+
|
|
733
|
+
// Ajustar márgenes para los extremos
|
|
734
|
+
if (index === 0) {
|
|
735
|
+
innerX = blockX;
|
|
736
|
+
innerWidth = blockWidth - (gap / 2);
|
|
737
|
+
}
|
|
738
|
+
if (index === cantidadEnFila - 1) {
|
|
739
|
+
innerWidth = blockWidth - (gap / 2);
|
|
740
|
+
}
|
|
741
|
+
if (cantidadEnFila === 1) {
|
|
742
|
+
innerWidth = 240;
|
|
743
|
+
innerX = margin + (availableWidth / 2) - (innerWidth / 2);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Dibujar fondo y borde
|
|
747
|
+
doc.save();
|
|
748
|
+
doc.fillColor("#FCFCFC").strokeColor("#EEEEEE").lineWidth(1);
|
|
749
|
+
doc.rect(innerX, startY, innerWidth, signatureHeight).fillAndStroke();
|
|
750
|
+
doc.restore();
|
|
751
|
+
|
|
752
|
+
let currentX = innerX + padding;
|
|
753
|
+
|
|
754
|
+
// Dibujar logo
|
|
755
|
+
try {
|
|
756
|
+
const rutaLogo = path.join(__dirname, "./LogoUTN.png");
|
|
757
|
+
// Centrar verticalmente el logo dentro del recuadro
|
|
758
|
+
const logoY = startY + (signatureHeight / 2) - 13;
|
|
759
|
+
doc.image(rutaLogo, currentX, logoY, { width: logoWidth });
|
|
760
|
+
|
|
761
|
+
// Línea vertical separadora
|
|
762
|
+
doc.save();
|
|
763
|
+
doc.moveTo(currentX + logoWidth + padding, startY + padding)
|
|
764
|
+
.lineTo(currentX + logoWidth + padding, startY + signatureHeight - padding)
|
|
765
|
+
.strokeColor("#CCCCCC")
|
|
766
|
+
.lineWidth(0.5)
|
|
767
|
+
.stroke();
|
|
768
|
+
doc.restore();
|
|
769
|
+
|
|
770
|
+
currentX += logoWidth + (padding * 2);
|
|
771
|
+
} catch (e) {
|
|
772
|
+
// Si falla el logo, currentX se mantiene igual para el texto
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Área para el texto
|
|
776
|
+
const textWidth = innerWidth - (currentX - innerX) - padding;
|
|
777
|
+
let textY = startY + padding + 1;
|
|
778
|
+
|
|
779
|
+
doc.fillColor("black");
|
|
780
|
+
|
|
781
|
+
// 1. Título
|
|
782
|
+
doc.fontSize(6).font(this.config.fuenteNegrita);
|
|
783
|
+
doc.text("Firma electrónica por:", currentX, textY, { width: textWidth, align: 'left' });
|
|
784
|
+
textY = doc.y + 1;
|
|
785
|
+
|
|
786
|
+
// 2. Nombre
|
|
787
|
+
doc.fontSize(6).font(this.config.fuenteBase);
|
|
788
|
+
doc.text(firma.Nombre || 'N/A', currentX, textY, { width: textWidth, align: 'left' });
|
|
789
|
+
textY = doc.y + 1;
|
|
790
|
+
|
|
791
|
+
// 3. Cédula
|
|
792
|
+
doc.text(`Cédula: ${firma.Identificacion || 'N/A'}`, currentX, textY, { width: textWidth, align: 'left' });
|
|
793
|
+
textY = doc.y + 1;
|
|
794
|
+
|
|
795
|
+
// 4. Instancia
|
|
796
|
+
doc.text(`Instancia: ${firma.Instancia || 'N/A'}`, currentX, textY, { width: textWidth, align: 'left' });
|
|
797
|
+
textY = doc.y + 1;
|
|
798
|
+
|
|
799
|
+
// 5. Fecha
|
|
800
|
+
doc.text(`Fecha: ${firma.Fecha || 'N/A'}`, currentX, textY, { width: textWidth, align: 'left' });
|
|
801
|
+
textY = doc.y + 1;
|
|
802
|
+
|
|
803
|
+
// 6. Motivo
|
|
804
|
+
doc.text(`Motivo: ${firma.Motivo || 'N/A'}`, currentX, textY, { width: textWidth, align: 'left' });
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// Mover el cursor para la siguiente fila
|
|
808
|
+
doc.y = startY + signatureHeight + 10;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Agregar un poco de margen después de todas las firmas
|
|
812
|
+
doc.y += 10;
|
|
813
|
+
}
|
|
454
814
|
}
|
|
455
815
|
|
|
456
816
|
module.exports = new ReportePDF();
|
|
@@ -86,7 +86,6 @@ class Servicio1 {
|
|
|
86
86
|
// Resultados = await ejecutarConsultaSIGU(SELECTBase + " SELECT * FROM Datos WHERE " + WHERE + ORDERBY + " LIMIT ? OFFSET ?", [parseInt(Parametros.PaginadorTamanio), parseInt(Parametros.PaginadorIndice)]);
|
|
87
87
|
// return { Registros: Resultados, TotalDeRegistros: Total[0]['Total'] };
|
|
88
88
|
// }
|
|
89
|
-
// const ss = "";
|
|
90
89
|
// }
|
|
91
90
|
// // Predeterminadamente se retornan los datos paginados pero sin filtrar
|
|
92
91
|
// // Los símbolos &&&& se usan como valor predeterminado
|
package/templates/frontend/src/app/Paginas/gestion-de-reportes/gestion-de-reportes.component.css
CHANGED
|
@@ -1,4 +1,54 @@
|
|
|
1
1
|
.contenido {
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-wrap: wrap;
|
|
4
|
+
gap: 16px;
|
|
5
|
+
padding: 16px;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
width: 100%;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Forzamos 4 columnas calculando el espacio disponible */
|
|
11
|
+
.contenido>* {
|
|
12
|
+
width: calc(25% - 12px);
|
|
13
|
+
/* 25% menos una parte del gap (16px * 3 / 4) */
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.cdk-drag-preview {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.cdk-drag-placeholder {
|
|
25
|
+
opacity: 0.3;
|
|
26
|
+
background: #ccc;
|
|
27
|
+
border: 2px dashed #999;
|
|
28
|
+
border-radius: 8px;
|
|
29
|
+
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
|
30
|
+
/* El placeholder debe mantener el tamaño de la tarjeta */
|
|
31
|
+
min-height: 150px;
|
|
32
|
+
/* Ajustar según la altura promedio de tus tarjetas */
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.cdk-drag-animating {
|
|
36
|
+
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.contenido.cdk-drop-list-dragging div:not(.cdk-drag-placeholder) {
|
|
40
|
+
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Responsividad: 2 columnas en tablets, 1 en móviles */
|
|
44
|
+
@media (max-width: 1200px) {
|
|
45
|
+
.contenido>* {
|
|
46
|
+
width: calc(50% - 8px);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@media (max-width: 600px) {
|
|
51
|
+
.contenido>* {
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
4
54
|
}
|
|
@@ -172,6 +172,18 @@ export class GestionTablaXYZComponent {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
obtenerDatosParaPoblarLaTabla() {
|
|
175
|
+
// let Filtrado = undefined
|
|
176
|
+
// if (this.PaginarResultados) {
|
|
177
|
+
// if (this.PaginadorTamanio === 0) {
|
|
178
|
+
// const pageSize = localStorage.getItem('pageSize');
|
|
179
|
+
// if (pageSize) {
|
|
180
|
+
// this.PaginadorTamanio = parseInt(pageSize);
|
|
181
|
+
// }
|
|
182
|
+
// }
|
|
183
|
+
// Filtrado = `/${this.PaginadorAccion}/${this.PaginadorIndice}/${this.PaginadorTamanio}/${this.PaginadorFiltro}/${this.PaginadorColumnaParaFiltrar}/${this.PaginadorColumnasParaFiltrar}/${this.ColumnaParaOrdenar}/${this.TipoDeOrden}`;
|
|
184
|
+
// } else {
|
|
185
|
+
// Filtrado = `/TablaSinFiltro/-/-/-/-/-/-/-`;
|
|
186
|
+
// }
|
|
175
187
|
this.http.get(`${this.datosGlobalesService.ObtenerURL()}XYZ/listar`).subscribe({
|
|
176
188
|
next: (datos: any) => {
|
|
177
189
|
if (datos.error) {
|