port-nimara-client-portal/server/api/expenses/export-csv.ts

158 lines
5.3 KiB
TypeScript

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'
});
}
});