import { requireSalesOrAdmin } from '@/server/utils/auth'; import { logAuditEvent } from '@/server/utils/audit-logger'; import { getExpenseById } from '@/server/utils/nocodb'; import { processExpenseWithCurrency } from '@/server/utils/currency'; import { uploadBuffer } from '@/server/utils/minio'; import type { Expense } from '@/utils/types'; export default defineEventHandler(async (event) => { await requireSalesOrAdmin(event); const body = await readBody(event); const { expenseIds } = body; if (!expenseIds || !Array.isArray(expenseIds) || expenseIds.length === 0) { throw createError({ statusCode: 400, statusMessage: 'Expense IDs are required' }); } console.log('[expenses/export-csv] Generating CSV for expenses:', expenseIds); try { // Get user info for file naming const userInfo = event.context.user; const userName = userInfo?.preferred_username || userInfo?.email || 'user'; // Fetch all expenses const expenses: Expense[] = []; const failedExpenses: string[] = []; for (const id of expenseIds) { try { const expense = await getExpenseById(id.toString()); const processedExpense = await processExpenseWithCurrency(expense); expenses.push(processedExpense); } catch (error) { console.error(`[expenses/export-csv] Failed to fetch expense ${id}:`, error); failedExpenses.push(id.toString()); } } if (failedExpenses.length > 0) { throw createError({ statusCode: 400, statusMessage: `Failed to fetch expenses: ${failedExpenses.join(', ')}` }); } if (expenses.length === 0) { throw createError({ statusCode: 404, statusMessage: 'No expenses found' }); } // Sort expenses by date expenses.sort((a, b) => new Date(a.Time).getTime() - new Date(b.Time).getTime()); // Calculate totals const subtotalEUR = expenses.reduce((sum, expense) => { if (expense.currency === 'EUR') { return sum + (expense.PriceNumber || 0); } else { // Convert to EUR (assuming PriceNumber is in original currency and we have EUR conversion) return sum + (expense.PriceNumber || 0) / (expense.ConversionRate || 1); } }, 0); const processingFee = subtotalEUR * 0.05; const totalWithFee = subtotalEUR + processingFee; // Generate CSV content const csvHeaders = [ 'Date', 'Time', 'Establishment', 'Category', 'Payment Method', 'Payer', 'Amount (EUR)', 'Original Amount', 'Currency', 'Conversion Rate', 'Description', 'Receipt Count', 'Paid Status' ]; const csvRows = expenses.map(expense => [ new Date(expense.Time).toLocaleDateString(), new Date(expense.Time).toLocaleTimeString(), `"${(expense['Establishment Name'] || '').replace(/"/g, '""')}"`, expense.Category || '', expense['Payment Method'] || '', expense.Payer || '', expense.currency === 'EUR' ? (expense.PriceNumber || 0).toFixed(2) : ((expense.PriceNumber || 0) / (expense.ConversionRate || 1)).toFixed(2), (expense.PriceNumber || 0).toFixed(2), expense.currency || '', expense.ConversionRate || 1, `"${(expense.Contents || '').replace(/"/g, '""')}"`, expense.Receipt?.length || 0, expense.Paid ? 'Yes' : 'No' ]); // Add summary rows csvRows.push([]); csvRows.push(['SUMMARY']); csvRows.push(['Subtotal (EUR)', '', '', '', '', '', subtotalEUR.toFixed(2)]); csvRows.push(['Processing Fee (5%)', '', '', '', '', '', processingFee.toFixed(2)]); csvRows.push(['Total with Fee (EUR)', '', '', '', '', '', totalWithFee.toFixed(2)]); const csvContent = [ csvHeaders.join(','), ...csvRows.map(row => row.join(',')) ].join('\n'); // Generate filename with date range const dates = expenses.map(e => new Date(e.Time)); const startDate = new Date(Math.min(...dates.map(d => d.getTime()))); const endDate = new Date(Math.max(...dates.map(d => d.getTime()))); const formatDate = (date: Date) => date.toISOString().split('T')[0]; const filename = `expenses_${userName}_${formatDate(startDate)}_to_${formatDate(endDate)}.csv`; // Store CSV in MinIO const csvBuffer = Buffer.from(csvContent, 'utf8'); const year = new Date().getFullYear(); const month = String(new Date().getMonth() + 1).padStart(2, '0'); const storagePath = `expense-sheets/${year}/${month}/${filename}`; try { await uploadBuffer(csvBuffer, storagePath, 'text/csv'); console.log(`[expenses/export-csv] CSV stored at: ${storagePath}`); } catch (error) { console.error('[expenses/export-csv] Failed to store CSV in MinIO:', error); // Continue with direct download even if storage fails } // Return CSV for direct download setHeader(event, 'Content-Type', 'text/csv'); setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`); setHeader(event, 'Content-Length', csvBuffer.length); return csvContent; } catch (error: any) { console.error('[expenses/export-csv] Error generating CSV:', error); throw createError({ statusCode: 500, statusMessage: error.message || 'Failed to generate CSV export' }); } });