Files
fibdash/src/routes/data.js
2025-07-19 21:58:07 +02:00

352 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
'', '', '', '', // 710 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;