import { requireAuth } from '@/server/utils/auth'; import { getExpenseById } from '@/server/utils/nocodb'; import { processExpenseWithCurrency } from '@/server/utils/currency'; import { createError } from 'h3'; import { formatDate } from '@/utils/dateUtils'; interface PDFOptions { documentName: string; subheader?: string; groupBy: 'none' | 'payer' | 'category' | 'date'; includeReceipts: boolean; includeSummary: boolean; includeDetails: boolean; pageFormat: 'A4' | 'Letter' | 'Legal'; includeProcessingFee?: boolean; } interface Expense { Id: number; 'Establishment Name': string; Price: string; PriceNumber: number; DisplayPrice: string; PriceUSD?: number; ConversionRate?: number; Payer: string; Category: string; 'Payment Method': string; Time: string; Contents?: string; Receipt?: any[]; } export default defineEventHandler(async (event) => { await requireAuth(event); const body = await readBody(event); const { expenseIds, options } = body; if (!expenseIds || !Array.isArray(expenseIds) || expenseIds.length === 0) { throw createError({ statusCode: 400, statusMessage: 'Expense IDs are required' }); } if (!options || !options.documentName) { throw createError({ statusCode: 400, statusMessage: 'PDF options with document name are required' }); } console.log('[expenses/generate-pdf] PDF generation requested for expenses:', expenseIds); try { // Fetch expense data const expenses: Expense[] = []; for (const expenseId of expenseIds) { const expense = await getExpenseById(expenseId); if (expense) { const processedExpense = await processExpenseWithCurrency(expense); expenses.push(processedExpense); } } if (expenses.length === 0) { throw createError({ statusCode: 404, statusMessage: 'No valid expenses found' }); } // Calculate totals const totals = calculateTotals(expenses, options.includeProcessingFee); 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); // Return PDF as base64 for download const pdfBase64 = Buffer.from(pdfContent).toString('base64'); return { success: true, data: { filename: `${options.documentName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`, content: pdfBase64, mimeType: 'application/pdf', size: pdfContent.length } }; } catch (error: any) { // If it's our intentional error, re-throw it if (error.statusCode === 501) { throw error; } console.error('[expenses/generate-pdf] Error generating PDF:', error); throw createError({ statusCode: 500, statusMessage: error.message || 'Failed to generate PDF' }); } }); function calculateTotals(expenses: Expense[], includeProcessingFee: boolean) { const originalTotal = expenses.reduce((sum, exp) => sum + (exp.PriceNumber || 0), 0); const usdTotal = expenses.reduce((sum, exp) => sum + (exp.PriceUSD || exp.PriceNumber || 0), 0); const processingFee = includeProcessingFee ? originalTotal * 0.05 : 0; const finalTotal = originalTotal + processingFee; return { originalTotal, usdTotal, processingFee, finalTotal, count: expenses.length }; } function getGroupingLabel(groupBy: string): string { switch (groupBy) { case 'payer': return 'By Person'; case 'category': return 'By Category'; case 'date': return 'By Date'; default: return 'No Grouping'; } } function generatePDFContent(expenses: Expense[], options: PDFOptions, totals: any): string { // Generate HTML content that can be converted to PDF const html = ` ${options.documentName}
${options.documentName}
${options.subheader ? `
${options.subheader}
` : ''}
${options.includeSummary ? `

Summary

Total Expenses: ${totals.count}

Subtotal: €${totals.originalTotal.toFixed(2)}

USD Equivalent: $${totals.usdTotal.toFixed(2)}

${options.includeProcessingFee ? `

Processing Fee (5%): €${totals.processingFee.toFixed(2)}

` : ''}

Final Total: €${totals.finalTotal.toFixed(2)}

Grouping: ${getGroupingLabel(options.groupBy)}

` : ''} ${options.includeDetails ? generateExpenseTable(expenses, options) : ''}
Generated on: ${new Date().toLocaleString()}
`; return html; } function generateExpenseTable(expenses: Expense[], options: PDFOptions): string { let tableHTML = ` ${options.includeDetails ? '' : ''} `; if (options.groupBy === 'none') { // No grouping - just list all expenses expenses.forEach(expense => { tableHTML += generateExpenseRow(expense, options); }); } else { // Group expenses const groups = groupExpenses(expenses, options.groupBy); Object.keys(groups).forEach(groupKey => { const groupExpenses = groups[groupKey]; const groupTotal = groupExpenses.reduce((sum, exp) => sum + (exp.PriceNumber || 0), 0); // Group header tableHTML += ` `; // Group expenses groupExpenses.forEach(expense => { tableHTML += generateExpenseRow(expense, options); }); }); } tableHTML += `
Date Establishment Category Payer Amount Payment MethodDescription
${groupKey} (${groupExpenses.length} expenses - €${groupTotal.toFixed(2)})
`; return tableHTML; } function generateExpenseRow(expense: Expense, options: PDFOptions): string { const date = expense.Time ? formatDate(expense.Time) : 'N/A'; const description = expense.Contents || 'N/A'; return ` ${date} ${expense['Establishment Name'] || 'N/A'} ${expense.Category || 'N/A'} ${expense.Payer || 'N/A'} €${expense.PriceNumber ? expense.PriceNumber.toFixed(2) : '0.00'} ${expense['Payment Method'] || 'N/A'} ${options.includeDetails ? `${description}` : ''} `; } function groupExpenses(expenses: Expense[], groupBy: string): Record { const groups: Record = {}; expenses.forEach(expense => { let groupKey = 'Unknown'; switch (groupBy) { case 'payer': groupKey = expense.Payer || 'Unknown Payer'; break; case 'category': groupKey = expense.Category || 'Unknown Category'; break; case 'date': groupKey = expense.Time ? formatDate(expense.Time) : 'Unknown Date'; break; } if (!groups[groupKey]) { groups[groupKey] = []; } groups[groupKey].push(expense); }); return groups; }