UNPKG

safeer-pdf-generator

Version:

Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery

373 lines (372 loc) 11.9 kB
import { getNestedProperty } from '../utils/guards.js'; /** * Default template compiler that matches the original NestJS implementation */ export function defaultTemplate(params) { const { title, data, columns, locale = 'en', userInfo = {}, infoSection = [], pdfFilteredColumns = [], translationFn: t = (key) => key, } = params; const today = new Date(); const isRTL = locale === 'ar'; // Use reliable date formatting to avoid "about:blank" issues const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const datePart = `${months[today.getMonth()]} ${today.getDate()}, ${today.getFullYear()}`; let hours = today.getHours(); const minutes = today.getMinutes(); const ampm = hours >= 12 ? 'PM' : 'AM'; hours = hours % 12; hours = hours ? hours : 12; // the hour '0' should be '12' const timePart = `${hours}:${minutes.toString().padStart(2, '0')} ${ampm}`; // Puppeteer header (for all pages) const header = ` <div style=" width: 100%; box-sizing: border-box; display: flex; flex-direction: ${isRTL ? 'row-reverse' : 'row'}; align-items: center; justify-content: space-between; border-bottom: 1px solid #e0e0e0; padding: 0 10px 4px 10px; font-family: 'Tajawal', Arial, sans-serif; "> <div style="flex: 0 0 auto;"> <img src="${userInfo.companyLogoBase64 || ''}" style="width: 100px; height: 45px; object-fit: contain;" /> </div> <div style="flex: 1; text-align: center; padding: 0 15px;"> <div style="font-size: 15px; font-weight: bold; color: #2c3e50;">${t(title)}</div> <div style="font-size: 12px; color: #2c3e50;">${datePart} &nbsp; ${timePart}</div> </div> <div style="flex: 0 0 auto; font-size: 10px; color: #2c3e50; text-align: ${isRTL ? 'left' : 'right'};"> <div>${userInfo.name || ''}</div> <div>${userInfo.email || ''}</div> <div>${userInfo.mobile || ''}</div> </div> </div> `; // No footer in template - page numbers and footer are now handled by MergeService.addPageNumbers() const footer = ``; // Info section rendering const renderInfoSection = () => { if (!infoSection || infoSection.length === 0) return ''; return ` <div class="info-container"> <div class="info-row"> ${infoSection .map(item => ` <div class="info-item"> <span class="info-label">${t(item.label)}</span> <span class="info-label">:</span> <span class="info-value">${item.value}</span> </div> `) .join('')} </div> </div> `; }; // Table rendering const renderCell = (col, value, _item) => { if (['image', 'logo', 'avatar', 'flag', 'icon'].includes(col.dataIndex) && value) { return `<img src="${value}" class="image-cell" alt="${col.title}" />`; } if (col.dataIndex === 'document' && value) { return `<a href="${value}" class="link-cell">Download</a>`; } if (['isActive', 'isBlocked', 'isMain'].includes(col.dataIndex)) { return `${value ? t('yes') : t('no')}`; } return `${value || '-'}`; }; const filteredColumns = columns.filter(col => !pdfFilteredColumns.includes(col.key)); const renderTable = () => { if (!data || data.length === 0) { return `<div class="no-data">${t('noData')}</div>`; } // Only render first 500 rows per chunk for performance const visibleData = data.slice(0, 500); const hiddenCount = data.length - visibleData.length; return ` <table class="data-table"> <thead> <tr class="table-header"> ${filteredColumns .map(col => ` <th class="cell cell-header"> ${col.title.toUpperCase()} </th> `) .join('')} </tr> </thead> <tbody> ${visibleData .map((item, rowIndex) => ` <tr class="table-row ${rowIndex % 2 === 0 ? 'alternate-row' : ''}"> ${filteredColumns .map(col => { const value = getNestedProperty(item, col.dataIndex); return `<td class="cell">${renderCell(col, value, item)}</td>`; }) .join('')} </tr> `) .join('')} </tbody> ${hiddenCount > 0 ? ` <tfoot> <tr> <td colspan="${filteredColumns.length}" class="row-count-message"> Showing ${visibleData.length} of ${data.length} rows in this chunk </td> </tr> </tfoot> ` : ''} </table> `; }; // Main HTML (body) -- best for printable area const html = ` <!DOCTYPE html> <html dir="${isRTL ? 'rtl' : 'ltr'}"> <head> <meta charset="UTF-8"> <title>${title}</title> <style> @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@400;700&display=swap'); html, body { width: 100%; min-width: 100%; max-width: 100%; margin: 0; padding: 0; } body { font-family: 'Tajawal', Arial, sans-serif; background-color: #FFFFFF; margin: 0; padding: 0; padding-bottom: 60px; /* Add bottom padding to prevent footer overlap */ } .page { width: 100%; min-width: 100%; max-width: 100%; display: flex; flex-direction: column; margin: 0; padding: 0; } .info-container { border: 1px solid #e0e0e0; border-radius: 4px; background-color: #f8f9fa; margin: 0 0 10px 0; padding: 10px; } .info-row { display: flex; margin-top: 5px; gap: 10px; flex-direction: ${isRTL ? 'row-reverse' : 'row'}; } .info-item { width: 32%; display: flex; align-items: center; flex-direction: ${isRTL ? 'row-reverse' : 'row'}; } .info-label { font-size: 11px; font-weight: bold; color: #2c3e50; margin: 0 2px; } .info-value { font-size: 11px; color: #2c3e50; } .content-container { flex-grow: 1; width: 100%; margin: 0; padding: 0; margin-bottom: 60px; /* Add bottom margin for footer space */ } .data-table { width: 100%; border-collapse: collapse; margin: 0; padding: 0; table-layout: auto; min-width: 100%; } .data-table thead { display: table-header-group; } .table-header { background-color: #f8f9fa; page-break-inside: avoid; page-break-after: avoid; } .table-row { border-bottom: 1px solid #e0e0e0; page-break-inside: avoid; } .alternate-row { background-color: #f8f9fa; } .cell, .cell-header { border-top: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0; border-left: none; border-right: none; padding: 8px 6px; font-size: 12px; text-align: center; color: #212529; overflow: hidden; text-overflow: ellipsis; white-space: normal; word-wrap: break-word; max-width: 150px; min-height: 20px; vertical-align: middle; } .cell-header { font-weight: bold; font-size: 13px; color: #2c3e50; background-color: #f8f9fa; padding: 10px 8px; white-space: normal; word-wrap: break-word; } .image-cell { width: 40px; height: 40px; object-fit: contain; margin: 0 auto; display: block; } .link-cell { color: #3498db; text-decoration: none; } .row-count-message { text-align: center; font-size: 9px; color: #95a5a6; padding: 8px; font-style: italic; border-top: 1px dashed #ecf0f1; } .no-data { text-align: center; padding: 20px; font-size: 14px; color: #7f8c8d; background-color: #f8f9fa; border-radius: 4px; } /* Print-specific styles for repeating headers */ @media print { .data-table { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .data-table thead { display: table-header-group; } .data-table tbody { display: table-row-group; } .table-header { page-break-inside: avoid; page-break-after: avoid; } .table-row { page-break-inside: avoid; } } </style> </head> <body> <div class="page"> ${renderInfoSection()} <div class="content-container"> ${renderTable()} </div> </div> </body> </html> `; return { html, header, footer }; } /** * Simple template for minimal reports */ export const simpleTemplate = (params) => { const { title, data = [], columns = [], translationFn: t = (key) => key } = params; const header = ` <div style="text-align: center; padding: 10px; border-bottom: 1px solid #ccc;"> <h2 style="margin: 0; font-family: Arial, sans-serif;">${t(title)}</h2> </div> `; const footer = ``; const renderSimpleTable = () => { if (!data || data.length === 0) { return `<p style="text-align: center;">${t('noData')}</p>`; } return ` <table style="width: 100%; border-collapse: collapse; font-family: Arial, sans-serif;"> <thead> <tr style="background-color: #f5f5f5;"> ${columns .map(col => ` <th style="border: 1px solid #ddd; padding: 8px; text-align: left;"> ${col.title} </th> `) .join('')} </tr> </thead> <tbody> ${data .map((row, index) => ` <tr style="background-color: ${index % 2 === 0 ? '#fff' : '#f9f9f9'};"> ${columns .map(col => ` <td style="border: 1px solid #ddd; padding: 8px;"> ${getNestedProperty(row, col.dataIndex) || '-'} </td> `) .join('')} </tr> `) .join('')} </tbody> </table> `; }; const html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>${title}</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } table { border-collapse: collapse; } </style> </head> <body> ${renderSimpleTable()} </body> </html> `; return { html, header, footer }; }; //# sourceMappingURL=defaultTemplate.js.map