From f24de4803f3f386a1e8da948a4e0e97221ab4d61 Mon Sep 17 00:00:00 2001 From: Kallesh B S Date: Thu, 7 Aug 2025 15:15:19 +0530 Subject: [PATCH] pdf generation --- .gitignore | 3 + package-lock.json | 220 ++++++- package.json | 2 + .../carnet-application.controller.ts | 33 +- .../carnet-application.service.ts | 69 ++- src/utils/helper.ts | 585 +++++++++++++++++- 6 files changed, 898 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 4b56acf..0bf9940 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ /node_modules /build +# pdf +/public/carnet-pdf/*.pdf + # Logs logs *.log diff --git a/package-lock.json b/package-lock.json index 02e819b..0b5d96e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,8 @@ "oracledb": "^6.7.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "pdf-merger-js": "^5.1.2", + "pdfkit": "^0.17.1", "pg": "^8.13.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -1145,9 +1147,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2892,6 +2894,24 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3283,6 +3303,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@swc/types": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", @@ -4944,6 +4973,15 @@ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", "license": "MIT" }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -5682,6 +5720,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -5886,6 +5930,12 @@ "wrappy": "1" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -6572,7 +6622,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -6935,6 +6984,32 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/fontkit/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -7045,14 +7120,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -8723,6 +8799,12 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "license": "MIT" + }, "node_modules/js-md4": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", @@ -8936,6 +9018,25 @@ "integrity": "sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==", "license": "MIT" }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9844,6 +9945,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10008,6 +10115,62 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/pdf-merger-js": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/pdf-merger-js/-/pdf-merger-js-5.1.2.tgz", + "integrity": "sha512-RCBjLQILZ8UA4keO/Ip2/gjUuxigMMoK7mO5eJg6zjlnyymboFnRTgzKwOs/FiU9ornS2m72Qr95oARX1C24fw==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "pdf-lib": "^1.17.1" + }, + "bin": { + "pdf-merge": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/pdf-merger-js/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pdfkit": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.1.tgz", + "integrity": "sha512-Kkf1I9no14O/uo593DYph5u3QwiMfby7JsBSErN1WqeyTgCBNJE3K4pXBn3TgkdKUIVu+buSl4bYUNC+8Up4xg==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, "node_modules/peek-readable": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz", @@ -10237,6 +10400,11 @@ "node": ">=4" } }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -10698,6 +10866,12 @@ "dev": true, "license": "ISC" }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -11931,6 +12105,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -12541,6 +12721,32 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/package.json b/package.json index 8a5e4d7..3b833a3 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ "oracledb": "^6.7.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "pdf-merger-js": "^5.1.2", + "pdfkit": "^0.17.1", "pg": "^8.13.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/src/oracle/carnet-application/carnet-application.controller.ts b/src/oracle/carnet-application/carnet-application.controller.ts index 6ad999e..e162398 100644 --- a/src/oracle/carnet-application/carnet-application.controller.ts +++ b/src/oracle/carnet-application/carnet-application.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Put, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Res, StreamableFile, UseGuards } from '@nestjs/common'; import { CarnetApplicationService } from './carnet-application.service'; import { ApiTags } from '@nestjs/swagger'; @@ -14,6 +14,10 @@ import { import { Roles } from 'src/decorators/roles.decorator'; import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; import { RolesGuard } from 'src/guards/roles.guard'; +import { Response } from 'express'; +import { join } from 'path'; +import { createReadStream } from 'fs'; +import { deleteFilesAsync } from 'src/utils/helper'; @ApiTags('Carnet Application - Oracle') @@ -152,8 +156,31 @@ export class CarnetApplicationController { // [PRINT_PKG] @Post('PrintCarnet') - PrintCarnet(@Body() body: PrintCarnetDTO) { - return this.carnetApplicationService.PrintCarnet(body); + @Roles('ca', 'sa', 'ua') + async PrintCarnet(@Body() body: PrintCarnetDTO, @Res({ passthrough: true }) res: Response) { + const fileName = 'carnet.pdf'; + const filePath = join(process.cwd(), 'public/carnet-pdf', fileName); + const filesArray = ['p2.pdf', fileName]; + + try { + await this.carnetApplicationService.PrintCarnet(body); + + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${fileName}"`, + }); + + const stream = createReadStream(filePath); + + // Clean up files after response finishes streaming + res.on('finish', async () => { + await deleteFilesAsync(filesArray); + }); + + return new StreamableFile(stream); + } catch (error) { + return error.message; + } } } diff --git a/src/oracle/carnet-application/carnet-application.service.ts b/src/oracle/carnet-application/carnet-application.service.ts index 3fd24e8..c67ade0 100644 --- a/src/oracle/carnet-application/carnet-application.service.ts +++ b/src/oracle/carnet-application/carnet-application.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { OracleDBService } from 'src/db/db.service'; import * as oracledb from 'oracledb' import { InternalServerException } from 'src/exceptions/internalServerError.exception'; -import { closeOracleDbConnection, fetchCursor, handleError, setEmptyStringsToNull } from 'src/utils/helper'; +import { closeOracleDbConnection, deleteFilesAsync, fetchCursor, generateFinalPDF, generateGeneralistPDF1, generateGeneralistPDF2, handleError, pdfCarnetData, pdfGLData, setEmptyStringsToNull } from 'src/utils/helper'; import { CarnetProcessingCenterDTO, SaveCarnetApplicationDTO, TransmitApplicationtoProcessDTO, @@ -1114,7 +1114,8 @@ export class CarnetApplicationService { } // [PRINT_PKG] - async PrintCarnet(body: PrintCarnetDTO) { + + async getPrintingData(body: PrintCarnetDTO) { let connection; try { connection = await this.oracleDBService.getConnection(); @@ -1147,12 +1148,17 @@ export class CarnetApplicationService { const fres: any = await fetchCursor(outBinds.P_CARNETDATACURSOR, CarnetApplicationService.name); const fres1: any = await fetchCursor(outBinds.P_GLDATACURSOR, CarnetApplicationService.name); + if (fres.length === 0 || fres1.length === 0) { + throw new BadRequestException("Print failed please check the submitted details"); + } + if (fres.length > 0 && fres[0].ERRORMESG) { this.logger.warn(fres[0].ERRORMESG); throw new BadRequestException(fres[0].ERRORMESG) } - return { P_CARNETDATACURSOR: fres, P_GLDATACURSOR: fres1 }; + return { fres: fres, fres1: fres1 } + } catch (error) { handleError(error, CarnetApplicationService.name) } finally { @@ -1160,4 +1166,61 @@ export class CarnetApplicationService { } } + async PrintCarnet(body: PrintCarnetDTO) { + + + try { + + const fres: any = await this.getPrintingData(body); + const fres1: any = await this.getPrintingData(body); + + if (fres.length === 0 || fres1.length === 0) { + throw new BadRequestException("Print failed please check the submitted details"); + } + + if (fres.length > 0 && fres[0].ERRORMESG) { + this.logger.warn(fres[0].ERRORMESG); + throw new BadRequestException(fres[0].ERRORMESG) + } + + try { + const glData: pdfGLData[] = fres1.map(x => { + return { + itemNo: x.ITEMNO, + description: x.ITEMDESCRIPTION, + noOfPieces: x.NOOFPIECES, + weight: x.WEIGHT, + itemValue: x.ITEMVALUE, + countryOfOrigin: x.GOODSORIGINCOUNTRY + }; + }); + + const carnetData: pdfCarnetData = { + carnetNo: fres[0].CARNETNO, + contPageNo: 0 + } + + let p2Status: string = ""; + + if (body.P_PRINTGL === 'Y') { + p2Status = await generateGeneralistPDF1(glData, carnetData); + } else { + p2Status = await generateGeneralistPDF2(carnetData.carnetNo); + } + + if (p2Status === "p2 done") { + await generateFinalPDF(['p2.pdf'], 'carnet.pdf'); + } + + } catch (error) { + throw new InternalServerException() + } + + // return { P_CARNETDATACURSOR: fres, P_GLDATACURSOR: fres1 }; + + } catch (error) { + handleError(error, CarnetApplicationService.name) + } + } + } diff --git a/src/utils/helper.ts b/src/utils/helper.ts index ce1d39a..f7f5012 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -5,10 +5,28 @@ import { } from '@nestjs/common'; import * as oracledb from 'oracledb'; +const PDFDocument = require('pdfkit'); +const fs = require('fs'); +const path = require('path'); +import { PDFDocument as pdfl } from 'pdf-lib'; +import PDFMerger from 'pdf-merger-js'; +import * as fsPromises from 'fs/promises'; +import { BadRequestException as BR } from 'src/exceptions/badRequest.exception'; + +const doc = new PDFDocument({ + size: 'A4', + margins: { + top: 50, + bottom: 50, + left: 50, + right: 50 + } +}); + const logger = new Logger('Helper'); export const handleError = (error: any, context: string = 'UnknownService'): never => { - if (error instanceof BadRequestException) { + if (error instanceof BadRequestException || error instanceof BR) { logger.warn(`[${context}] ${error.message}`); throw error; } @@ -57,3 +75,568 @@ export const setEmptyStringsToNull = (obj: any): void => { } }); } + +export interface pdfGLData { + itemNo: number; + description: string; + noOfPieces: number; + weight: string; + itemValue: number; + countryOfOrigin: string +} + +export interface pdfCarnetData { + carnetNo: string; + contPageNo: number; +} + +function formatNumber(num) { + if (isNaN(num)) return 'N/A'; + return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +export const generateGeneralistPDF1 = async (jsonData: pdfGLData[], headerData: pdfCarnetData) => { + // --- Table Configuration --- + const table = { + x: doc.x, + y: doc.y + 20, // Start a bit below the current cursor + cellTopPadding: 5, + cellBottomPadding: 2, + cellHorizontalPadding: 5, + cellSpacing: 0, + headerColor: '#FFFFFF', + rowColor: '#FFFFFF', + alternatingRowColor: '#FFFFFF', + lineColor: '#000000', + headerFontColor: '#000000', + bodyFontColor: '#000000', + fontSize: 10, + headerFontSize: 10, + lineWidth: 0.5, + widths: [50, 120, 70, 80, 70, 100], // + totalRowPaddingTop: 10 // New padding for the total row + }; + + // Define the maximum number of lines for the entire page + const maxLinesPerPage = 37; + const fixedHeaderRowHeight = 22; // You can tweak this value + const fixedHeaderFontSize = 10; // Same font size for all rows + + + // Fixed header rows, which are not part of the dynamic data + function getFixedHeaderData(headerValues) { + return [ + { + isMergedRow: true, + cols: [ + { text: 'General List', startCol: 0, endCol: 5, } + ] + }, + { + isMergedRow: true, + cols: [ + { text: '', startCol: 0, endCol: 2 }, + { text: 'Carnet No:', startCol: 3, endCol: 4 }, + { text: headerValues.carnetNo, startCol: 5, endCol: 5 }, + ] + }, + { + isMergedRow: true, + cols: [ + { text: '', startCol: 0, endCol: 2 }, + { text: 'Contn.Sheet No:', startCol: 3, endCol: 4 }, + { text: headerValues.contPageNo, startCol: 5, endCol: 5 }, + ] + }, + { // This is the new blank row + isMergedRow: true, + cols: [ + { text: '', startCol: 0, endCol: 5 } + ] + }, + ['Item No', 'Description', 'No of Pieces', 'Weight', 'Item Value', 'Country Of Origin'] + ]; + } + + + // Helper function to draw a single horizontal line + function drawHorizontalLine(doc, x, y, width, lineColor, lineWidth) { + doc.strokeColor(lineColor) + .lineWidth(lineWidth) + .moveTo(x, y) + .lineTo(x + width, y) + .stroke(); + } + + // Helper function to draw vertical lines between columns for a specific height range + function drawVerticalLines(doc, table, startY, endY) { + doc.strokeColor(table.lineColor) + .lineWidth(table.lineWidth); + + let verticalX = table.x; + for (let i = 0; i < table.widths.length; i++) { + doc.moveTo(verticalX, startY) + .lineTo(verticalX, endY) + .stroke(); + verticalX += table.widths[i]; + } + // Draw the final right vertical line + doc.moveTo(verticalX, startY) + .lineTo(verticalX, endY) + .stroke(); + } + + // Helper function to draw the table header + function drawHeader(doc, table, headerData, cursorY, initialX, totalTableWidth) { + const totalVerticalPadding = table.cellTopPadding + table.cellBottomPadding; + const totalHorizontalPadding = table.cellHorizontalPadding * 2; + + let headerHeight = 0; + let maxTextHeight = 0; + doc.fontSize(table.headerFontSize).font('Helvetica'); + + for (let i = 0; i < headerData.length; i++) { + const textOptions = { + width: table.widths[i] - totalHorizontalPadding, + lineGap: 3 + }; + const h = doc.heightOfString(headerData[i], textOptions); + if (h > maxTextHeight) maxTextHeight = h; + } + + headerHeight = maxTextHeight + totalVerticalPadding; + const headerLines = Math.ceil(maxTextHeight / doc.heightOfString('a', { lineGap: 3 })); + + let currentX = initialX; + for (let i = 0; i < headerData.length; i++) { + doc.rect(currentX, cursorY, table.widths[i], headerHeight).fill(table.headerColor); + currentX += table.widths[i]; + } + + currentX = initialX; + for (let i = 0; i < headerData.length; i++) { + doc.fillColor(table.headerFontColor); + doc.text(headerData[i], currentX + table.cellHorizontalPadding, cursorY + table.cellTopPadding, { + width: table.widths[i] - totalHorizontalPadding, + lineGap: 3 + }); + currentX += table.widths[i]; + } + + return { height: headerHeight, lines: headerLines }; + } + + // Helper function to draw a merged row + function drawMergedRow(doc, table, rowData, cursorY, initialX) { + const totalHorizontalPadding = table.cellHorizontalPadding * 2; + + const rowHeight = fixedHeaderRowHeight; // Use fixed height + const fontSize = fixedHeaderFontSize; + + let currentX = initialX; + + doc.fontSize(fontSize).font('Helvetica'); // Ensure consistent font + + for (let i = 0; i < table.widths.length; i++) { + const cell = rowData.cols.find(c => c.startCol === i); + if (cell) { + let mergedWidth = 0; + for (let j = cell.startCol; j <= cell.endCol; j++) { + mergedWidth += table.widths[j]; + } + const fillColor = table.rowColor; + doc.rect(currentX, cursorY, mergedWidth, rowHeight).fill(fillColor); + + if (cell.text) { + doc.fillColor(table.bodyFontColor); + doc.text(cell.text, currentX + table.cellHorizontalPadding, cursorY + table.cellTopPadding, { + width: mergedWidth - totalHorizontalPadding, + height: rowHeight - table.cellTopPadding - table.cellBottomPadding, + lineGap: 3 + }); + } + currentX += mergedWidth; + i = cell.endCol; + } else { + currentX += table.widths[i]; + } + } + + return { height: rowHeight, lines: 1 }; + } + + // Helper function to draw the fixed header block + function drawFixedHeader(doc, table, fixedHeaderData, cursorY, initialX, totalTableWidth) { + let headerBlockHeight = 0; + let headerBlockLines = 0; + let horizontalLineYCoords: number[] = []; + + horizontalLineYCoords.push(cursorY); // Top of the entire header block + + // Draw merged rows (first 4 rows, including the new blank one) + for (let i = 0; i < 4; i++) { + const rowData = fixedHeaderData[i]; + const { height: rowHeight, lines: rowLines } = drawMergedRow(doc, table, rowData, cursorY, initialX); + cursorY += rowHeight; + headerBlockHeight += rowHeight; + headerBlockLines += rowLines; + horizontalLineYCoords.push(cursorY); + } + + // Draw the main header + const headerRow = fixedHeaderData[4]; + const { height: headerHeight, lines: headerLines } = drawHeader(doc, table, headerRow, cursorY, initialX, totalTableWidth); + cursorY += headerHeight; + headerBlockHeight += headerHeight; + headerBlockLines += headerLines; + horizontalLineYCoords.push(cursorY); + + // Now draw the horizontal lines for the entire header block + for (const yCoord of horizontalLineYCoords) { + drawHorizontalLine(doc, initialX, yCoord, totalTableWidth, table.lineColor, table.lineWidth); + } + + // Now, manually draw the vertical lines for each header row based on its merged cells + // Outer left border for the entire header block + doc.moveTo(initialX, horizontalLineYCoords[0]).lineTo(initialX, cursorY).stroke(); + // Outer right border for the entire header block + doc.moveTo(initialX + totalTableWidth, horizontalLineYCoords[0]).lineTo(initialX + totalTableWidth, cursorY).stroke(); + + // Loop through the fixed header rows to draw internal vertical lines + for (let i = 0; i < fixedHeaderData.length; i++) { + const rowData = fixedHeaderData[i]; + const topY = horizontalLineYCoords[i]; + const bottomY = horizontalLineYCoords[i + 1]; + let currentX = initialX; + + if (Array.isArray(rowData)) { + // Draw all vertical lines for the non-merged header row + for (let j = 0; j < table.widths.length - 1; j++) { + currentX += table.widths[j]; + doc.moveTo(currentX, topY).lineTo(currentX, bottomY).stroke(); + } + } else if (i === 3) { + // Special case for the blank merged row (4th row, index 3) + // We want to draw all the vertical lines as if it's a regular row + for (let j = 0; j < table.widths.length - 1; j++) { + currentX += table.widths[j]; + doc.moveTo(currentX, topY).lineTo(currentX, bottomY).stroke(); + } + } + else { + // Draw internal lines for other merged rows + for (const cell of rowData.cols) { + let mergedWidth = 0; + for (let j = cell.startCol; j <= cell.endCol; j++) { + mergedWidth += table.widths[j]; + } + currentX += mergedWidth; + // Don't draw a vertical line if it's the very last column's right border + if (cell.endCol < table.widths.length - 1) { + doc.moveTo(currentX, topY).lineTo(currentX, bottomY).stroke(); + } + } + } + } + + return { height: headerBlockHeight, endY: cursorY, lines: headerBlockLines }; + } + + // Helper function to draw the total row without borders or vertical lines + function drawTotalRow(doc, table, totalValue, cursorY, initialX) { + // const totalRowDataArray = ['', '', '', 'Total', totalValue.toFixed(2), '']; + const totalRowDataArray = ['', '', '', 'Total', formatNumber(totalValue), '']; + const totalVerticalPadding = table.cellTopPadding + table.cellBottomPadding; + const totalHorizontalPadding = table.cellHorizontalPadding * 2; + const totalTableWidth = table.widths.reduce((a, b) => a + b, 0); + + let totalRowHeight = 0; + let maxTotalHeightInRow = 0; + for (let j = 0; j < totalRowDataArray.length; j++) { + const textOptions = { + width: table.widths[j] - totalHorizontalPadding, + lineGap: 3 + }; + const h = doc.heightOfString(totalRowDataArray[j], textOptions); + if (h > maxTotalHeightInRow) { + maxTotalHeightInRow = h; + } + } + totalRowHeight = maxTotalHeightInRow + totalVerticalPadding; + const linesInTotalRow = Math.ceil(maxTotalHeightInRow / doc.heightOfString('a', { lineGap: 3 })); + + const startY = cursorY + table.totalRowPaddingTop; + + // Draw the text only, without any rectangles or lines + let currentX = initialX; + doc.fontSize(table.fontSize); + for (let j = 0; j < totalRowDataArray.length; j++) { + doc.fillColor(table.bodyFontColor); + doc.text(totalRowDataArray[j], currentX + table.cellHorizontalPadding, startY + table.cellTopPadding, { + width: table.widths[j] - totalHorizontalPadding, + lineGap: 3 + }); + currentX += table.widths[j]; + } + + return { height: totalRowHeight + table.totalRowPaddingTop, lines: linesInTotalRow, endY: startY + totalRowHeight }; + } + + + // --- Function to manually draw a single data row --- + function drawDataRow(doc, table, rowData, cursorY, initialX, isEvenRow) { + const totalVerticalPadding = table.cellTopPadding + table.cellBottomPadding; + const totalHorizontalPadding = table.cellHorizontalPadding * 2; + const totalTableWidth = table.widths.reduce((a, b) => a + b, 0); + + let rowHeight = 0; + let maxHeightInRow = 0; + + const rowDataArray = [ + rowData.itemNo, + rowData.description, + rowData.noOfPieces, + rowData.weight, + formatNumber(rowData.itemValue), // formatted Item Value + rowData.countryOfOrigin + ]; + + for (let j = 0; j < rowDataArray.length; j++) { + const textOptions = { width: table.widths[j] - totalHorizontalPadding, lineGap: 3 }; + const h = doc.heightOfString(rowDataArray[j] || '', textOptions); + if (h > maxHeightInRow) { + maxHeightInRow = h; + } + } + + rowHeight = maxHeightInRow + totalVerticalPadding; + const linesInRow = Math.ceil(maxHeightInRow / doc.heightOfString('a', { lineGap: 3 })); + + // Draw the data row + const fillColor = isEvenRow ? table.alternatingRowColor : table.rowColor; + doc.rect(initialX, cursorY, totalTableWidth, rowHeight).fill(fillColor); // Fill the entire row rectangle + let currentX = initialX; + doc.fontSize(table.fontSize); + // for (let j = 0; j < rowDataArray.length; j++) { + // doc.fillColor(table.bodyFontColor); + // doc.text(rowDataArray[j], currentX + table.cellHorizontalPadding, cursorY + table.cellTopPadding, { + // width: table.widths[j] - totalHorizontalPadding, + // lineGap: 3 + // }); + // currentX += table.widths[j]; + // } + + for (let j = 0; j < rowDataArray.length; j++) { + doc.fillColor(table.bodyFontColor); + + const alignRight = (j === 2 || j === 4); // right-align No of Pieces and Item Value + + doc.text(rowDataArray[j], currentX + table.cellHorizontalPadding, cursorY + table.cellTopPadding, { + width: table.widths[j] - totalHorizontalPadding, + lineGap: 3, + align: alignRight ? 'right' : 'left' + }); + + currentX += table.widths[j]; + } + + return { height: rowHeight, lines: linesInRow, endY: cursorY + rowHeight }; + } + + // --- Function to manually draw the table from JSON data --- + function drawTable(doc, table, jsonData, headerData) { + let cursorY = table.y; + const initialX = table.x; + let continuationPageNo = 0; + + const totalTableWidth = table.widths.reduce((a, b) => a + b, 0); + + let rowStart = 0; + + while (rowStart < jsonData.length) { + let linesUsed = 0; + let rowsOnPage: any = []; + let currentY = cursorY; + let currentPageTotal = 0; + + // Draw fixed header block and count its visual lines + const fixedHeaderData = getFixedHeaderData({ ...headerData, contPageNo: String(continuationPageNo) }); + const fixedHeaderInfo = drawFixedHeader(doc, table, fixedHeaderData, currentY, initialX, totalTableWidth); + currentY = fixedHeaderInfo.endY; + linesUsed += fixedHeaderInfo.lines; + + // Collect data rows that can fit in remaining lines + for (let i = rowStart; i < jsonData.length; i++) { + const rowData = jsonData[i]; + const isEvenRow = i % 2 === 0; + + const { height, lines } = drawDataRow(doc, table, rowData, -1000, initialX, isEvenRow); // simulate row height + const remainingRows = jsonData.length - i - 1; + const willBeLastPage = remainingRows === 0; + const extraLines = 1 + (willBeLastPage ? 1 : 0); + if (linesUsed + lines + extraLines + 1 > maxLinesPerPage) break; // +1 for total row + rowsOnPage.push({ data: rowData, isEven: isEvenRow, height, lines }); + linesUsed += lines; + rowStart++; + currentPageTotal += parseFloat(rowData.itemValue); + } + + // Render data rows + let horizontalLineYCoords: number[] = [currentY]; + for (const row of rowsOnPage as any) { + drawDataRow(doc, table, row.data, currentY, initialX, row.isEven); + currentY += row.height; + horizontalLineYCoords.push(currentY); + } + + // Draw horizontal lines for data rows + for (const yCoord of horizontalLineYCoords) { + drawHorizontalLine(doc, initialX, yCoord, totalTableWidth, table.lineColor, table.lineWidth); + } + + // Draw vertical lines for data block + drawVerticalLines(doc, table, horizontalLineYCoords[0], currentY); + + // Draw total row + const totalRowInfo = drawTotalRow(doc, table, currentPageTotal, currentY, initialX); + currentY = totalRowInfo.endY; + + const isLastDataPage = rowStart >= jsonData.length; + if (isLastDataPage) { + const endMessage = '***** End of list, No item can be added *****'; + const fontSize = 10; + const verticalGap = 10; + + doc.fontSize(fontSize).font('Helvetica-Bold'); + + const textWidth = doc.widthOfString(endMessage); + const centerX = (doc.page.width - textWidth) / 2; + const endY = currentY + verticalGap; + + doc.text(endMessage, centerX, endY); + currentY = endY + doc.heightOfString(endMessage); // update Y if needed + } + + + // Add new page if more data remains + if (rowStart < jsonData.length) { + doc.addPage(); + continuationPageNo++; + cursorY = doc.page.margins.top; + } + } + + return continuationPageNo + 1; + } + + // Draw the custom table and get the total page count + const totalPages = drawTable(doc, table, jsonData, headerData); + + // Check if the total number of pages is even and add a "VOID" page if it is + if (totalPages % 2 === 0) { + doc.addPage(); + const voidText = 'VOID'; + doc.fontSize(14).font('Helvetica'); + const textWidth = doc.widthOfString(voidText); + const centerX = (doc.page.width - textWidth) / 2; + doc.text(voidText, centerX, doc.page.margins.top); + } + + // Finalize the PDF file + doc.end(); + + const generalistFilePath = path.join(__dirname, '../../../public/carnet-pdf', 'p2.pdf') + doc.pipe(fs.createWriteStream(generalistFilePath)); + + console.log('PDF created successfully!'); + + return "p2 done"; +} + +export const generateGeneralistPDF2 = async (carnetNumber: string) => { + const pageWidth = doc.page.width; + const seeAttachedText = 'See Attached'; + const seeAttachedWidth = doc.widthOfString(seeAttachedText); + const uniqueNoText = `Carnet No: ${carnetNumber}`; + const uniqueNoWidth = doc.widthOfString(uniqueNoText); + + // Center position for "See Attached" + const centerX = pageWidth / 2; + const seeAttachedX = centerX - (seeAttachedWidth / 2); + + // Position "Unique No" next to "See Attached" + const spacing = 90; // gap between texts + const uniqueNoX = seeAttachedX + seeAttachedWidth + spacing; + + // Vertically align both (e.g., 100 from top) + const y = 100; + + // If unique number will overflow, adjust its X position to stay in same line + let adjustedUniqueNoX = uniqueNoX; + if (uniqueNoX + uniqueNoWidth > pageWidth - 50) { + adjustedUniqueNoX = pageWidth - uniqueNoWidth - 50; + } + + doc.text(seeAttachedText, seeAttachedX, y); + doc.text(uniqueNoText, adjustedUniqueNoX, y); + + // Finalize PDF + doc.end(); + + const generalistFilePath = path.join(__dirname, '../../../public/carnet-pdf', 'p2.pdf') + doc.pipe(fs.createWriteStream(generalistFilePath)); + + return "p2 done" +} + +export const generateGreenCoverPDF = async () => { + const templatePath = path.join(__dirname, '../../..', 'public/pdf-templates', '2GeneralList.pdf'); + const existingPdfBytes = fs.readFileSync(templatePath); + + const pdfDoc = await pdfl.load(existingPdfBytes); + const form = pdfDoc.getForm(); + + let fields = form.getFields(); +} + +export const generateFinalPDF = async (inputFiles: string[], outputFile: string) => { + const merger = new PDFMerger(); + + for (const fileName of inputFiles) { + const filePath = path.join(__dirname, '../../../public/carnet-pdf', fileName); + console.log(`📄 Adding: ${filePath}`); + + try { + await merger.add(filePath); + } catch (err) { + console.error(`❌ Failed to add PDF: ${fileName}`, err); + } + } + + const outputPath = path.join(__dirname, '../../../public/carnet-pdf', outputFile); + + try { + await merger.save(outputPath); + console.log(`✅ Merged PDF saved to ${outputPath}`); + } catch (err) { + console.error(`❌ Failed to save merged PDF`, err); + throw new Error("Failed to save merged PDF"); + } +}; + +export const deleteFilesAsync = async (relativeFilePaths: string[]): Promise => { + for (const relativeFilePath of relativeFilePaths) { + const filePath = path.join(__dirname, '../../../public/carnet-pdf', relativeFilePath); + + if (fs.existsSync(filePath)) { + try { + await fsPromises.unlink(filePath); + console.log(`🗑️ Deleted: ${filePath}`); + } catch (err) { + console.error(`❌ Error deleting file: ${filePath}`, err); + } + } else { + console.warn(`⚠️ File not found: ${filePath}`); + } + } +}; \ No newline at end of file