const express = require('express'); const fs = require('fs'); const path = require('path'); const { authenticateToken } = require('../middleware/auth'); const router = express.Router(); // Parse CSV data const parseCSV = () => { try { const csvPath = path.join(__dirname, '../../data.csv'); const csvData = fs.readFileSync(csvPath, 'utf8'); const lines = csvData.split('\n'); const headers = lines[0].split(';').map(h => h.replace(/"/g, '')); const transactions = []; for (let i = 1; i < lines.length; i++) { const line = lines[i]; if (!line.trim()) continue; // Parse CSV line (handle semicolon-separated values with quotes) const values = []; let current = ''; let inQuotes = false; for (let j = 0; j < line.length; j++) { const char = line[j]; if (char === '"') { inQuotes = !inQuotes; } else if (char === ';' && !inQuotes) { values.push(current); current = ''; } else { current += char; } } values.push(current); // Add last value if (values.length >= headers.length) { const transaction = {}; headers.forEach((header, index) => { transaction[header] = values[index] || ''; }); // Parse date and amount if (transaction['Buchungstag']) { const dateParts = transaction['Buchungstag'].split('.'); if (dateParts.length === 3) { // Convert DD.MM.YY to proper date const day = dateParts[0]; const month = dateParts[1]; const year = '20' + dateParts[2]; // Assuming 20xx transaction.parsedDate = new Date(year, month - 1, day); transaction.monthYear = `${year}-${month.padStart(2, '0')}`; } } // Parse amount if (transaction['Betrag']) { const amount = transaction['Betrag'].replace(',', '.').replace(/[^-0-9.]/g, ''); transaction.numericAmount = parseFloat(amount) || 0; } transactions.push(transaction); } } return transactions; } catch (error) { console.error('Error parsing CSV:', error); return []; } }; // Get available months router.get('/months', authenticateToken, (req, res) => { try { const transactions = parseCSV(); const months = [...new Set(transactions .filter(t => t.monthYear) .map(t => t.monthYear) )].sort().reverse(); // Newest first res.json({ months }); } catch (error) { console.error('Error getting months:', error); res.status(500).json({ error: 'Failed to load months' }); } }); // Get database transactions for JTL comparison const getJTLTransactions = async () => { try { const { executeQuery } = require('../config/database'); const query = ` SELECT cKonto, cKontozusatz, cName, dBuchungsdatum, tZahlungsabgleichUmsatz.kZahlungsabgleichUmsatz, cVerwendungszweck, fBetrag, tUmsatzKontierung.data FROM [eazybusiness].[dbo].[tZahlungsabgleichUmsatz] LEFT JOIN tUmsatzKontierung ON (tUmsatzKontierung.kZahlungsabgleichUmsatz = tZahlungsabgleichUmsatz.kZahlungsabgleichUmsatz) ORDER BY dBuchungsdatum desc, tZahlungsabgleichUmsatz.kZahlungsabgleichUmsatz desc `; const result = await executeQuery(query); return result.recordset || []; } catch (error) { console.error('Error fetching JTL transactions:', error); return []; } }; // Get transactions for a specific month router.get('/transactions/:monthYear', authenticateToken, async (req, res) => { try { const { monthYear } = req.params; const transactions = parseCSV(); const monthTransactions = transactions .filter(t => t.monthYear === monthYear) .sort((a, b) => b.parsedDate - a.parsedDate); // Newest first // Get JTL transactions for comparison let jtlTransactions = []; try { jtlTransactions = await getJTLTransactions(); } catch (error) { console.log('JTL database not available, continuing without JTL data'); } // Filter JTL transactions for the selected month const [year, month] = monthYear.split('-'); const jtlMonthTransactions = jtlTransactions.filter(jtl => { const jtlDate = new Date(jtl.dBuchungsdatum); return jtlDate.getFullYear() === parseInt(year) && jtlDate.getMonth() === parseInt(month) - 1; }); // Add JTL status to each CSV transaction const transactionsWithJTL = monthTransactions.map(transaction => { // Try to match by amount and date (approximate matching) const amount = transaction.numericAmount; const transactionDate = transaction.parsedDate; const jtlMatch = jtlMonthTransactions.find(jtl => { const jtlAmount = parseFloat(jtl.fBetrag) || 0; const jtlDate = new Date(jtl.dBuchungsdatum); // Match by amount (exact) and date (same day) const amountMatch = Math.abs(amount - jtlAmount) < 0.01; const dateMatch = transactionDate && jtlDate && transactionDate.getFullYear() === jtlDate.getFullYear() && transactionDate.getMonth() === jtlDate.getMonth() && transactionDate.getDate() === jtlDate.getDate(); return amountMatch && dateMatch; }); return { ...transaction, hasJTL: !!jtlMatch, jtlId: jtlMatch ? jtlMatch.kZahlungsabgleichUmsatz : null, isFromCSV: true }; }); // Find JTL transactions that don't have CSV matches (red rows) const unmatchedJTLTransactions = jtlMonthTransactions .filter(jtl => { const jtlAmount = parseFloat(jtl.fBetrag) || 0; const jtlDate = new Date(jtl.dBuchungsdatum); // Check if this JTL transaction has a CSV match const hasCSVMatch = monthTransactions.some(transaction => { const amount = transaction.numericAmount; const transactionDate = transaction.parsedDate; const amountMatch = Math.abs(amount - jtlAmount) < 0.01; const dateMatch = transactionDate && jtlDate && transactionDate.getFullYear() === jtlDate.getFullYear() && transactionDate.getMonth() === jtlDate.getMonth() && transactionDate.getDate() === jtlDate.getDate(); return amountMatch && dateMatch; }); return !hasCSVMatch; }) .map(jtl => ({ // Convert JTL format to CSV-like format for display 'Buchungstag': new Date(jtl.dBuchungsdatum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }), 'Verwendungszweck': jtl.cVerwendungszweck || '', 'Buchungstext': 'JTL Transaction', 'Beguenstigter/Zahlungspflichtiger': jtl.cName || '', 'Betrag': jtl.fBetrag ? jtl.fBetrag.toString().replace('.', ',') : '0,00', numericAmount: parseFloat(jtl.fBetrag) || 0, parsedDate: new Date(jtl.dBuchungsdatum), monthYear: monthYear, hasJTL: true, jtlId: jtl.kZahlungsabgleichUmsatz, isFromCSV: false, isJTLOnly: true })); // Combine CSV and JTL-only transactions const allTransactions = [...transactionsWithJTL, ...unmatchedJTLTransactions] .sort((a, b) => b.parsedDate - a.parsedDate); // Calculate summary const summary = { totalTransactions: allTransactions.length, totalIncome: allTransactions .filter(t => t.numericAmount > 0) .reduce((sum, t) => sum + t.numericAmount, 0), totalExpenses: allTransactions .filter(t => t.numericAmount < 0) .reduce((sum, t) => sum + Math.abs(t.numericAmount), 0), netAmount: allTransactions.reduce((sum, t) => sum + t.numericAmount, 0), jtlMatches: allTransactions.filter(t => t.hasJTL && t.isFromCSV).length, jtlMissing: allTransactions.filter(t => !t.hasJTL && t.isFromCSV).length, jtlOnly: allTransactions.filter(t => t.isJTLOnly).length, csvOnly: allTransactions.filter(t => !t.hasJTL && t.isFromCSV).length }; res.json({ transactions: allTransactions, summary, monthYear }); } catch (error) { console.error('Error getting transactions:', error); res.status(500).json({ error: 'Failed to load transactions' }); } }); // DATEV export functionality const buildDatevHeader = (periodStart, periodEnd) => { const ts = new Date().toISOString().replace(/[-T:\.Z]/g, '').slice(0, 17); // yyyymmddHHMMSSfff const meta = { consultant: 1001, client: 10001, fyStart: periodStart.slice(0, 4) + '0101', // fiscal year start accLength: 4, description: 'Bank Statement Export', currency: 'EUR' }; return [ '"EXTF"', 700, 21, '"Buchungsstapel"', 12, ts, '', '', '', '', // 7‑10 spare meta.consultant, meta.client, // 11, 12 meta.fyStart, meta.accLength, // 13, 14 periodStart, periodEnd, // 15, 16 '"' + meta.description + '"', 'AM', 1, 0, 1, meta.currency ].join(';'); }; const DATEV_COLS = [ 'Umsatz (ohne Soll/Haben-Kz)', 'Soll/Haben-Kennzeichen', 'WKZ Umsatz', 'Kurs', 'Basis-Umsatz', 'WKZ Basis-Umsatz', 'Konto', 'Gegenkonto (ohne BU-Schlüssel)', 'BU-Schlüssel', 'Belegdatum', 'Belegfeld 1', 'Belegfeld 2', 'Skonto', 'Buchungstext', 'Postensperre', 'Diverse Adressnummer', 'Geschäftspartnerbank', 'Sachverhalt', 'Zinssperre', 'Beleglink' ].join(';'); const formatDatevAmount = (amount) => { return Math.abs(amount).toFixed(2).replace('.', ','); }; const formatDatevDate = (dateString) => { if (!dateString) return ''; const parts = dateString.split('.'); if (parts.length === 3) { const day = parts[0].padStart(2, '0'); const month = parts[1].padStart(2, '0'); return day + month; } return ''; }; const quote = (str, maxLen = 60) => { if (!str) return '""'; return '"' + str.slice(0, maxLen).replace(/"/g, '""') + '"'; }; // DATEV export endpoint router.get('/datev/:monthYear', authenticateToken, async (req, res) => { try { const { monthYear } = req.params; const [year, month] = monthYear.split('-'); // Get transactions for the month const transactions = parseCSV(); const monthTransactions = transactions .filter(t => t.monthYear === monthYear) .sort((a, b) => a.parsedDate - b.parsedDate); // Oldest first for DATEV if (!monthTransactions.length) { return res.status(404).json({ error: 'No transactions found for this month' }); } // Build DATEV format const periodStart = `${year}${month.padStart(2, '0')}01`; const periodEnd = new Date(year, month, 0).toISOString().slice(0, 10).replace(/-/g, ''); const header = buildDatevHeader(periodStart, periodEnd); const rows = monthTransactions.map((transaction, index) => { const amount = Math.abs(transaction.numericAmount); const isDebit = transaction.numericAmount < 0 ? 'S' : 'H'; // S = Soll (debit), H = Haben (credit) return [ formatDatevAmount(amount), // #1 Umsatz isDebit, // #2 Soll/Haben quote('EUR', 3), // #3 WKZ Umsatz '', '', '', // #4-6 (no FX) '1200', // #7 Konto (Bank account) transaction.numericAmount < 0 ? '4000' : '8400', // #8 Gegenkonto (expense/income) '', // #9 BU-Schlüssel formatDatevDate(transaction['Buchungstag']), // #10 Belegdatum quote((index + 1).toString(), 36), // #11 Belegfeld 1 (sequential number) '', '', // #12, #13 quote(transaction['Verwendungszweck'] || transaction['Buchungstext'], 60), // #14 Buchungstext '', '', '', '', '', // #15-19 unused '' // #20 Beleglink ].join(';'); }); const csv = [header, DATEV_COLS, ...rows].join('\r\n'); // Set headers for file download const filename = `DATEV_${year}_${month.padStart(2, '0')}.csv`; res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.setHeader('Content-Type', 'text/csv; charset=latin1'); // Convert to latin1 encoding for DATEV compatibility const buffer = Buffer.from(csv, 'utf8'); res.send(buffer); } catch (error) { console.error('Error generating DATEV export:', error); res.status(500).json({ error: 'Failed to generate DATEV export' }); } }); module.exports = router;