utn-cli 2.0.88 → 2.0.89

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.0.88",
3
+ "version": "2.0.89",
4
4
  "description": "Herramienta CLI unificada para la gestión de plantillas en SIGU.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- const { ejecutarConsulta, ejecutarConsultaSIGU, ejecutarConsultaCumulo, crearObjetoConexionSIGU } = require('./db.js');
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 [persona] = await ejecutarConsultaSIGU("SELECT Identificacion, CONCAT(Nombre, ' ', PrimerApellido, ' ', SegundoApellido) AS NombreCompleto FROM SIGU.SIGU_Personas WHERE Identificador = ?", [Identificador]);
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
- // Ejemplo ElementosParaLaTabla:
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 date = new Date();
255
- const year = date.getFullYear();
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 now = new Date();
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
- //Ejemplo ElementosParaLaTabla:
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
- return `
420
- </body>
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);
@@ -751,68 +461,6 @@ class Miscelaneo {
751
461
  return;
752
462
  }
753
463
 
754
- async AutenticarConGoogle(Solicitud) {
755
- const { OAuth2Client } = require('google-auth-library');
756
- const jwt = require('jsonwebtoken');
757
- const clientId = await this.googleClientId();
758
- const client = new OAuth2Client(clientId);
759
- const ConexionSigu = await crearObjetoConexionSIGU();
760
- const LastUser = await this.generarLastUser(Solicitud);
761
-
762
- try {
763
- const ticket = await client.verifyIdToken({
764
- idToken: Solicitud.body.token,
765
- audience: clientId,
766
- });
767
- const payload = ticket.getPayload();
768
- const email = payload['email'];
769
-
770
- const resultadosEmail = await ConexionSigu.query("SELECT `Identificador` FROM `SIGU`.`SIGU_CorreosPersona` WHERE `CorreoElectronico` = ? AND `Principal` = TRUE LIMIT 1", [email]);
771
-
772
- if (resultadosEmail[0].length === 0) {
773
- console.log("El correo de Google", email, "no está registrado como principal en SIGU");
774
- return { error: "Su cuenta de Google no está vinculada a ningún usuario de SIGU." };
775
- }
776
-
777
- const Identificador = resultadosEmail[0][0]['Identificador'];
778
- const resultadosUsuario = await ConexionSigu.query("SELECT `Identificacion` FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` = ? AND `Activo` = TRUE", [Identificador]);
779
-
780
- if (resultadosUsuario[0].length === 0) {
781
- return { error: "El usuario asociado a esta cuenta no está activo." };
782
- }
783
-
784
- const Identificacion = resultadosUsuario[0][0]['Identificacion'];
785
-
786
- // Generar Token
787
- const Token = await jwt.sign({ Identificador: Identificador, uid: Identificador }, await this.palabraSecretaParaTokens(), { expiresIn: '2h' });
788
- await ConexionSigu.query("INSERT INTO `SIGU`.`SIGU_Sesiones` VALUES (?, ?, ?, NOW(4), ?) ON DUPLICATE KEY UPDATE `Token` = ?, `LastUser` = ?", [Identificador, Solicitud.headers.host.trim(), Token, LastUser, Token, LastUser]);
789
- await ConexionSigu.query("DELETE FROM `SIGU`.`SIGU_SesionesFallidas` WHERE `Identificador` = ?", [Identificador]);
790
-
791
- // OBTENER IP DEL USUARIO
792
- const ipUsuario = (Solicitud.headers['x-forwarded-for'] || '').split(',').shift() || Solicitud.socket?.remoteAddress || Solicitud.connection?.remoteAddress || '-';
793
-
794
- // SI LA IP YA EXISTE CONTINUA
795
- await ConexionSigu.query("\
796
- INSERT INTO `SIGU`.`SIGU_DireccionesUsadasPorLosUsuarios` \
797
- (`DireccionUsadaPorElUsuario`, `Identificador`, `LastUpdate`, `LastUser`) \
798
- VALUES (?, ?, NOW(4), ?) \
799
- ON DUPLICATE KEY UPDATE `LastUpdate` = NOW(4), `LastUser` = ?;", [
800
- ipUsuario,
801
- Identificador,
802
- LastUser,
803
- LastUser
804
- ]);
805
-
806
- return { Token, Dominio: ((process.env.ENV || 'local') === 'production' ? '.sigu.utn.ac.cr' : '.181.193.85.44.nip.io') };
807
-
808
- } catch (error) {
809
- console.error("Error en AutenticarConGoogle:", error);
810
- return { error: "Error al autenticar con Google" };
811
- } finally {
812
- if (ConexionSigu) await ConexionSigu.end();
813
- }
814
- }
815
-
816
464
  async Autenticar(Solicitud) {
817
465
  const crypto = require('crypto');
818
466
  const bcrypt = require('bcryptjs');
@@ -1048,7 +696,7 @@ class Miscelaneo {
1048
696
  `a`.`Tipo`, IF(`a`.`Icono` <> '', CONCAT('https://storage.sigu.utn.ac.cr/images/cards/', `a`.`Icono`), '') AS `Icono`, `a`.`Color`, `a`.`Correo`,\
1049
697
  `a`.`Version`, `a`.`FechaDePublicacion`, `a`.`AcuerdoDeNivelDeServicio`, `a`.`DiccionarioDeDatos`, `a`.`Repositorios`,\
1050
698
  `a`.`EnlaceDelVideo`,`a`.`EnlaceDelManual`\
1051
- , `a`.`Estado`, REGEXP_SUBSTR(SUBSTRING_INDEX(`a`.`Repositorios`, '/', -1), '[^,]*front[^,]*') AS `Frontend`\
699
+ , `a`.`Estado`, REGEXP_SUBSTR(`a`.`Repositorios`, '[^,]*front[^,]*') AS `Frontend`\
1052
700
  FROM `SIGU`.`SIGU_ModulosV2` `a`\
1053
701
  WHERE `a`.`Nombre` = ?", [this.NombreCanonicoDelModulo]);
1054
702
  return Modulos.map((linea) => {
@@ -1114,7 +762,7 @@ class Miscelaneo {
1114
762
  `a`.`Tipo`, IF(`a`.`Icono` <> '', CONCAT('https://storage.sigu.utn.ac.cr/images/cards/', `a`.`Icono`), '') AS `Icono`, `a`.`Color`, `a`.`Correo`,\
1115
763
  `a`.`Version`, `a`.`FechaDePublicacion`, `a`.`AcuerdoDeNivelDeServicio`, `a`.`DiccionarioDeDatos`, `a`.`Repositorios`,\
1116
764
  `a`.`EnlaceDelVideo`,`a`.`EnlaceDelManual`\
1117
- , `a`.`Estado`, REGEXP_SUBSTR(SUBSTRING_INDEX(`a`.`Repositorios`, '/', -1), '[^,]*front[^,]*') AS `Frontend`\
765
+ , `a`.`Estado`, REGEXP_SUBSTR(`a`.`Repositorios`, '[^,]*front[^,]*') AS `Frontend`\
1118
766
  FROM `SIGU`.`SIGU_ModulosV2` `a`\
1119
767
  JOIN `SIGU`.`SIGU_PermisosV2` `b` ON (`a`.`Nombre` = `b`.`Modulo`)\
1120
768
  WHERE `a`.`Estado` = 'Activo' AND `a`.`Padre` = ? AND `b`.`PermisoId` IN (?)\
@@ -1242,10 +890,10 @@ class Miscelaneo {
1242
890
  const Version = this.Version + "$$" + this.versionDelNucleo().split(' ')[0];
1243
891
  await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_ModulosV2` VALUES\
1244
892
  (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(4), ?, 'DiccionarioDeDatos'\
1245
- , ?, '-', '-', 'Activo', NOW(4), USER()) ON DUPLICATE KEY UPDATE `Version` = ?, `Repositorios` = ?"
893
+ , ?, '-', '-', 'Activo', NOW(4), USER()) ON DUPLICATE KEY UPDATE `Version` = ?"
1246
894
  , [this.NombreCanonicoDelModulo, this.MenuPadre, this.DescripcionDelModulo, this.DetalleDelModulo
1247
895
  , this.TipoDeCard, this.IconoDelModulo, this.ColorDelModulo, this.CorreoParaReportes, Version, this.versionDelNucleo().split(' ')[0], this.Repositorios
1248
- , Version, this.Repositorios]);
896
+ , Version]);
1249
897
  await ejecutarConsultaSIGU("SELECT IFNULL(MAX(`PermisoId`), 0) + 1 INTO @`SiguientePermisoId` FROM `SIGU`.`SIGU_PermisosV2`;\
1250
898
  INSERT INTO `SIGU`.`SIGU_PermisosV2` VALUES\
1251
899
  (@`SiguientePermisoId`, ?, ?, ?, NOW(4), USER()) ON DUPLICATE KEY UPDATE `LastUser` = USER(), `Nombre` = ?, `Descripcion` = ?;"
@@ -1544,7 +1192,7 @@ class Miscelaneo {
1544
1192
  }
1545
1193
 
1546
1194
  versionDelNucleo() {
1547
- return "VERSION_DEL_NUCLEO" + " " + this.NombreCanonicoDelModulo;
1195
+ return "2.0.82" + " " + this.NombreCanonicoDelModulo;
1548
1196
  }
1549
1197
 
1550
1198
  async destinatarioDeInformesDeError() {
@@ -1609,7 +1257,7 @@ class Miscelaneo {
1609
1257
  console.log(error);
1610
1258
  return;
1611
1259
  }
1612
- return await ejecutarConsultaSIGU("SELECT `a`.`Identificador`, `a`.`Identificacion`\
1260
+ return ejecutarConsultaSIGU("SELECT `a`.`Identificador`, `a`.`Identificacion`\
1613
1261
  , `a`.`Nombre`, `a`.`PrimerApellido`, `a`.`SegundoApellido`\
1614
1262
  , (SELECT GROUP_CONCAT(`b`.`CuentaIBAN`) FROM `SIGU`.`SIGU_CuentasBancariasPersonas` `b` WHERE `b`.`Estado` = TRUE AND `a`.`Identificador` = `b`.`Identificador`) AS `CuentasIBAN`\
1615
1263
  FROM `SIGU`.`SIGU_Personas` `a` WHERE `a`.`Identificador` = ?"
@@ -1630,33 +1278,28 @@ class Miscelaneo {
1630
1278
  (SELECT `Identificador` FROM `SIGU`.`SIGU_RolesPersonas` WHERE `PerfilGeneralId` = 1 /*El perfil 1 parece ser para Estudiantes*/)");
1631
1279
  }
1632
1280
 
1633
- async obtenerBeneficios() {
1634
- return await ejecutarConsultaSIGU("SELECT `BeneficioId`, `Beneficio`, `Estado` FROM `SIGU`.`SIGU_Beneficios`");
1635
- }
1636
-
1637
- async obtenerIdentificacion(Identificador) {
1638
- return await ejecutarConsultaSIGU("SELECT `Identificacion` FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` = ?", [Identificador]);
1281
+ obtenerBeneficios() {
1282
+ return ejecutarConsultaSIGU("SELECT `BeneficioId`, `Beneficio`, `Estado` FROM `SIGU`.`SIGU_Beneficios`");
1639
1283
  }
1640
1284
 
1641
- async obtenerPeriodos() {
1642
- return await ejecutarConsultaSIGU("SELECT `PeriodoId`, `CodigoPeriodo`, `Anio`, `Periodo`, `FechaInicio`, `FechaFinal`, `Tipo`, `Estado` FROM `SIGU`.`SIGU_Periodos` ORDER BY `Anio` DESC, `CodigoPeriodo` DESC");
1285
+ obtenerIdentificacion(Identificador) {
1286
+ return ejecutarConsultaSIGU("SELECT `Identificacion` FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` = ?", [Identificador]);
1643
1287
  }
1644
1288
 
1645
- async obtenerAnios() {
1646
- return await ejecutarConsultaSIGU("SELECT DISTINCT `Anio` FROM `SIGU`.`SIGU_Periodos`");
1289
+ obtenerPeriodos() {
1290
+ return ejecutarConsultaSIGU("SELECT `PeriodoId`, `CodigoPeriodo`, `Anio`, `Periodo`, `FechaInicio`, `FechaFinal`, `Tipo`, `Estado` FROM `SIGU`.`SIGU_Periodos` ORDER BY `Anio` DESC, `CodigoPeriodo` DESC");
1647
1291
  }
1648
1292
 
1649
- async obtenerSedes() {
1650
- return await ejecutarConsultaSIGU("SELECT `SedesId`, `CodigoAvatar`, `Descripcion`, `Siglas` FROM `SIGU`.`SIGU_Sedes` ORDER BY `Descripcion`");
1293
+ obtenerAnios() {
1294
+ return ejecutarConsultaSIGU("SELECT DISTINCT `Anio` FROM `SIGU`.`SIGU_Periodos`");
1651
1295
  }
1652
1296
 
1653
- async obtenerRecintos() {
1654
- return await ejecutarConsultaSIGU("SELECT `a`.`RecintoId`, CONCAT(`b`.`Descripcion`, '/', `a`.`Recinto`) AS `Recinto` FROM `SIGU`.`SIGU_Recintos` `a` LEFT JOIN `SIGU`.`SIGU_Sedes` `b` ON (`a`.`SedeId` = `b`.`SedesId`) ORDER BY `Recinto`");
1297
+ obtenerSedes() {
1298
+ return ejecutarConsultaSIGU("SELECT `SedesId`, `CodigoAvatar`, `Descripcion`, `Siglas` FROM `SIGU`.`SIGU_Sedes` ORDER BY `Descripcion`");
1655
1299
  }
1656
1300
 
1657
- async googleClientId() {
1658
- const clientId = await ejecutarConsultaSIGU("SELECT `Valor` FROM `SIGU`.`SIGU_VariablesDeSistema` WHERE `Nombre` = 'Google-ClientId'");
1659
- return clientId[0]?.['Valor'] || '';
1301
+ obtenerRecintos() {
1302
+ return ejecutarConsultaSIGU("SELECT `a`.`RecintoId`, CONCAT(`b`.`Descripcion`, '/', `a`.`Recinto`) AS `Recinto` FROM `SIGU`.`SIGU_Recintos` `a` LEFT JOIN `SIGU`.`SIGU_Sedes` `b` ON (`a`.`SedeId` = `b`.`SedesId`) ORDER BY `Recinto`");
1660
1303
  }
1661
1304
 
1662
1305
  async palabraSecretaParaTokens() {
@@ -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({ size: 'Letter', margin: this.config.margin, bufferPages: true });
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 - 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 });
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
- doc.moveDown(4);
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);
@@ -251,6 +383,66 @@ class ReportePDF {
251
383
  // doc.end();
252
384
  }
253
385
 
386
+ generarFoliado(doc, folioInicial) {
387
+ if (!doc || folioInicial === undefined) return;
388
+
389
+ const range = doc.bufferedPageRange();
390
+ let folioActual = parseInt(folioInicial);
391
+
392
+ for (let i = range.start; i < range.start + range.count; i++) {
393
+ doc.switchToPage(i);
394
+
395
+ const margin = this.config.margin;
396
+ const pageWidth = doc.page.width;
397
+ const yPos = 30; // Posición en la parte superior, fuera del área del encabezado principal
398
+
399
+ doc.save();
400
+ doc.fillColor("black")
401
+ .fontSize(this.config.fontSizeSubtitulo)
402
+ .font(this.config.fuenteNegrita);
403
+
404
+ doc.text(String(folioActual), pageWidth - margin - 100, yPos, {
405
+ width: 100,
406
+ align: 'right'
407
+ });
408
+
409
+ doc.restore();
410
+ folioActual++;
411
+ }
412
+ }
413
+
414
+ generarMarcaDeAgua(doc, texto, color = "#CCCCCC") {
415
+ if (!doc || !texto) return;
416
+
417
+ const range = doc.bufferedPageRange();
418
+ for (let i = range.start; i < range.start + range.count; i++) {
419
+ doc.switchToPage(i);
420
+
421
+ const pageWidth = doc.page.width;
422
+ const pageHeight = doc.page.height;
423
+
424
+ doc.save();
425
+
426
+ // Configuración de la marca de agua
427
+ doc.fillColor(color);
428
+ doc.fillOpacity(0.15); // Transparencia para que no tape el contenido
429
+ doc.fontSize(60);
430
+ doc.font(this.config.fuenteNegrita);
431
+
432
+ // Posicionamiento central con rotación
433
+ doc.translate(pageWidth / 2, pageHeight / 2);
434
+ doc.rotate(-45);
435
+
436
+ // Dibujar el texto centrado en el eje rotado
437
+ doc.text(texto, -pageWidth / 2, -30, {
438
+ width: pageWidth,
439
+ align: 'center'
440
+ });
441
+
442
+ doc.restore();
443
+ }
444
+ }
445
+
254
446
  generarTabla(doc, datos, metadatos) {
255
447
  if (!datos || datos.length === 0) return;
256
448
 
@@ -342,6 +534,7 @@ class ReportePDF {
342
534
  let isBold = false;
343
535
  let isItalic = false;
344
536
  let isUnderline = false;
537
+ let currentLink = null;
345
538
 
346
539
  const fontSize = opciones.fontSize || this.config.fontSizeSubtitulo;
347
540
  const align = opciones.align || 'justify';
@@ -358,6 +551,13 @@ class ReportePDF {
358
551
 
359
552
  doc.fillColor('black');
360
553
 
554
+ // Comprobamos si hay espacio suficiente en la página antes de empezar a dibujar
555
+ // Estimamos un alto de línea basado en el tamaño de fuente
556
+ const estimatedHeight = fontSize * 1.5;
557
+ if (doc.y + estimatedHeight > doc.page.height - doc.page.margins.bottom - 40) {
558
+ doc.addPage();
559
+ }
560
+
361
561
  // Como pdfkit podría venir de un texto dibujado con coordenadas X y Y absolutas (que rompe el flujo interno de texto),
362
562
  // definimos la posición X al margen izquierdo para iniciar el párrafo, manteniendo la coordenada Y actual.
363
563
  doc.x = margin;
@@ -385,10 +585,26 @@ class ReportePDF {
385
585
  textoParaImprimir += ' ';
386
586
  }
387
587
 
588
+ // Comprobar espacio por cada fragmento para prevenir que `continued` desborde mal
589
+ if (doc.y + fontSize > doc.page.height - doc.page.margins.bottom - 20) {
590
+ // En PDFKit, hacer addPage() mientras hay 'continued' true a veces falla, pero ayuda forzarlo antes.
591
+ doc.text(' ', { continued: false }); // forzar fin de linea
592
+ doc.addPage();
593
+ doc.x = margin; // reposicionar en nueva pagina
594
+ }
595
+
596
+ // Si hay un link activo, usamos color azul y subrayado por defecto si no se especificó lo contrario
597
+ if (currentLink) {
598
+ doc.fillColor('blue');
599
+ } else {
600
+ doc.fillColor('black');
601
+ }
602
+
388
603
  doc.text(textoParaImprimir, {
389
604
  width: availableWidth,
390
605
  continued: isContinued,
391
- underline: isUnderline,
606
+ underline: isUnderline || (currentLink ? true : false),
607
+ link: currentLink,
392
608
  align: align
393
609
  });
394
610
  };
@@ -425,6 +641,17 @@ class ReportePDF {
425
641
  isUnderline = false;
426
642
  continue;
427
643
  }
644
+ if (lowerToken.startsWith('<a')) {
645
+ const hrefMatch = token.match(/href=["']([^"']*)["']/i);
646
+ if (hrefMatch) {
647
+ currentLink = hrefMatch[1];
648
+ }
649
+ continue;
650
+ }
651
+ if (lowerToken === '</a>') {
652
+ currentLink = null;
653
+ continue;
654
+ }
428
655
  if (lowerToken === '<br>' || lowerToken === '<br/>' || lowerToken === '<br />') {
429
656
  // Si hay un salto de línea, rompemos el continued escribiendo un espacio final
430
657
  doc.text(' ', { continued: false });
@@ -451,6 +678,120 @@ class ReportePDF {
451
678
 
452
679
  doc.moveDown(1);
453
680
  }
681
+
682
+ generarFirmas(doc, firmas) {
683
+ if (!firmas || !Array.isArray(firmas) || firmas.length === 0) return;
684
+
685
+ const margin = this.config.margin;
686
+ const pageWidth = doc.page.width;
687
+ const availableWidth = pageWidth - 2 * margin;
688
+ const signatureHeight = 75;
689
+ const gap = 10;
690
+ const logoWidth = 35;
691
+ const padding = 6;
692
+
693
+ // Dividimos las firmas en grupos de 3 (máximo por fila)
694
+ for (let i = 0; i < firmas.length; i += 3) {
695
+ const filaDeFirmas = firmas.slice(i, i + 3);
696
+ const cantidadEnFila = filaDeFirmas.length;
697
+
698
+ // Ancho total por bloque de firma según la cantidad en esta fila
699
+ const blockWidth = availableWidth / cantidadEnFila;
700
+
701
+ // Verificar si hay espacio en la página actual para esta fila
702
+ if (doc.y + signatureHeight > doc.page.height - 80) {
703
+ doc.addPage();
704
+ }
705
+
706
+ const startY = doc.y + 10;
707
+
708
+ filaDeFirmas.forEach((firma, index) => {
709
+ const blockX = margin + (index * blockWidth);
710
+
711
+ let innerWidth = blockWidth - gap;
712
+ let innerX = blockX + (gap / 2);
713
+
714
+ // Ajustar márgenes para los extremos
715
+ if (index === 0) {
716
+ innerX = blockX;
717
+ innerWidth = blockWidth - (gap / 2);
718
+ }
719
+ if (index === cantidadEnFila - 1) {
720
+ innerWidth = blockWidth - (gap / 2);
721
+ }
722
+ if (cantidadEnFila === 1) {
723
+ innerWidth = 240;
724
+ innerX = margin + (availableWidth / 2) - (innerWidth / 2);
725
+ }
726
+
727
+ // Dibujar fondo y borde
728
+ doc.save();
729
+ doc.fillColor("#FCFCFC").strokeColor("#EEEEEE").lineWidth(1);
730
+ doc.rect(innerX, startY, innerWidth, signatureHeight).fillAndStroke();
731
+ doc.restore();
732
+
733
+ let currentX = innerX + padding;
734
+
735
+ // Dibujar logo
736
+ try {
737
+ const rutaLogo = path.join(__dirname, "./LogoUTN.png");
738
+ // Centrar verticalmente el logo dentro del recuadro
739
+ const logoY = startY + (signatureHeight / 2) - 13;
740
+ doc.image(rutaLogo, currentX, logoY, { width: logoWidth });
741
+
742
+ // Línea vertical separadora
743
+ doc.save();
744
+ doc.moveTo(currentX + logoWidth + padding, startY + padding)
745
+ .lineTo(currentX + logoWidth + padding, startY + signatureHeight - padding)
746
+ .strokeColor("#CCCCCC")
747
+ .lineWidth(0.5)
748
+ .stroke();
749
+ doc.restore();
750
+
751
+ currentX += logoWidth + (padding * 2);
752
+ } catch (e) {
753
+ // Si falla el logo, currentX se mantiene igual para el texto
754
+ }
755
+
756
+ // Área para el texto
757
+ const textWidth = innerWidth - (currentX - innerX) - padding;
758
+ let textY = startY + padding + 1;
759
+
760
+ doc.fillColor("black");
761
+
762
+ // 1. Título
763
+ doc.fontSize(6).font(this.config.fuenteNegrita);
764
+ doc.text("Firma electrónica por:", currentX, textY, { width: textWidth, align: 'left' });
765
+ textY = doc.y + 1;
766
+
767
+ // 2. Nombre
768
+ doc.fontSize(6).font(this.config.fuenteBase);
769
+ doc.text(firma.Nombre || 'N/A', currentX, textY, { width: textWidth, align: 'left' });
770
+ textY = doc.y + 1;
771
+
772
+ // 3. Cédula
773
+ doc.text(`Cédula: ${firma.Identificacion || 'N/A'}`, currentX, textY, { width: textWidth, align: 'left' });
774
+ textY = doc.y + 1;
775
+
776
+ // 4. Instancia
777
+ doc.text(`Instancia: ${firma.Instancia || 'N/A'}`, currentX, textY, { width: textWidth, align: 'left' });
778
+ textY = doc.y + 1;
779
+
780
+ // 5. Fecha
781
+ doc.text(`Fecha: ${firma.Fecha || 'N/A'}`, currentX, textY, { width: textWidth, align: 'left' });
782
+ textY = doc.y + 1;
783
+
784
+ // 6. Motivo
785
+ doc.text(`Motivo: ${firma.Motivo || 'N/A'}`, currentX, textY, { width: textWidth, align: 'left' });
786
+ });
787
+
788
+ // Mover el cursor para la siguiente fila
789
+ doc.y = startY + signatureHeight + 10;
790
+ }
791
+
792
+ // Agregar un poco de margen después de todas las firmas
793
+ doc.y += 10;
794
+ }
454
795
  }
455
796
 
456
797
  module.exports = new ReportePDF();
@@ -14,7 +14,7 @@
14
14
  }
15
15
  </div>
16
16
  </div>
17
- <div class="pie">
17
+ <!-- <div class="pie">
18
18
  <p>CONTINUAR</p>
19
- </div>
19
+ </div> -->
20
20
  </div>
@@ -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
  }