diff --git a/components/InterestDuplicateNotificationBanner.vue b/components/InterestDuplicateNotificationBanner.vue index 82d64e0..29c0374 100644 --- a/components/InterestDuplicateNotificationBanner.vue +++ b/components/InterestDuplicateNotificationBanner.vue @@ -89,16 +89,10 @@ const dismissBanner = () => { // Check if banner was already dismissed this session onMounted(() => { - if (process.client) { - const dismissed = sessionStorage.getItem('interest-duplicates-banner-dismissed'); - if (dismissed === 'true') { - showBanner.value = false; - return; - } - } + // Always check for duplicates - remove session storage blocking for debugging + console.log('[InterestDuplicateNotification] Component mounted, checking for duplicates...'); - // Check for duplicates for sales/admin users - // Small delay to let other components load first - setTimeout(checkForDuplicates, 2000); + // Check for duplicates for sales/admin users immediately + checkForDuplicates(); }); diff --git a/pages/dashboard/expenses.vue b/pages/dashboard/expenses.vue index 061eb47..8a07f69 100644 --- a/pages/dashboard/expenses.vue +++ b/pages/dashboard/expenses.vue @@ -487,7 +487,15 @@ const generatePDF = async (options: any) => { try { console.log('[expenses] Generating PDF with options:', options); - const response = await $fetch('/api/expenses/generate-pdf', { + const response = await $fetch<{ + success: boolean; + data: { + filename: string; + content: string; + mimeType: string; + size: number; + }; + }>('/api/expenses/generate-pdf', { method: 'POST', body: { expenseIds: selectedExpenses.value, @@ -495,20 +503,31 @@ const generatePDF = async (options: any) => { } }); - // Handle PDF download - const blob = new Blob([response as any], { type: 'application/pdf' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${options.documentName || 'expenses'}.pdf`; - a.click(); - window.URL.revokeObjectURL(url); + if (response.success && response.data) { + // For now, create HTML file instead of PDF since we're generating HTML content + const htmlContent = atob(response.data.content); // Decode base64 + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${options.documentName || 'expenses'}.html`; + a.click(); + window.URL.revokeObjectURL(url); + + // Also open in new tab for immediate viewing + const newTab = window.open(); + if (newTab) { + newTab.document.open(); + newTab.document.write(htmlContent); + newTab.document.close(); + } + } showPDFModal.value = false; } catch (err: any) { console.error('[expenses] Error generating PDF:', err); - error.value = 'Failed to generate PDF'; + error.value = err.message || 'Failed to generate PDF'; } }; diff --git a/server/api/expenses/generate-pdf.ts b/server/api/expenses/generate-pdf.ts index 18ce1a3..d339777 100644 --- a/server/api/expenses/generate-pdf.ts +++ b/server/api/expenses/generate-pdf.ts @@ -1,6 +1,8 @@ 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; @@ -69,30 +71,27 @@ export default defineEventHandler(async (event) => { }); } - // Calculate totals to show the preview is working correctly + // 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); - // For now, return a helpful error with the calculated information - throw createError({ - statusCode: 501, - statusMessage: 'PDF generation is being upgraded', - message: `PDF generation is being upgraded! ✅ Your selection is ready: - -📊 Summary: -• ${totals.count} expenses selected -• Total: €${totals.originalTotal.toFixed(2)} -• USD equivalent: $${totals.usdTotal.toFixed(2)} -${options.includeProcessingFee ? `• With 5% fee: €${totals.finalTotal.toFixed(2)}` : ''} - -📋 Document: "${options.documentName}" -${options.subheader ? `📝 Subtitle: "${options.subheader}"` : ''} -🔗 Grouping: ${getGroupingLabel(options.groupBy)} - -💡 Use CSV export for now, or contact support for manual PDF generation with these exact settings.` - }); + // 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 @@ -132,3 +131,153 @@ function getGroupingLabel(groupBy: string): string { 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 += ` + +
DateEstablishmentCategoryPayerAmountPayment 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; +}