This commit is contained in:
sebseb7
2025-07-19 21:58:07 +02:00
commit 102a4ec9ff
37 changed files with 14258 additions and 0 deletions

352
src/routes/data.js Normal file
View File

@@ -0,0 +1,352 @@
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;