From 7d75fb1df72bcca70508806fab473895366d1d6c Mon Sep 17 00:00:00 2001 From: luis cespedes Date: Thu, 8 May 2025 16:27:11 -0400 Subject: [PATCH] alerta al enviar pdf --- angular.json | 8 +- package.json | 5 +- src/app/app.config.ts | 4 +- .../alert-dialog/alert-dialog.component.html | 28 +++ .../alert-dialog/alert-dialog.component.scss | 11 + .../alert-dialog/alert-dialog.component.ts | 75 ++++++ src/app/components/layout/layout.component.ts | 3 +- .../visor-pdf/visor-pdf.component.html | 30 ++- .../visor-pdf/visor-pdf.component.ts | 202 ++--------------- src/app/services/alert.service.ts | 128 +++++++++++ src/app/services/index.ts | 2 + src/app/services/pdf.service.ts | 214 ++++++++++++++++++ 12 files changed, 510 insertions(+), 200 deletions(-) create mode 100644 src/app/components/alert-dialog/alert-dialog.component.html create mode 100644 src/app/components/alert-dialog/alert-dialog.component.scss create mode 100644 src/app/components/alert-dialog/alert-dialog.component.ts create mode 100644 src/app/services/alert.service.ts create mode 100644 src/app/services/pdf.service.ts diff --git a/angular.json b/angular.json index 67a8205..bc6ae55 100644 --- a/angular.json +++ b/angular.json @@ -3,7 +3,7 @@ "version": 1, "newProjectRoot": "projects", "projects": { - "cronogramas-primeng": { + "cronogramas": { "projectType": "application", "schematics": { "@schematics/angular:component": { @@ -17,7 +17,7 @@ "build": { "builder": "@angular-devkit/build-angular:application", "options": { - "outputPath": "dist/cronogramas-primeng", + "outputPath": "dist/cronogramas", "index": "src/index.html", "browser": "src/main.ts", "polyfills": [ @@ -80,10 +80,10 @@ "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { - "buildTarget": "cronogramas-primeng:build:production" + "buildTarget": "cronogramas:build:production" }, "development": { - "buildTarget": "cronogramas-primeng:build:development" + "buildTarget": "cronogramas:build:development" } }, "defaultConfiguration": "development" diff --git a/package.json b/package.json index fe0bd42..cdf5c46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "cronogramas-primeng", - "version": "0.0.0", + "name": "cronogramas", + "version": "0.1.0", "scripts": { "ng": "ng", "start": "ng serve", @@ -37,6 +37,7 @@ "@angular/compiler-cli": "^19.2.0", "@types/file-saver": "^2.0.7", "@types/jasmine": "~5.1.0", + "@types/pdfmake": "^0.2.11", "@types/xlsx": "^0.0.35", "jasmine-core": "~5.6.0", "karma": "~6.4.0", diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 6d4c25f..39d7ce9 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -7,6 +7,7 @@ import { provideAnimations } from '@angular/platform-browser/animations'; import { providePrimeNG } from 'primeng/config'; import Aura from '@primeng/themes/aura'; import { authInterceptor } from './interceptors/auth.interceptor'; +import { MessageService } from 'primeng/api'; export const appConfig: ApplicationConfig = { providers: [ @@ -24,6 +25,7 @@ export const appConfig: ApplicationConfig = { darkModeSelector: false || 'none' } } - }) + }), + MessageService ] }; diff --git a/src/app/components/alert-dialog/alert-dialog.component.html b/src/app/components/alert-dialog/alert-dialog.component.html new file mode 100644 index 0000000..1198ea0 --- /dev/null +++ b/src/app/components/alert-dialog/alert-dialog.component.html @@ -0,0 +1,28 @@ + + +
+ +
{{ title }}
+
{{ message }}
+ +
+ + +
+
+
\ No newline at end of file diff --git a/src/app/components/alert-dialog/alert-dialog.component.scss b/src/app/components/alert-dialog/alert-dialog.component.scss new file mode 100644 index 0000000..912eda7 --- /dev/null +++ b/src/app/components/alert-dialog/alert-dialog.component.scss @@ -0,0 +1,11 @@ +:host ::ng-deep { + .alert-dialog { + .p-dialog-header { + padding-bottom: 0.5rem; + } + + .p-dialog-content { + overflow-y: visible; + } + } +} \ No newline at end of file diff --git a/src/app/components/alert-dialog/alert-dialog.component.ts b/src/app/components/alert-dialog/alert-dialog.component.ts new file mode 100644 index 0000000..9fd76e4 --- /dev/null +++ b/src/app/components/alert-dialog/alert-dialog.component.ts @@ -0,0 +1,75 @@ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DialogModule } from 'primeng/dialog'; +import { ButtonModule } from 'primeng/button'; +import { Subject } from 'rxjs'; + +export interface AlertDialogOptions { + visible: boolean; + title: string; + message: string; + icon: 'success' | 'error' | 'warning' | 'info' | 'question'; + showConfirmButton: boolean; + confirmButtonText: string; + showCancelButton: boolean; + cancelButtonText: string; +} + +@Component({ + selector: 'app-alert-dialog', + standalone: true, + imports: [CommonModule, DialogModule, ButtonModule], + templateUrl: './alert-dialog.component.html', + styleUrls: ['./alert-dialog.component.scss'] +}) +export class AlertDialogComponent { + visible = false; + title = ''; + message = ''; + icon: 'success' | 'error' | 'warning' | 'info' | 'question' = 'success'; + showConfirmButton = true; + confirmButtonText = 'Aceptar'; + showCancelButton = false; + cancelButtonText = 'Cancelar'; + + private confirmSubject = new Subject(); + public onConfirm = this.confirmSubject.asObservable(); + + show(options: Partial = {}): void { + this.visible = true; + this.title = options.title || this.title; + this.message = options.message || this.message; + this.icon = options.icon || this.icon; + this.showConfirmButton = options.showConfirmButton !== undefined ? options.showConfirmButton : this.showConfirmButton; + this.confirmButtonText = options.confirmButtonText || this.confirmButtonText; + this.showCancelButton = options.showCancelButton !== undefined ? options.showCancelButton : this.showCancelButton; + this.cancelButtonText = options.cancelButtonText || this.cancelButtonText; + } + + confirm(): void { + this.visible = false; + this.confirmSubject.next(true); + } + + cancel(): void { + this.visible = false; + this.confirmSubject.next(false); + } + + getIconClass(): string { + switch (this.icon) { + case 'success': + return 'pi-check-circle text-green-500'; + case 'error': + return 'pi-times-circle text-red-500'; + case 'warning': + return 'pi-exclamation-triangle text-yellow-500'; + case 'info': + return 'pi-info-circle text-blue-500'; + case 'question': + return 'pi-question-circle text-purple-500'; + default: + return 'pi-check-circle text-green-500'; + } + } +} \ No newline at end of file diff --git a/src/app/components/layout/layout.component.ts b/src/app/components/layout/layout.component.ts index 35bb0df..b38e6a7 100644 --- a/src/app/components/layout/layout.component.ts +++ b/src/app/components/layout/layout.component.ts @@ -21,8 +21,7 @@ import { MessageService } from 'primeng/api'; ], templateUrl: './layout.component.html', styleUrl: './layout.component.scss', - standalone: true, - providers : [MessageService] + standalone: true }) export class LayoutComponent implements OnInit { isSidebarVisible: boolean = true; diff --git a/src/app/components/visor-pdf/visor-pdf.component.html b/src/app/components/visor-pdf/visor-pdf.component.html index ebd17c8..c7508be 100644 --- a/src/app/components/visor-pdf/visor-pdf.component.html +++ b/src/app/components/visor-pdf/visor-pdf.component.html @@ -1,15 +1,21 @@
-
- -
-
- - + -
-
\ No newline at end of file + + + + Enviando... + + + + \ No newline at end of file diff --git a/src/app/components/visor-pdf/visor-pdf.component.ts b/src/app/components/visor-pdf/visor-pdf.component.ts index fab43a1..538f82b 100644 --- a/src/app/components/visor-pdf/visor-pdf.component.ts +++ b/src/app/components/visor-pdf/visor-pdf.component.ts @@ -2,21 +2,26 @@ import { Component, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DynamicDialogRef, DynamicDialogConfig } from 'primeng/dynamicdialog'; import { SafePipe } from '../../../pipes/safe.pipe'; - -import pdfMake from 'pdfmake/build/pdfmake'; -import pdfFonts from 'pdfmake/build/vfs_fonts'; -import { TDocumentDefinitions } from 'pdfmake/interfaces'; +import { ButtonModule } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; import { MessageService } from 'primeng/api'; -pdfMake.vfs = pdfFonts.vfs; +import { PdfService } from '../../services/pdf.service'; +import { AlertService } from '../../services/alert.service'; @Component({ selector: 'app-visor-pdf', standalone: true, - imports: [CommonModule, SafePipe], + imports: [ + CommonModule, + SafePipe, + ButtonModule, + ToastModule, + ProgressSpinnerModule + ], templateUrl: './visor-pdf.component.html', - styleUrls: ['./visor-pdf.component.scss'], - providers: [MessageService] + styleUrls: ['./visor-pdf.component.scss'] }) export class VisorPdfComponent implements OnInit { product: any; @@ -27,165 +32,8 @@ export class VisorPdfComponent implements OnInit { private dialogRef = inject(DynamicDialogRef); private config = inject(DynamicDialogConfig); private messageService = inject(MessageService); - - docDefinition: TDocumentDefinitions = { - content: [ - // Encabezado con información de empresa y cronograma - { - columns: [ - { - width: '50%', - text: 'Empresa: Constructora Los Andes S.A.', - margin: [0, 0, 0, 5] - }, - { - width: '50%', - text: [ - { text: 'Cronograma: SC-23-45\n' }, - { text: 'Nombre sistema: Terminal Portuario Norte' } - ], - alignment: 'right', - margin: [0, 0, 0, 5] - } - ] - }, - - // Título y subtítulo - { - text: 'CRONOGRAMA BASE DE OBRAS E INVERSIONES', - alignment: 'center', - bold: true, - fontSize: 12, - margin: [0, 10, 0, 0] - }, - { - text: 'ACTUALIZACIÓN PLAN DE DESARROLLO O NUEVA CONCESIÓN', - alignment: 'center', - bold: true, - fontSize: 11, - margin: [0, 0, 0, 10] - }, - - // Tabla principal - { - table: { - headerRows: 1, - widths: ['10%', '12%', '23%', '12%', '10%', '10%', '10%', '13%'], - body: [ - [ - { text: 'ETAPA', style: 'tableHeader', alignment: 'center' }, - { text: 'CÓDIGO GLOSA', style: 'tableHeader', alignment: 'center' }, - { text: 'DESCRIPCIÓN GLOSA', style: 'tableHeader', alignment: 'center' }, - { text: 'MONTO INVERSIÓN UF', style: 'tableHeader', alignment: 'center' }, - { text: 'AÑO INICIO', style: 'tableHeader', alignment: 'center' }, - { text: 'AÑO TERMINO', style: 'tableHeader', alignment: 'center' }, - { text: 'MES TERMINO', style: 'tableHeader', alignment: 'center' }, - { text: 'NOTA', style: 'tableHeader', alignment: 'center' } - ], - [ - { text: '1', alignment: 'center' }, - { text: 'INF-001', alignment: 'center' }, - { text: 'Estudios preliminares', alignment: 'left' }, - { text: '2,500', alignment: 'right' }, - { text: '2025', alignment: 'center' }, - { text: '2025', alignment: 'center' }, - { text: 'Julio', alignment: 'center' }, - { text: 'Fase inicial', alignment: 'left' } - ], - [ - { text: '2', alignment: 'center' }, - { text: 'MOV-102', alignment: 'center' }, - { text: 'Movimiento de tierras', alignment: 'left' }, - { text: '15,750', alignment: 'right' }, - { text: '2025', alignment: 'center' }, - { text: '2026', alignment: 'center' }, - { text: 'Enero', alignment: 'center' }, - { text: 'En terreno', alignment: 'left' } - ], - [ - { text: '3', alignment: 'center' }, - { text: 'CIM-203', alignment: 'center' }, - { text: 'Construcción de cimientos', alignment: 'left' }, - { text: '32,800', alignment: 'right' }, - { text: '2026', alignment: 'center' }, - { text: '2026', alignment: 'center' }, - { text: 'Mayo', alignment: 'center' }, - { text: 'Crítico', alignment: 'left' } - ], - [ - { text: '4', alignment: 'center' }, - { text: 'EST-304', alignment: 'center' }, - { text: 'Estructura principal', alignment: 'left' }, - { text: '65,420', alignment: 'right' }, - { text: '2026', alignment: 'center' }, - { text: '2027', alignment: 'center' }, - { text: 'Febrero', alignment: 'center' }, - { text: 'Alta inversión', alignment: 'left' } - ], - [ - { text: '5', alignment: 'center' }, - { text: 'TER-405', alignment: 'center' }, - { text: 'Terminaciones y acabados', alignment: 'left' }, - { text: '18,300', alignment: 'right' }, - { text: '2027', alignment: 'center' }, - { text: '2027', alignment: 'center' }, - { text: 'Octubre', alignment: 'center' }, - { text: 'Etapa final', alignment: 'left' } - ] - ] - } - }, - - // Total - { - columns: [ - { - width: '80%', - text: 'TOTAL:', - alignment: 'right', - bold: true, - margin: [0, 15, 10, 20] - }, - { - width: '20%', - text: '134,770 UF', - alignment: 'left', - bold: true, - margin: [0, 15, 0, 20] - } - ] - }, - - // Firma - { - text: 'X', - alignment: 'center', - bold: true, - fontSize: 14, - margin: [0, 15, 0, 0] - }, - { - text: 'Firma SSS', - alignment: 'center', - fontSize: 10, - margin: [0, 0, 0, 10] - } - ], - - // Estilos - styles: { - tableHeader: { - bold: true, - fontSize: 10, - fillColor: '#f2f2f2' - } - }, - - // Definiciones de página - pageSize: 'A4', - pageOrientation: 'portrait', - pageMargins: [40, 40, 40, 40] - }; + private pdfService = inject(PdfService); + private alertService = inject(AlertService); ngOnInit() { // Obtener el producto pasado a través del servicio de diálogo @@ -196,33 +44,29 @@ export class VisorPdfComponent implements OnInit { } generarPDF() { - const pdfDocGenerator = pdfMake.createPdf(this.docDefinition); - pdfDocGenerator.getDataUrl((dataUrl) => { + this.pdfService.generateCronogramaPdf(this.product).then(dataUrl => { this.pdfSrc = dataUrl; }); } descargarPDF() { - pdfMake.createPdf(this.docDefinition).download('cronogramaspdf'); + this.pdfService.downloadCronogramaPdf('cronograma', this.product); } + enviarPDF() { this.enviando = true; console.log('Enviando PDF...'); - + setTimeout(() => { this.enviando = false; console.log('Mostrando mensaje de envio...') - - this.messageService.add({ - severity: 'success', - summary: '¡Enviado con éxito!', - detail: 'El cronograma ha sido enviado correctamente', - life: 30000, - }); + this.alertService.success( + 'El cronograma ha sido enviado correctamente a la plataforma.', + '¡Enviado con éxito!' + ); }, 2000); } - cerrar() { this.dialogRef.close(); } diff --git a/src/app/services/alert.service.ts b/src/app/services/alert.service.ts new file mode 100644 index 0000000..da895db --- /dev/null +++ b/src/app/services/alert.service.ts @@ -0,0 +1,128 @@ +import { Injectable, ComponentRef, createComponent, EnvironmentInjector, ApplicationRef } from '@angular/core'; +import { AlertDialogComponent, AlertDialogOptions } from '../components/alert-dialog/alert-dialog.component'; + +@Injectable({ + providedIn: 'root' +}) +export class AlertService { + private alertRef: ComponentRef | null = null; + + constructor( + private injector: EnvironmentInjector, + private appRef: ApplicationRef + ) {} + + /** + * Muestra un mensaje de alerta tipo Sweet Alert + * @param options Opciones de configuración del alert + * @returns Una promesa que se resuelve con true (confirmar) o false (cancelar) + */ + show(options: Partial = {}): Promise { + return new Promise((resolve) => { + // Si ya existe un alert, lo eliminamos + this.closeCurrentAlert(); + + // Creamos el componente dinámicamente + this.alertRef = createComponent(AlertDialogComponent, { + environmentInjector: this.injector, + }); + + // Agregamos a la aplicación y al DOM + document.body.appendChild(this.alertRef.location.nativeElement); + this.appRef.attachView(this.alertRef.hostView); + + // Configuramos las opciones y mostramos + this.alertRef.instance.show(options); + + // Manejamos la respuesta + const subscription = this.alertRef.instance.onConfirm.subscribe((result) => { + subscription.unsubscribe(); + this.closeCurrentAlert(); + resolve(result); + }); + }); + } + + /** + * Muestra un mensaje de éxito + * @param message Mensaje a mostrar + * @param title Título del alert (opcional) + */ + success(message: string, title: string = '¡Operación exitosa!'): Promise { + return this.show({ + title, + message, + icon: 'success', + showCancelButton: false + }); + } + + /** + * Muestra un mensaje de error + * @param message Mensaje a mostrar + * @param title Título del alert (opcional) + */ + error(message: string, title: string = 'Error'): Promise { + return this.show({ + title, + message, + icon: 'error', + showCancelButton: false + }); + } + + /** + * Muestra un mensaje de advertencia + * @param message Mensaje a mostrar + * @param title Título del alert (opcional) + */ + warning(message: string, title: string = 'Advertencia'): Promise { + return this.show({ + title, + message, + icon: 'warning', + showCancelButton: false + }); + } + + /** + * Muestra un mensaje informativo + * @param message Mensaje a mostrar + * @param title Título del alert (opcional) + */ + info(message: string, title: string = 'Información'): Promise { + return this.show({ + title, + message, + icon: 'info', + showCancelButton: false + }); + } + + /** + * Muestra un diálogo de confirmación + * @param message Mensaje a mostrar + * @param title Título del alert (opcional) + */ + confirm(message: string, title: string = '¿Está seguro?'): Promise { + return this.show({ + title, + message, + icon: 'question', + showCancelButton: true, + confirmButtonText: 'Sí, confirmar', + cancelButtonText: 'Cancelar' + }); + } + + /** + * Cierra el alert actual si existe + */ + private closeCurrentAlert(): void { + if (this.alertRef) { + this.appRef.detachView(this.alertRef.hostView); + this.alertRef.location.nativeElement.remove(); + this.alertRef = null; + } + } +} \ No newline at end of file diff --git a/src/app/services/index.ts b/src/app/services/index.ts index c26dd2a..dd35e28 100644 --- a/src/app/services/index.ts +++ b/src/app/services/index.ts @@ -5,3 +5,5 @@ export * from './ajuste-pd.service'; export * from './unidad-informacion.service'; export * from './tipo-carga.service'; export * from './estado-aprobacion.service'; +export * from './pdf.service'; +export * from './alert.service'; diff --git a/src/app/services/pdf.service.ts b/src/app/services/pdf.service.ts new file mode 100644 index 0000000..f0f8116 --- /dev/null +++ b/src/app/services/pdf.service.ts @@ -0,0 +1,214 @@ +import { Injectable } from '@angular/core'; +import pdfMake from 'pdfmake/build/pdfmake'; +import pdfFonts from 'pdfmake/build/vfs_fonts'; +import { TDocumentDefinitions } from 'pdfmake/interfaces'; + +pdfMake.vfs = pdfFonts.vfs; + +@Injectable({ + providedIn: 'root' +}) +export class PdfService { + + constructor() { } + + /** + * Genera un PDF con la definición por defecto para cronogramas + * @param data Datos opcionales para personalizar el documento + * @returns Promise con la URL de datos del PDF generado + */ + generateCronogramaPdf(data?: any): Promise { + const docDefinition = this.getCronogramaDocDefinition(data); + return this.generatePdfDataUrl(docDefinition); + } + + /** + * Descarga un PDF con la definición de cronograma + * @param filename Nombre del archivo a descargar + * @param data Datos opcionales para personalizar el documento + */ + downloadCronogramaPdf(filename: string = 'cronograma', data?: any): void { + const docDefinition = this.getCronogramaDocDefinition(data); + pdfMake.createPdf(docDefinition).download(filename); + } + + /** + * Genera una URL de datos a partir de la definición del documento + * @param docDefinition Definición del documento PDF + * @returns Promise con la URL de datos del PDF + */ + private generatePdfDataUrl(docDefinition: TDocumentDefinitions): Promise { + return new Promise((resolve) => { + const pdfDocGenerator = pdfMake.createPdf(docDefinition); + pdfDocGenerator.getDataUrl((dataUrl) => { + resolve(dataUrl); + }); + }); + } + + /** + * Obtiene la definición del documento para un cronograma + * @param data Datos opcionales para personalizar el documento + * @returns Definición del documento PDF + */ + private getCronogramaDocDefinition(data?: any): TDocumentDefinitions { + return { + content: [ + // Encabezado con información de empresa y cronograma + { + columns: [ + { + width: '50%', + text: 'Empresa: Constructora Los Andes S.A.', + margin: [0, 0, 0, 5] + }, + { + width: '50%', + text: [ + { text: 'Cronograma: SC-23-45\n' }, + { text: 'Nombre sistema: Terminal Portuario Norte' } + ], + alignment: 'right', + margin: [0, 0, 0, 5] + } + ] + }, + + // Título y subtítulo + { + text: 'CRONOGRAMA BASE DE OBRAS E INVERSIONES', + alignment: 'center', + bold: true, + fontSize: 12, + margin: [0, 10, 0, 0] + }, + { + text: 'ACTUALIZACIÓN PLAN DE DESARROLLO O NUEVA CONCESIÓN', + alignment: 'center', + bold: true, + fontSize: 11, + margin: [0, 0, 0, 10] + }, + + // Tabla principal + { + table: { + headerRows: 1, + widths: ['10%', '12%', '23%', '12%', '10%', '10%', '10%', '13%'], + body: [ + [ + { text: 'ETAPA', style: 'tableHeader', alignment: 'center' }, + { text: 'CÓDIGO GLOSA', style: 'tableHeader', alignment: 'center' }, + { text: 'DESCRIPCIÓN GLOSA', style: 'tableHeader', alignment: 'center' }, + { text: 'MONTO INVERSIÓN UF', style: 'tableHeader', alignment: 'center' }, + { text: 'AÑO INICIO', style: 'tableHeader', alignment: 'center' }, + { text: 'AÑO TERMINO', style: 'tableHeader', alignment: 'center' }, + { text: 'MES TERMINO', style: 'tableHeader', alignment: 'center' }, + { text: 'NOTA', style: 'tableHeader', alignment: 'center' } + ], + [ + { text: '1', alignment: 'center' }, + { text: 'INF-001', alignment: 'center' }, + { text: 'Estudios preliminares', alignment: 'left' }, + { text: '2,500', alignment: 'right' }, + { text: '2025', alignment: 'center' }, + { text: '2025', alignment: 'center' }, + { text: 'Julio', alignment: 'center' }, + { text: 'Fase inicial', alignment: 'left' } + ], + [ + { text: '2', alignment: 'center' }, + { text: 'MOV-102', alignment: 'center' }, + { text: 'Movimiento de tierras', alignment: 'left' }, + { text: '15,750', alignment: 'right' }, + { text: '2025', alignment: 'center' }, + { text: '2026', alignment: 'center' }, + { text: 'Enero', alignment: 'center' }, + { text: 'En terreno', alignment: 'left' } + ], + [ + { text: '3', alignment: 'center' }, + { text: 'CIM-203', alignment: 'center' }, + { text: 'Construcción de cimientos', alignment: 'left' }, + { text: '32,800', alignment: 'right' }, + { text: '2026', alignment: 'center' }, + { text: '2026', alignment: 'center' }, + { text: 'Mayo', alignment: 'center' }, + { text: 'Crítico', alignment: 'left' } + ], + [ + { text: '4', alignment: 'center' }, + { text: 'EST-304', alignment: 'center' }, + { text: 'Estructura principal', alignment: 'left' }, + { text: '65,420', alignment: 'right' }, + { text: '2026', alignment: 'center' }, + { text: '2027', alignment: 'center' }, + { text: 'Febrero', alignment: 'center' }, + { text: 'Alta inversión', alignment: 'left' } + ], + [ + { text: '5', alignment: 'center' }, + { text: 'TER-405', alignment: 'center' }, + { text: 'Terminaciones y acabados', alignment: 'left' }, + { text: '18,300', alignment: 'right' }, + { text: '2027', alignment: 'center' }, + { text: '2027', alignment: 'center' }, + { text: 'Octubre', alignment: 'center' }, + { text: 'Etapa final', alignment: 'left' } + ] + ] + } + }, + + // Total + { + columns: [ + { + width: '80%', + text: 'TOTAL:', + alignment: 'right', + bold: true, + margin: [0, 15, 10, 20] + }, + { + width: '20%', + text: '134,770 UF', + alignment: 'left', + bold: true, + margin: [0, 15, 0, 20] + } + ] + }, + + // Firma + { + text: 'X', + alignment: 'center', + bold: true, + fontSize: 14, + margin: [0, 15, 0, 0] + }, + { + text: 'Firma SSS', + alignment: 'center', + fontSize: 10, + margin: [0, 0, 0, 10] + } + ], + + // Estilos + styles: { + tableHeader: { + bold: true, + fontSize: 10, + fillColor: '#f2f2f2' + } + }, + + // Definiciones de página + pageSize: 'A4', + pageOrientation: 'portrait', + pageMargins: [40, 40, 40, 40] + }; + } +} \ No newline at end of file