safeer-pdf-generator
Version:
Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery
373 lines (372 loc) • 11.9 kB
JavaScript
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} ${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