Refactor expense form and add PDF generation functionality
- Update expense form fields (merchant->establishmentName, amount->price) - Add PDF generation with Puppeteer integration - Create PDFOptionsModal component for export options - Update expense form validation and UI layout - Add server API endpoint for PDF generation
This commit is contained in:
@@ -3,12 +3,14 @@ import { getExpenseById } from '@/server/utils/nocodb';
|
||||
import { processExpenseWithCurrency } from '@/server/utils/currency';
|
||||
import { createError } from 'h3';
|
||||
import { formatDate } from '@/utils/dateUtils';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
interface PDFOptions {
|
||||
documentName: string;
|
||||
subheader?: string;
|
||||
groupBy: 'none' | 'payer' | 'category' | 'date';
|
||||
includeReceipts: boolean;
|
||||
includeReceiptContents: boolean;
|
||||
includeSummary: boolean;
|
||||
includeDetails: boolean;
|
||||
pageFormat: 'A4' | 'Letter' | 'Legal';
|
||||
@@ -77,19 +79,22 @@ export default defineEventHandler(async (event) => {
|
||||
console.log('[expenses/generate-pdf] Successfully calculated totals:', totals);
|
||||
console.log('[expenses/generate-pdf] Options received:', options);
|
||||
|
||||
// Generate PDF content
|
||||
const pdfContent = generatePDFContent(expenses, options, totals);
|
||||
// Generate HTML content
|
||||
const htmlContent = generateHTMLContent(expenses, options, totals);
|
||||
|
||||
// Convert HTML to PDF using Puppeteer
|
||||
const pdfBuffer = await generatePDFFromHTML(htmlContent, options);
|
||||
|
||||
// Return PDF as base64 for download
|
||||
const pdfBase64 = Buffer.from(pdfContent).toString('base64');
|
||||
const pdfBase64 = pdfBuffer.toString('base64');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
filename: `${options.documentName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`,
|
||||
filename: `${options.documentName.replace(/[^a-zA-Z0-9\-_\s]/g, '_')}.pdf`,
|
||||
content: pdfBase64,
|
||||
mimeType: 'application/pdf',
|
||||
size: pdfContent.length
|
||||
size: pdfBuffer.length
|
||||
}
|
||||
};
|
||||
|
||||
@@ -132,7 +137,7 @@ function getGroupingLabel(groupBy: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function generatePDFContent(expenses: Expense[], options: PDFOptions, totals: any): string {
|
||||
function generateHTMLContent(expenses: Expense[], options: PDFOptions, totals: any): string {
|
||||
// Generate HTML content that can be converted to PDF
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
@@ -197,7 +202,7 @@ function generateExpenseTable(expenses: Expense[], options: PDFOptions): string
|
||||
<th>Payer</th>
|
||||
<th>Amount</th>
|
||||
<th>Payment Method</th>
|
||||
${options.includeDetails ? '<th>Description</th>' : ''}
|
||||
${options.includeReceiptContents ? '<th>Description</th>' : ''}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -219,7 +224,7 @@ function generateExpenseTable(expenses: Expense[], options: PDFOptions): string
|
||||
// Group header
|
||||
tableHTML += `
|
||||
<tr class="group-header">
|
||||
<td colspan="${options.includeDetails ? '7' : '6'}">${groupKey} (${groupExpenses.length} expenses - €${groupTotal.toFixed(2)})</td>
|
||||
<td colspan="${options.includeReceiptContents ? '7' : '6'}">${groupKey} (${groupExpenses.length} expenses - €${groupTotal.toFixed(2)})</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
@@ -250,7 +255,7 @@ function generateExpenseRow(expense: Expense, options: PDFOptions): string {
|
||||
<td>${expense.Payer || 'N/A'}</td>
|
||||
<td>€${expense.PriceNumber ? expense.PriceNumber.toFixed(2) : '0.00'}</td>
|
||||
<td>${expense['Payment Method'] || 'N/A'}</td>
|
||||
${options.includeDetails ? `<td>${description}</td>` : ''}
|
||||
${options.includeReceiptContents ? `<td>${description}</td>` : ''}
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
@@ -281,3 +286,78 @@ function groupExpenses(expenses: Expense[], groupBy: string): Record<string, Exp
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
async function generatePDFFromHTML(htmlContent: string, options: PDFOptions): Promise<Buffer> {
|
||||
let browser;
|
||||
|
||||
try {
|
||||
console.log('[expenses/generate-pdf] Launching Puppeteer browser...');
|
||||
|
||||
// Launch browser with optimized settings
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--no-first-run',
|
||||
'--no-zygote',
|
||||
'--disable-gpu'
|
||||
]
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Set content with proper encoding
|
||||
await page.setContent(htmlContent, {
|
||||
waitUntil: 'networkidle0',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
// Get page format dimensions
|
||||
const format = getPageFormat(options.pageFormat);
|
||||
|
||||
console.log('[expenses/generate-pdf] Generating PDF with format:', format);
|
||||
|
||||
// Generate PDF with proper options
|
||||
const pdfUint8Array = await page.pdf({
|
||||
format: format.format,
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: '20mm',
|
||||
right: '15mm',
|
||||
bottom: '20mm',
|
||||
left: '15mm'
|
||||
},
|
||||
preferCSSPageSize: true
|
||||
});
|
||||
|
||||
// Convert Uint8Array to Buffer
|
||||
const pdfBuffer = Buffer.from(pdfUint8Array);
|
||||
|
||||
console.log('[expenses/generate-pdf] PDF generated successfully, size:', pdfBuffer.length, 'bytes');
|
||||
|
||||
return pdfBuffer;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[expenses/generate-pdf] Puppeteer error:', error);
|
||||
throw new Error(`PDF generation failed: ${error?.message || 'Unknown error'}`);
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPageFormat(pageFormat: string): { format: any } {
|
||||
switch (pageFormat) {
|
||||
case 'Letter':
|
||||
return { format: 'letter' };
|
||||
case 'Legal':
|
||||
return { format: 'legal' };
|
||||
case 'A4':
|
||||
default:
|
||||
return { format: 'a4' };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user