import { BadRequestException, InternalServerErrorException, Logger, } 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 || error instanceof BR) { logger.warn(`[${context}] ${error.message}`); throw error; } if (error.message?.includes('NJS-107')) { logger.warn(`[${context}] Invalid cursor encountered: ${error.message}`); throw new BadRequestException('Invalid database response.'); } if (error.message?.includes('ORA-')) { logger.error(`[${context}] Oracle error occurred: ${error.message}`); throw new InternalServerErrorException(error.message); } logger.error(`[${context}] Unexpected error:`, error.stack || error); throw new InternalServerErrorException(error.message || 'Internal error'); }; export const closeOracleDbConnection = async (connection: any, context: string = 'UnknownService'): Promise => { if (connection) { try { await connection.close(); } catch (closeErr) { logger.error(`[${context}] Failed to close DB connection`, closeErr.stack || closeErr); } } }; export const fetchCursor = async (cursor: oracledb.ResultSet, context: string = 'UnknownService'): Promise => { try { const rows = await cursor.getRows(); await cursor.close(); return rows; } catch (err) { logger.error(`[${context}] Failed to fetch from cursor`, err.stack || err); throw new InternalServerErrorException('Error reading data from database.'); } }; export const setEmptyStringsToNull = (obj: any): void => { Object.keys(obj).forEach((key) => { if (typeof obj[key] === 'object' && obj[key] !== null) { setEmptyStringsToNull(obj[key]); } else if (obj[key] === '') { obj[key] = null; } }); } 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}`); } } };