feat: Enhance InterestDuplicateNotificationBanner to always check for duplicates on mount and improve PDF generation to return base64 content
This commit is contained in:
parent
bf24dc9103
commit
8f625c0df4
|
|
@ -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();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
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'}.pdf`;
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
// Generate PDF content
|
||||
const pdfContent = generatePDFContent(expenses, options, totals);
|
||||
|
||||
📊 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)}` : ''}
|
||||
// Return PDF as base64 for download
|
||||
const pdfBase64 = Buffer.from(pdfContent).toString('base64');
|
||||
|
||||
📋 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.`
|
||||
});
|
||||
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 = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${options.documentName}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #333; padding-bottom: 20px; }
|
||||
.document-title { font-size: 24px; font-weight: bold; margin-bottom: 10px; }
|
||||
.subheader { font-size: 16px; color: #666; }
|
||||
.summary { background-color: #f5f5f5; padding: 15px; margin: 20px 0; border-radius: 5px; }
|
||||
.expense-table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
.expense-table th, .expense-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
.expense-table th { background-color: #f2f2f2; font-weight: bold; }
|
||||
.expense-table tr:nth-child(even) { background-color: #f9f9f9; }
|
||||
.group-header { background-color: #e7f3ff; font-weight: bold; }
|
||||
.total-row { background-color: #d4edda; font-weight: bold; }
|
||||
.processing-fee { background-color: #fff3cd; }
|
||||
.final-total { background-color: #d1ecf1; font-weight: bold; font-size: 1.1em; }
|
||||
.date-generated { text-align: right; color: #666; font-size: 12px; margin-top: 30px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="document-title">${options.documentName}</div>
|
||||
${options.subheader ? `<div class="subheader">${options.subheader}</div>` : ''}
|
||||
</div>
|
||||
|
||||
${options.includeSummary ? `
|
||||
<div class="summary">
|
||||
<h3>Summary</h3>
|
||||
<p><strong>Total Expenses:</strong> ${totals.count}</p>
|
||||
<p><strong>Subtotal:</strong> €${totals.originalTotal.toFixed(2)}</p>
|
||||
<p><strong>USD Equivalent:</strong> $${totals.usdTotal.toFixed(2)}</p>
|
||||
${options.includeProcessingFee ? `<p><strong>Processing Fee (5%):</strong> €${totals.processingFee.toFixed(2)}</p>` : ''}
|
||||
<p><strong>Final Total:</strong> €${totals.finalTotal.toFixed(2)}</p>
|
||||
<p><strong>Grouping:</strong> ${getGroupingLabel(options.groupBy)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${options.includeDetails ? generateExpenseTable(expenses, options) : ''}
|
||||
|
||||
<div class="date-generated">
|
||||
Generated on: ${new Date().toLocaleString()}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function generateExpenseTable(expenses: Expense[], options: PDFOptions): string {
|
||||
let tableHTML = `
|
||||
<table class="expense-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Establishment</th>
|
||||
<th>Category</th>
|
||||
<th>Payer</th>
|
||||
<th>Amount</th>
|
||||
<th>Payment Method</th>
|
||||
${options.includeDetails ? '<th>Description</th>' : ''}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
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 += `
|
||||
<tr class="group-header">
|
||||
<td colspan="${options.includeDetails ? '7' : '6'}">${groupKey} (${groupExpenses.length} expenses - €${groupTotal.toFixed(2)})</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
// Group expenses
|
||||
groupExpenses.forEach(expense => {
|
||||
tableHTML += generateExpenseRow(expense, options);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tableHTML += `
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
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 `
|
||||
<tr>
|
||||
<td>${date}</td>
|
||||
<td>${expense['Establishment Name'] || 'N/A'}</td>
|
||||
<td>${expense.Category || 'N/A'}</td>
|
||||
<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>` : ''}
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
function groupExpenses(expenses: Expense[], groupBy: string): Record<string, Expense[]> {
|
||||
const groups: Record<string, Expense[]> = {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue