From 5c416c77f0e0ce19c3a095bfd5d5f8ed2f3979e2 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Fri, 1 Aug 2025 15:45:01 +0200 Subject: [PATCH] Refactor data.js to remove old CSV parsing logic and integrate a new router structure. Consolidate transaction retrieval and enhance error handling for various endpoints. Update the DATEV export functionality to utilize the new database queries and improve overall code organization. --- src/routes/data.js | 1408 +----------------------- src/routes/data/bankingTransactions.js | 160 +++ src/routes/data/csvImport.js | 373 +++++++ src/routes/data/datev.js | 71 ++ src/routes/data/helpers/jtl.js | 67 ++ src/routes/data/index.js | 22 + src/routes/data/kreditors.js | 181 +++ src/routes/data/months.js | 33 + src/routes/data/pdf.js | 72 ++ src/routes/data/transactions.js | 348 ++++++ 10 files changed, 1332 insertions(+), 1403 deletions(-) create mode 100644 src/routes/data/bankingTransactions.js create mode 100644 src/routes/data/csvImport.js create mode 100644 src/routes/data/datev.js create mode 100644 src/routes/data/helpers/jtl.js create mode 100644 src/routes/data/index.js create mode 100644 src/routes/data/kreditors.js create mode 100644 src/routes/data/months.js create mode 100644 src/routes/data/pdf.js create mode 100644 src/routes/data/transactions.js diff --git a/src/routes/data.js b/src/routes/data.js index bd26153..45d43a1 100644 --- a/src/routes/data.js +++ b/src/routes/data.js @@ -1,1408 +1,10 @@ const express = require('express'); -const fs = require('fs'); -const path = require('path'); -const { authenticateToken } = require('../middleware/auth'); + +// Explicitly require the index file to avoid resolving a non-router object. +const dataRouter = require('./data/index.js'); const router = express.Router(); -// Old CSV parsing removed - now using database-based CSV import +router.use('/', dataRouter); -// Get available months from database -router.get('/months', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - - const query = ` - SELECT DISTINCT - FORMAT(combined.date_col, 'yyyy-MM') as month_year - FROM ( - SELECT buchungsdatum as date_col FROM fibdash.AccountingItems WHERE buchungsdatum IS NOT NULL - UNION ALL - SELECT parsed_date as date_col FROM fibdash.CSVTransactions WHERE parsed_date IS NOT NULL - ) combined - WHERE combined.date_col IS NOT NULL - ORDER BY month_year DESC - `; - - const result = await executeQuery(query); - const months = result.recordset.map(row => row.month_year); - - 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); - const transactions = result.recordset || []; - - // Get PDF documents for each transaction - const pdfQuery = `SELECT kUmsatzBeleg, kZahlungsabgleichUmsatz, textContent, markDown, extraction, datevlink FROM tUmsatzBeleg`; - const pdfResult = await executeQuery(pdfQuery); - - for(const item of pdfResult.recordset){ - for(const transaction of transactions){ - if(item.kZahlungsabgleichUmsatz == transaction.kZahlungsabgleichUmsatz){ - if(!transaction.pdfs) transaction.pdfs = []; - transaction.pdfs.push({ - kUmsatzBeleg: item.kUmsatzBeleg, - content: item.textContent, - markDown: item.markDown, - extraction: item.extraction, - datevlink: item.datevlink - }); - } - } - } - - // Get links for each transaction - const linksQuery = ` - SELECT kZahlungsabgleichUmsatzLink, kZahlungsabgleichUmsatz, linktarget, linktype, note, - tPdfObjekt.kPdfObjekt, tPdfObjekt.textContent, tPdfObjekt.markDown, - tPdfObjekt.extraction - FROM tZahlungsabgleichUmsatzLink - LEFT JOIN tPdfObjekt ON (tZahlungsabgleichUmsatzLink.linktarget = tPdfObjekt.kLieferantenbestellung) - WHERE linktype = 'kLieferantenBestellung' - `; - const linksResult = await executeQuery(linksQuery); - - for(const item of linksResult.recordset){ - for(const transaction of transactions){ - if(item.kZahlungsabgleichUmsatz == transaction.kZahlungsabgleichUmsatz){ - if(!transaction.links) transaction.links = []; - transaction.links.push(item); - } - } - } - - return transactions; - } catch (error) { - console.error('Error fetching JTL transactions:', error); - throw error; // Re-throw the error so the caller knows the database is unavailable - } -}; - -// Get transactions for a specific time period (month, quarter, or year) -router.get('/transactions/:timeRange', authenticateToken, async (req, res) => { - try { - const { timeRange } = req.params; - - // Get CSV transactions with JTL matches AND JTL-only transactions - const { executeQuery } = require('../config/database'); - const query = ` - SELECT - csv.buchungstag as 'Buchungstag', - csv.wertstellung as 'Valutadatum', - csv.umsatzart as 'Buchungstext', - csv.verwendungszweck as 'Verwendungszweck', - csv.beguenstigter_zahlungspflichtiger as 'Beguenstigter/Zahlungspflichtiger', - csv.kontonummer_iban as 'Kontonummer/IBAN', - csv.bic as 'BIC (SWIFT-Code)', - csv.betrag_original as 'Betrag', - csv.waehrung as 'Waehrung', - csv.numeric_amount as numericAmount, - csv.parsed_date, - FORMAT(csv.parsed_date, 'yyyy-MM') as monthYear, - jtl.kZahlungsabgleichUmsatz as jtlId, - CASE WHEN jtl.kZahlungsabgleichUmsatz IS NOT NULL THEN 1 ELSE 0 END as hasJTL, - k.name as kreditor_name, - k.kreditorId as kreditor_id, - k.is_banking as kreditor_is_banking, - bat.assigned_kreditor_id, - ak.name as assigned_kreditor_name, - 0 as isJTLOnly, - 1 as isFromCSV, - ub.textContent as jtl_document_data, - ub.kUmsatzBeleg, - ub.datevlink - FROM fibdash.CSVTransactions csv - LEFT JOIN eazybusiness.dbo.tZahlungsabgleichUmsatz jtl ON ( - ABS(csv.numeric_amount - jtl.fBetrag) < 0.01 AND - ABS(DATEDIFF(day, csv.parsed_date, jtl.dBuchungsdatum)) <= 1 - ) - LEFT JOIN eazybusiness.dbo.tUmsatzBeleg ub ON ub.kZahlungsabgleichUmsatz = jtl.kZahlungsabgleichUmsatz - LEFT JOIN fibdash.Kreditor k ON csv.kontonummer_iban = k.iban - LEFT JOIN fibdash.BankingAccountTransactions bat ON csv.id = bat.csv_transaction_id - LEFT JOIN fibdash.Kreditor ak ON bat.assigned_kreditor_id = ak.id - - UNION ALL - - SELECT - FORMAT(jtl.dBuchungsdatum, 'dd.MM.yy') as 'Buchungstag', - FORMAT(jtl.dBuchungsdatum, 'dd.MM.yy') as 'Valutadatum', - 'JTL Transaction' as 'Buchungstext', - jtl.cVerwendungszweck as 'Verwendungszweck', - jtl.cName as 'Beguenstigter/Zahlungspflichtiger', - '' as 'Kontonummer/IBAN', - '' as 'BIC (SWIFT-Code)', - FORMAT(jtl.fBetrag, 'N2', 'de-DE') as 'Betrag', - '' as 'Waehrung', - jtl.fBetrag as numericAmount, - jtl.dBuchungsdatum as parsed_date, - FORMAT(jtl.dBuchungsdatum, 'yyyy-MM') as monthYear, - jtl.kZahlungsabgleichUmsatz as jtlId, - 1 as hasJTL, - NULL as kreditor_name, - NULL as kreditor_id, - NULL as kreditor_is_banking, - NULL as assigned_kreditor_id, - NULL as assigned_kreditor_name, - 1 as isJTLOnly, - 0 as isFromCSV, - ub.textContent as jtl_document_data, - ub.kUmsatzBeleg, - ub.datevlink - FROM eazybusiness.dbo.tZahlungsabgleichUmsatz jtl - LEFT JOIN eazybusiness.dbo.tUmsatzBeleg ub ON ub.kZahlungsabgleichUmsatz = jtl.kZahlungsabgleichUmsatz - WHERE NOT EXISTS ( - SELECT 1 FROM fibdash.CSVTransactions csv - WHERE ABS(csv.numeric_amount - jtl.fBetrag) < 0.01 - AND ABS(DATEDIFF(day, csv.parsed_date, jtl.dBuchungsdatum)) <= 1 - ) - - ORDER BY parsed_date DESC - `; - - const result = await executeQuery(query); - - // Get links data separately to avoid duplicate rows - const linksQuery = ` - SELECT - zul.kZahlungsabgleichUmsatz, - zul.linktarget, - zul.linktype, - zul.note, - po.kPdfObjekt, - po.textContent, - po.markDown, - po.extraction - FROM eazybusiness.dbo.tZahlungsabgleichUmsatzLink zul - LEFT JOIN eazybusiness.dbo.tPdfObjekt po ON zul.linktarget = po.kLieferantenbestellung - WHERE zul.linktype = 'kLieferantenBestellung' - `; - const linksResult = await executeQuery(linksQuery); - const linksData = linksResult.recordset || []; - - const transactions = result.recordset.map(transaction => ({ - ...transaction, - parsedDate: new Date(transaction.parsed_date), - hasJTL: Boolean(transaction.hasJTL), - isFromCSV: true, - jtlDatabaseAvailable: true, - hasKreditor: !!transaction.kreditor_name, - kreditor: transaction.kreditor_name ? { - name: transaction.kreditor_name, - kreditorId: transaction.kreditor_id, - is_banking: Boolean(transaction.kreditor_is_banking) - } : null, - assigned_kreditor: transaction.assigned_kreditor_name ? { - name: transaction.assigned_kreditor_name, - id: transaction.assigned_kreditor_id - } : null, - pdfs: transaction.jtl_document_data ? [{ - content: transaction.jtl_document_data, - kUmsatzBeleg: transaction.kUmsatzBeleg, - datevlink: transaction.datevlink - }] : [], - links: transaction.jtlId ? linksData.filter(link => - link.kZahlungsabgleichUmsatz === transaction.jtlId - ) : [] - })); - - let filteredTransactions = []; - let periodDescription = ''; - - if (timeRange.includes('-Q')) { - // Quarter format: YYYY-Q1, YYYY-Q2, etc. - const [year, quarterPart] = timeRange.split('-Q'); - const quarter = parseInt(quarterPart); - const startMonth = (quarter - 1) * 3 + 1; - const endMonth = startMonth + 2; - - filteredTransactions = transactions.filter(t => { - if (!t.monthYear) return false; - const [tYear, tMonth] = t.monthYear.split('-'); - const monthNum = parseInt(tMonth); - return tYear === year && monthNum >= startMonth && monthNum <= endMonth; - }); - - periodDescription = `Q${quarter} ${year}`; - } else if (timeRange.length === 4) { - // Year format: YYYY - filteredTransactions = transactions.filter(t => { - if (!t.monthYear) return false; - const [tYear] = t.monthYear.split('-'); - return tYear === timeRange; - }); - - periodDescription = `Jahr ${timeRange}`; - } else { - // Month format: YYYY-MM - filteredTransactions = transactions.filter(t => t.monthYear === timeRange); - const [year, month] = timeRange.split('-'); - const date = new Date(year, month - 1); - periodDescription = date.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }); - } - - const monthTransactions = filteredTransactions - .sort((a, b) => b.parsedDate - a.parsedDate); // Newest first - - // Get JTL transactions for comparison - let jtlTransactions = []; - let jtlDatabaseAvailable = false; - try { - jtlTransactions = await getJTLTransactions(); - jtlDatabaseAvailable = true; - console.log('DEBUG: JTL database connected, found', jtlTransactions.length, 'transactions'); - } catch (error) { - console.log('JTL database not available, continuing without JTL data:', error.message); - jtlDatabaseAvailable = false; - } - - // Filter JTL transactions for the selected time period - let jtlMonthTransactions = []; - - if (timeRange.includes('-Q')) { - const [year, quarterPart] = timeRange.split('-Q'); - const quarter = parseInt(quarterPart); - const startMonth = (quarter - 1) * 3 + 1; - const endMonth = startMonth + 2; - - jtlMonthTransactions = jtlTransactions.filter(jtl => { - const jtlDate = new Date(jtl.dBuchungsdatum); - const jtlMonth = jtlDate.getMonth() + 1; // 0-based to 1-based - return jtlDate.getFullYear() === parseInt(year) && - jtlMonth >= startMonth && jtlMonth <= endMonth; - }); - } else if (timeRange.length === 4) { - jtlMonthTransactions = jtlTransactions.filter(jtl => { - const jtlDate = new Date(jtl.dBuchungsdatum); - return jtlDate.getFullYear() === parseInt(timeRange); - }); - } else { - const [year, month] = timeRange.split('-'); - jtlMonthTransactions = jtlTransactions.filter(jtl => { - const jtlDate = new Date(jtl.dBuchungsdatum); - return jtlDate.getFullYear() === parseInt(year) && - jtlDate.getMonth() === parseInt(month) - 1; - }); - } - - // Get Kreditor information for IBAN lookup - let kreditorData = []; - try { - const { executeQuery } = require('../config/database'); - const kreditorQuery = `SELECT id, iban, name, kreditorId, is_banking FROM fibdash.Kreditor`; - const kreditorResult = await executeQuery(kreditorQuery); - kreditorData = kreditorResult.recordset || []; - } catch (error) { - console.log('Kreditor database not available, continuing without Kreditor data'); - } - - // Add JTL status and Kreditor information to each CSV transaction - const transactionsWithJTL = monthTransactions.map((transaction, index) => { - // Try to match by amount and date (approximate matching) - const amount = transaction.numericAmount; - const transactionDate = transaction.parsedDate; - - // Debug first transaction - if (index === 0) { - console.log('DEBUG First CSV transaction:', { - amount: amount, - transactionDate: transactionDate, - jtlMonthTransactionsCount: jtlMonthTransactions.length - }); - if (jtlMonthTransactions.length > 0) { - console.log('DEBUG First JTL transaction:', { - amount: parseFloat(jtlMonthTransactions[0].fBetrag), - date: new Date(jtlMonthTransactions[0].dBuchungsdatum) - }); - } - } - - 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(); - - // Debug potential matches for first transaction - if (index === 0 && (amountMatch || dateMatch)) { - console.log('DEBUG Potential match for first transaction:', { - csvAmount: amount, - jtlAmount: jtlAmount, - amountMatch: amountMatch, - csvDate: transactionDate, - jtlDate: jtlDate, - dateMatch: dateMatch, - bothMatch: amountMatch && dateMatch - }); - } - - return amountMatch && dateMatch; - }); - - // Look up Kreditor by IBAN - const transactionIban = transaction['Kontonummer/IBAN']; - const kreditorMatch = transactionIban ? kreditorData.find(k => k.iban === transactionIban) : null; - - return { - ...transaction, - hasJTL: jtlDatabaseAvailable ? !!jtlMatch : undefined, - jtlId: jtlMatch ? jtlMatch.kZahlungsabgleichUmsatz : null, - isFromCSV: true, - jtlDatabaseAvailable, - // Include document data from JTL match - pdfs: jtlMatch ? jtlMatch.pdfs || [] : [], - links: jtlMatch ? jtlMatch.links || [] : [], - // Include Kreditor information - kreditor: kreditorMatch ? { - id: kreditorMatch.id, - name: kreditorMatch.name, - kreditorId: kreditorMatch.kreditorId, - iban: kreditorMatch.iban, - is_banking: Boolean(kreditorMatch.is_banking) - } : null, - hasKreditor: !!kreditorMatch - }; - }); - - // 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 || '', - 'Kontonummer/IBAN': '', // JTL transactions don't have IBAN data - 'Betrag': jtl.fBetrag ? jtl.fBetrag.toString().replace('.', ',') : '0,00', - numericAmount: parseFloat(jtl.fBetrag) || 0, - parsedDate: new Date(jtl.dBuchungsdatum), - monthYear: timeRange, - hasJTL: true, - jtlId: jtl.kZahlungsabgleichUmsatz, - isFromCSV: false, - isJTLOnly: true, - // Include document data from JTL transaction - pdfs: jtl.pdfs || [], - links: jtl.links || [], - // JTL transactions don't have IBAN data, so no Kreditor match - kreditor: null, - hasKreditor: false - })); - - // Calculate summary - const summary = { - totalTransactions: filteredTransactions.length, - totalIncome: filteredTransactions - .filter(t => t.numericAmount > 0) - .reduce((sum, t) => sum + t.numericAmount, 0), - totalExpenses: filteredTransactions - .filter(t => t.numericAmount < 0) - .reduce((sum, t) => sum + Math.abs(t.numericAmount), 0), - netAmount: filteredTransactions.reduce((sum, t) => sum + t.numericAmount, 0), - timeRange: timeRange, - jtlDatabaseAvailable: true, - jtlMatches: filteredTransactions.filter(t => t.hasJTL === true && t.isFromCSV).length, - jtlMissing: filteredTransactions.filter(t => t.hasJTL === false && t.isFromCSV).length, - jtlOnly: filteredTransactions.filter(t => t.isJTLOnly === true).length, - csvOnly: filteredTransactions.filter(t => t.hasJTL === false && t.isFromCSV).length - }; - - res.json({ - transactions: filteredTransactions, - summary - }); - } 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/:timeRange', authenticateToken, async (req, res) => { - try { - const { timeRange } = req.params; - - // TODO: Update to use database queries instead of CSV file - res.status(501).json({ error: 'DATEV export temporarily disabled - use database-based queries' }); - return; - let filteredTransactions = []; - let periodStart, periodEnd, filename; - - if (timeRange.includes('-Q')) { - // Quarter format: YYYY-Q1, YYYY-Q2, etc. - const [year, quarterPart] = timeRange.split('-Q'); - const quarter = parseInt(quarterPart); - const startMonth = (quarter - 1) * 3 + 1; - const endMonth = startMonth + 2; - - filteredTransactions = transactions.filter(t => { - if (!t.monthYear) return false; - const [tYear, tMonth] = t.monthYear.split('-'); - const monthNum = parseInt(tMonth); - return tYear === year && monthNum >= startMonth && monthNum <= endMonth; - }); - - periodStart = `${year}${startMonth.toString().padStart(2, '0')}01`; - periodEnd = new Date(year, endMonth, 0).toISOString().slice(0, 10).replace(/-/g, ''); - filename = `DATEV_${year}_Q${quarter}.csv`; - } else if (timeRange.length === 4) { - // Year format: YYYY - filteredTransactions = transactions.filter(t => { - if (!t.monthYear) return false; - const [tYear] = t.monthYear.split('-'); - return tYear === timeRange; - }); - - periodStart = `${timeRange}0101`; - periodEnd = `${timeRange}1231`; - filename = `DATEV_${timeRange}.csv`; - } else { - // Month format: YYYY-MM - const [year, month] = timeRange.split('-'); - filteredTransactions = transactions.filter(t => t.monthYear === timeRange); - - periodStart = `${year}${month.padStart(2, '0')}01`; - periodEnd = new Date(year, month, 0).toISOString().slice(0, 10).replace(/-/g, ''); - filename = `DATEV_${year}_${month.padStart(2, '0')}.csv`; - } - - const monthTransactions = filteredTransactions - .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 time period' }); - } - - // Build DATEV format - 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 - 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' }); - } -}); - -// Get PDF from tUmsatzBeleg -router.get('/pdf/umsatzbeleg/:kUmsatzBeleg', authenticateToken, async (req, res) => { - try { - const { kUmsatzBeleg } = req.params; - const { executeQuery } = require('../config/database'); - - const query = ` - SELECT content, datevlink - FROM dbo.tUmsatzBeleg - WHERE kUmsatzBeleg = @kUmsatzBeleg AND content IS NOT NULL - `; - - const result = await executeQuery(query, { - kUmsatzBeleg: parseInt(kUmsatzBeleg) - }); - - if (!result.recordset || result.recordset.length === 0) { - return res.status(404).json({ error: 'PDF not found' }); - } - - const pdfData = result.recordset[0]; - const filename = `Umsatzbeleg_${kUmsatzBeleg}_${pdfData.datevlink || 'document'}.pdf`; - - res.setHeader('Content-Type', 'application/pdf'); - res.setHeader('Content-Disposition', `inline; filename="${filename}"`); - res.send(pdfData.content); - - } catch (error) { - console.error('Error fetching PDF from tUmsatzBeleg:', error); - res.status(500).json({ error: 'Failed to fetch PDF' }); - } -}); - -// Get PDF from tPdfObjekt -router.get('/pdf/pdfobject/:kPdfObjekt', authenticateToken, async (req, res) => { - try { - const { kPdfObjekt } = req.params; - const { executeQuery } = require('../config/database'); - - const query = ` - SELECT content, datevlink, kLieferantenbestellung - FROM dbo.tPdfObjekt - WHERE kPdfObjekt = @kPdfObjekt AND content IS NOT NULL - `; - - const result = await executeQuery(query, { - kPdfObjekt: parseInt(kPdfObjekt) - }); - - if (!result.recordset || result.recordset.length === 0) { - return res.status(404).json({ error: 'PDF not found' }); - } - - const pdfData = result.recordset[0]; - const filename = `PdfObjekt_${kPdfObjekt}_LB${pdfData.kLieferantenbestellung}_${pdfData.datevlink || 'document'}.pdf`; - - res.setHeader('Content-Type', 'application/pdf'); - res.setHeader('Content-Disposition', `inline; filename="${filename}"`); - res.send(pdfData.content); - - } catch (error) { - console.error('Error fetching PDF from tPdfObjekt:', error); - res.status(500).json({ error: 'Failed to fetch PDF' }); - } -}); - -// Kreditor API endpoints - -// Get all kreditors -router.get('/kreditors', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const query = ` - SELECT id, iban, name, kreditorId - FROM fibdash.Kreditor - ORDER BY name ASC, iban ASC - `; - - const result = await executeQuery(query); - res.json(result.recordset || []); - } catch (error) { - console.error('Error fetching kreditors:', error); - res.status(500).json({ error: 'Failed to fetch kreditors' }); - } -}); - -// Get kreditor by ID -router.get('/kreditors/:id', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { id } = req.params; - - const query = ` - SELECT id, iban, name, kreditorId, is_banking - FROM fibdash.Kreditor - WHERE id = @id - `; - - const result = await executeQuery(query, { id: parseInt(id) }); - - if (result.recordset.length === 0) { - return res.status(404).json({ error: 'Kreditor not found' }); - } - - res.json(result.recordset[0]); - } catch (error) { - console.error('Error fetching kreditor:', error); - res.status(500).json({ error: 'Failed to fetch kreditor' }); - } -}); - -// Create new kreditor -router.post('/kreditors', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { iban, name, kreditorId, is_banking } = req.body; - - // IBAN is optional for banking accounts or manual kreditor assignments - const isBanking = is_banking || false; - - // Validate required fields - if (!name || !kreditorId) { - return res.status(400).json({ error: 'Name and kreditorId are required' }); - } - - // IBAN validation - required for non-banking accounts - if (!isBanking && (!iban || iban.trim() === '')) { - return res.status(400).json({ error: 'IBAN is required (except for banking accounts)' }); - } - - // Check if IBAN already exists (only if IBAN is provided) - if (iban && iban.trim() !== '') { - const checkQuery = ` - SELECT id FROM fibdash.Kreditor - WHERE iban = @iban - `; - - const checkResult = await executeQuery(checkQuery, { iban }); - - if (checkResult.recordset.length > 0) { - return res.status(409).json({ error: 'Kreditor with this IBAN already exists' }); - } - } - - const insertQuery = ` - INSERT INTO fibdash.Kreditor (iban, name, kreditorId, is_banking) - OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.is_banking - VALUES (@iban, @name, @kreditorId, @is_banking) - `; - - const result = await executeQuery(insertQuery, { - iban: iban || null, - name, - kreditorId, - is_banking: isBanking - }); - - res.status(201).json(result.recordset[0]); - } catch (error) { - console.error('Error creating kreditor:', error); - res.status(500).json({ error: 'Failed to create kreditor' }); - } -}); - -// Update kreditor -router.put('/kreditors/:id', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { id } = req.params; - const { iban, name, kreditorId, is_banking } = req.body; - - // IBAN is optional for banking accounts or manual kreditor assignments - const isBanking = is_banking || false; - - // Validate required fields - if (!name || !kreditorId) { - return res.status(400).json({ error: 'Name and kreditorId are required' }); - } - - // IBAN validation - required for non-banking accounts - if (!isBanking && (!iban || iban.trim() === '')) { - return res.status(400).json({ error: 'IBAN is required (except for banking accounts)' }); - } - - // Check if kreditor exists - const checkQuery = `SELECT id FROM fibdash.Kreditor WHERE id = @id`; - const checkResult = await executeQuery(checkQuery, { id: parseInt(id) }); - - if (checkResult.recordset.length === 0) { - return res.status(404).json({ error: 'Kreditor not found' }); - } - - // Check for conflicts with other kreditors (only if IBAN is provided) - if (iban && iban.trim() !== '') { - const conflictQuery = ` - SELECT id FROM fibdash.Kreditor - WHERE iban = @iban AND id != @id - `; - - const conflictResult = await executeQuery(conflictQuery, { iban, id: parseInt(id) }); - - if (conflictResult.recordset.length > 0) { - return res.status(409).json({ error: 'Another kreditor with this IBAN already exists' }); - } - } - - const updateQuery = ` - UPDATE fibdash.Kreditor - SET iban = @iban, name = @name, kreditorId = @kreditorId, is_banking = @is_banking - OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.is_banking - WHERE id = @id - `; - - const result = await executeQuery(updateQuery, { - iban: iban || null, - name, - kreditorId, - is_banking: isBanking, - id: parseInt(id) - }); - - res.json(result.recordset[0]); - } catch (error) { - console.error('Error updating kreditor:', error); - res.status(500).json({ error: 'Failed to update kreditor' }); - } -}); - -// Delete kreditor (hard delete) -router.delete('/kreditors/:id', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { id } = req.params; - - const query = ` - DELETE FROM fibdash.Kreditor - WHERE id = @id - `; - - const result = await executeQuery(query, { id: parseInt(id) }); - - if (result.rowsAffected[0] === 0) { - return res.status(404).json({ error: 'Kreditor not found' }); - } - - res.json({ message: 'Kreditor deleted successfully' }); - } catch (error) { - console.error('Error deleting kreditor:', error); - res.status(500).json({ error: 'Failed to delete kreditor' }); - } -}); - -// Banking Account Transactions endpoints - -// Get banking account transactions for a specific transaction -router.get('/banking-transactions/:transactionId', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { transactionId } = req.params; - - const query = ` - SELECT - bat.*, - k.name as assigned_kreditor_name, - k.kreditorId as assigned_kreditor_id_code - FROM fibdash.BankingAccountTransactions bat - LEFT JOIN fibdash.Kreditor k ON bat.assigned_kreditor_id = k.id - WHERE bat.transaction_id = @transactionId OR bat.csv_transaction_id = @transactionId - `; - - const result = await executeQuery(query, { transactionId: parseInt(transactionId) }); - - res.json(result.recordset); - } catch (error) { - console.error('Error fetching banking account transactions:', error); - res.status(500).json({ error: 'Failed to fetch banking account transactions' }); - } -}); - -// Create banking account transaction assignment -router.post('/banking-transactions', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { transaction_id, csv_transaction_id, banking_iban, assigned_kreditor_id, notes, assigned_by } = req.body; - - // Validate required fields - need either transaction_id or csv_transaction_id - if ((!transaction_id && !csv_transaction_id) || !banking_iban || !assigned_kreditor_id) { - return res.status(400).json({ - error: 'Transaction ID (or CSV Transaction ID), banking IBAN, and assigned kreditor ID are required' - }); - } - - // Check if assignment already exists - const checkQuery = ` - SELECT id FROM fibdash.BankingAccountTransactions - WHERE transaction_id = @transaction_id OR csv_transaction_id = @csv_transaction_id - `; - - const checkResult = await executeQuery(checkQuery, { - transaction_id: transaction_id || null, - csv_transaction_id: csv_transaction_id || null - }); - - if (checkResult.recordset.length > 0) { - return res.status(409).json({ error: 'Banking transaction assignment already exists' }); - } - - const insertQuery = ` - INSERT INTO fibdash.BankingAccountTransactions - (transaction_id, csv_transaction_id, banking_iban, assigned_kreditor_id, notes, assigned_by) - OUTPUT INSERTED.* - VALUES (@transaction_id, @csv_transaction_id, @banking_iban, @assigned_kreditor_id, @notes, @assigned_by) - `; - - const result = await executeQuery(insertQuery, { - transaction_id: transaction_id || null, - csv_transaction_id: csv_transaction_id || null, - banking_iban, - assigned_kreditor_id, - notes: notes || null, - assigned_by: assigned_by || null - }); - - res.status(201).json(result.recordset[0]); - } catch (error) { - console.error('Error creating banking account transaction:', error); - res.status(500).json({ error: 'Failed to create banking account transaction' }); - } -}); - -// Update banking account transaction assignment -router.put('/banking-transactions/:id', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { id } = req.params; - const { assigned_kreditor_id, notes, assigned_by } = req.body; - - // Validate required fields - if (!assigned_kreditor_id) { - return res.status(400).json({ error: 'Assigned kreditor ID is required' }); - } - - const updateQuery = ` - UPDATE fibdash.BankingAccountTransactions - SET assigned_kreditor_id = @assigned_kreditor_id, - notes = @notes, - assigned_by = @assigned_by, - assigned_date = GETDATE() - OUTPUT INSERTED.* - WHERE id = @id - `; - - const result = await executeQuery(updateQuery, { - assigned_kreditor_id, - notes: notes || null, - assigned_by: assigned_by || null, - id: parseInt(id) - }); - - if (result.recordset.length === 0) { - return res.status(404).json({ error: 'Banking transaction assignment not found' }); - } - - res.json(result.recordset[0]); - } catch (error) { - console.error('Error updating banking account transaction:', error); - res.status(500).json({ error: 'Failed to update banking account transaction' }); - } -}); - -// Delete banking account transaction assignment -router.delete('/banking-transactions/:id', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { id } = req.params; - - const deleteQuery = ` - DELETE FROM fibdash.BankingAccountTransactions - WHERE id = @id - `; - - const result = await executeQuery(deleteQuery, { id: parseInt(id) }); - - res.json({ message: 'Banking transaction assignment deleted successfully' }); - } catch (error) { - console.error('Error deleting banking account transaction:', error); - res.status(500).json({ error: 'Failed to delete banking account transaction' }); - } -}); - -// Get all kreditors that can be assigned to banking transactions (non-banking kreditors) -router.get('/assignable-kreditors', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - - const query = ` - SELECT id, name, kreditorId - FROM fibdash.Kreditor - WHERE is_banking = 0 - ORDER BY name - `; - - const result = await executeQuery(query); - - res.json(result.recordset); - } catch (error) { - console.error('Error fetching assignable kreditors:', error); - res.status(500).json({ error: 'Failed to fetch assignable kreditors' }); - } -}); - -// CSV Import endpoints - -// Test CSV import endpoint (no auth for testing) - ACTUALLY IMPORTS TO DATABASE -router.post('/test-csv-import', async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { transactions, filename, batchId, headers } = req.body; - - if (!transactions || !Array.isArray(transactions)) { - return res.status(400).json({ error: 'Transactions array is required' }); - } - - const importBatchId = batchId || `test_import_${Date.now()}`; - let successCount = 0; - let errorCount = 0; - const errors = []; - - for (let i = 0; i < transactions.length; i++) { - const transaction = transactions[i]; - - try { - // Validate required fields - const validationErrors = []; - - if (!transaction['Buchungstag'] || transaction['Buchungstag'].trim() === '') { - validationErrors.push('Buchungstag is required'); - } - - if (!transaction['Betrag'] || transaction['Betrag'].toString().trim() === '') { - validationErrors.push('Betrag is required'); - } - - if (validationErrors.length > 0) { - errors.push({ - row: i + 1, - error: `Validation failed: ${validationErrors.join(', ')}`, - transaction: transaction - }); - errorCount++; - continue; - } - - // Parse the date - let parsedDate = null; - if (transaction['Buchungstag']) { - const dateStr = transaction['Buchungstag'].trim(); - const dateParts = dateStr.split(/[.\/\-]/); - if (dateParts.length === 3) { - const day = parseInt(dateParts[0]); - const month = parseInt(dateParts[1]) - 1; - let year = parseInt(dateParts[2]); - - if (year < 100) { - year += (year < 50) ? 2000 : 1900; - } - - parsedDate = new Date(year, month, day); - - if (isNaN(parsedDate.getTime())) { - parsedDate = null; - validationErrors.push(`Invalid date format: ${dateStr}`); - } - } - } - - // Parse the amount - let numericAmount = 0; - if (transaction['Betrag']) { - const amountStr = transaction['Betrag'].toString().replace(/[^\d,.-]/g, ''); - const normalizedAmount = amountStr.replace(',', '.'); - numericAmount = parseFloat(normalizedAmount) || 0; - } - - const insertQuery = ` - INSERT INTO fibdash.CSVTransactions - (buchungstag, wertstellung, umsatzart, betrag, betrag_original, waehrung, - beguenstigter_zahlungspflichtiger, kontonummer_iban, bic, verwendungszweck, - parsed_date, numeric_amount, import_batch_id, source_filename, source_row_number) - VALUES - (@buchungstag, @wertstellung, @umsatzart, @betrag, @betrag_original, @waehrung, - @beguenstigter_zahlungspflichtiger, @kontonummer_iban, @bic, @verwendungszweck, - @parsed_date, @numeric_amount, @import_batch_id, @source_filename, @source_row_number) - `; - - await executeQuery(insertQuery, { - buchungstag: transaction['Buchungstag'] || null, - wertstellung: transaction['Valutadatum'] || null, - umsatzart: transaction['Buchungstext'] || null, - betrag: numericAmount, - betrag_original: transaction['Betrag'] || null, - waehrung: transaction['Waehrung'] || null, - beguenstigter_zahlungspflichtiger: transaction['Beguenstigter/Zahlungspflichtiger'] || null, - kontonummer_iban: transaction['Kontonummer/IBAN'] || null, - bic: transaction['BIC (SWIFT-Code)'] || null, - verwendungszweck: transaction['Verwendungszweck'] || null, - parsed_date: parsedDate, - numeric_amount: numericAmount, - import_batch_id: importBatchId, - source_filename: filename || 'test_import', - source_row_number: i + 1 - }); - - successCount++; - } catch (error) { - console.error(`Error importing transaction ${i + 1}:`, error); - errors.push({ - row: i + 1, - error: error.message, - transaction: transaction - }); - errorCount++; - } - } - - res.json({ - success: true, - batchId: importBatchId, - imported: successCount, - errors: errorCount, - details: errors.length > 0 ? errors : undefined, - paypalTransaction: transactions.find(t => t['Kontonummer/IBAN'] === 'LU89751000135104200E') - }); - - } catch (error) { - console.error('Test import error:', error); - res.status(500).json({ error: 'Test import failed' }); - } -}); - -// Import CSV transactions to database -router.post('/import-csv-transactions', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { transactions, filename, batchId, headers } = req.body; - - if (!transactions || !Array.isArray(transactions)) { - return res.status(400).json({ error: 'Transactions array is required' }); - } - - // Expected CSV headers (German bank format) - const expectedHeaders = [ - 'Auftragskonto', - 'Buchungstag', - 'Valutadatum', - 'Buchungstext', - 'Verwendungszweck', - 'Glaeubiger ID', - 'Mandatsreferenz', - 'Kundenreferenz (End-to-End)', - 'Sammlerreferenz', - 'Lastschrift Ursprungsbetrag', - 'Auslagenersatz Ruecklastschrift', - 'Beguenstigter/Zahlungspflichtiger', - 'Kontonummer/IBAN', - 'BIC (SWIFT-Code)', - 'Betrag', - 'Waehrung', - 'Info' - ]; - - // Validate headers if provided - if (headers && Array.isArray(headers)) { - const missingHeaders = expectedHeaders.filter(expected => - !headers.some(header => header.trim() === expected) - ); - - if (missingHeaders.length > 0) { - return res.status(400).json({ - error: 'Invalid CSV format - missing required headers', - missing: missingHeaders, - expected: expectedHeaders, - received: headers - }); - } - } - - // Validate that we have transactions - if (transactions.length === 0) { - return res.status(400).json({ error: 'No transaction data found' }); - } - - const importBatchId = batchId || `import_${Date.now()}`; - let successCount = 0; - let errorCount = 0; - const errors = []; - - for (let i = 0; i < transactions.length; i++) { - const transaction = transactions[i]; - - try { - // Validate required fields for each transaction - const validationErrors = []; - - if (!transaction['Buchungstag'] || transaction['Buchungstag'].trim() === '') { - validationErrors.push('Buchungstag is required'); - } - - if (!transaction['Betrag'] || transaction['Betrag'].toString().trim() === '') { - validationErrors.push('Betrag is required'); - } - - if (!transaction['Beguenstigter/Zahlungspflichtiger'] || transaction['Beguenstigter/Zahlungspflichtiger'].trim() === '') { - validationErrors.push('Beguenstigter/Zahlungspflichtiger is required'); - } - - // Skip rows that are clearly invalid (like headers or empty rows) - if (validationErrors.length > 2) { - console.log(`Skipping invalid row ${i + 1}:`, validationErrors); - continue; - } - - if (validationErrors.length > 0) { - errors.push({ - row: i + 1, - error: `Validation failed: ${validationErrors.join(', ')}`, - transaction: transaction - }); - errorCount++; - continue; - } - // Parse the date - let parsedDate = null; - if (transaction['Buchungstag']) { - const dateStr = transaction['Buchungstag'].trim(); - // Try different date formats (DD.MM.YY, DD.MM.YYYY, DD/MM/YYYY, etc.) - const dateParts = dateStr.split(/[.\/\-]/); - if (dateParts.length === 3) { - const day = parseInt(dateParts[0]); - const month = parseInt(dateParts[1]) - 1; // JavaScript months are 0-based - let year = parseInt(dateParts[2]); - - // Handle 2-digit years (assume 21.07.25 means 2025) - if (year < 100) { - year += (year < 50) ? 2000 : 1900; // 00-49 = 2000-2049, 50-99 = 1950-1999 - } - - parsedDate = new Date(year, month, day); - - // Validate the date - if (isNaN(parsedDate.getTime()) || - parsedDate.getDate() !== day || - parsedDate.getMonth() !== month || - parsedDate.getFullYear() !== year) { - parsedDate = null; - validationErrors.push(`Invalid date format: ${dateStr}`); - } - } else { - validationErrors.push(`Invalid date format: ${dateStr}`); - } - } - - // Parse the amount - let numericAmount = 0; - if (transaction['Betrag']) { - const amountStr = transaction['Betrag'].toString().replace(/[^\d,.-]/g, ''); - const normalizedAmount = amountStr.replace(',', '.'); - numericAmount = parseFloat(normalizedAmount) || 0; - } - - const insertQuery = ` - INSERT INTO fibdash.CSVTransactions - (buchungstag, wertstellung, umsatzart, betrag, betrag_original, waehrung, - beguenstigter_zahlungspflichtiger, kontonummer_iban, bic, verwendungszweck, - parsed_date, numeric_amount, import_batch_id, source_filename, source_row_number) - VALUES - (@buchungstag, @wertstellung, @umsatzart, @betrag, @betrag_original, @waehrung, - @beguenstigter_zahlungspflichtiger, @kontonummer_iban, @bic, @verwendungszweck, - @parsed_date, @numeric_amount, @import_batch_id, @source_filename, @source_row_number) - `; - - await executeQuery(insertQuery, { - buchungstag: transaction['Buchungstag'] || null, - wertstellung: transaction['Valutadatum'] || null, - umsatzart: transaction['Buchungstext'] || null, - betrag: numericAmount, - betrag_original: transaction['Betrag'] || null, - waehrung: transaction['Waehrung'] || null, - beguenstigter_zahlungspflichtiger: transaction['Beguenstigter/Zahlungspflichtiger'] || null, - kontonummer_iban: transaction['Kontonummer/IBAN'] || null, - bic: transaction['BIC (SWIFT-Code)'] || null, - verwendungszweck: transaction['Verwendungszweck'] || null, - parsed_date: parsedDate, - numeric_amount: numericAmount, - import_batch_id: importBatchId, - source_filename: filename || null, - source_row_number: i + 1 - }); - - successCount++; - } catch (error) { - console.error(`Error importing transaction ${i + 1}:`, error); - errors.push({ - row: i + 1, - error: error.message, - transaction: transaction - }); - errorCount++; - } - } - - res.json({ - success: true, - batchId: importBatchId, - imported: successCount, - errors: errorCount, - details: errors.length > 0 ? errors : undefined - }); - - } catch (error) { - console.error('Error importing CSV transactions:', error); - res.status(500).json({ error: 'Failed to import CSV transactions' }); - } -}); - -// Get imported CSV transactions -router.get('/csv-transactions', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - const { batchId, limit = 100, offset = 0 } = req.query; - - let query = ` - SELECT - csv.*, - k.name as kreditor_name, - k.kreditorId as kreditor_id, - k.is_banking as kreditor_is_banking, - bat.assigned_kreditor_id, - ak.name as assigned_kreditor_name - FROM fibdash.CSVTransactions csv - LEFT JOIN fibdash.Kreditor k ON csv.kontonummer_iban = k.iban - LEFT JOIN fibdash.BankingAccountTransactions bat ON csv.id = bat.csv_transaction_id - LEFT JOIN fibdash.Kreditor ak ON bat.assigned_kreditor_id = ak.id - `; - - const params = {}; - - if (batchId) { - query += ' WHERE csv.import_batch_id = @batchId'; - params.batchId = batchId; - } - - query += ' ORDER BY csv.parsed_date DESC, csv.id DESC'; - query += ' OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY'; - - params.offset = parseInt(offset); - params.limit = parseInt(limit); - - const result = await executeQuery(query, params); - - res.json(result.recordset); - } catch (error) { - console.error('Error fetching CSV transactions:', error); - res.status(500).json({ error: 'Failed to fetch CSV transactions' }); - } -}); - -// Get CSV import batches -router.get('/csv-import-batches', authenticateToken, async (req, res) => { - try { - const { executeQuery } = require('../config/database'); - - const query = ` - SELECT - import_batch_id, - source_filename, - MIN(import_date) as import_date, - COUNT(*) as transaction_count, - SUM(CASE WHEN is_processed = 1 THEN 1 ELSE 0 END) as processed_count - FROM fibdash.CSVTransactions - GROUP BY import_batch_id, source_filename - ORDER BY MIN(import_date) DESC - `; - - const result = await executeQuery(query); - - res.json(result.recordset); - } catch (error) { - console.error('Error fetching import batches:', error); - res.status(500).json({ error: 'Failed to fetch import batches' }); - } -}); - -module.exports = router; \ No newline at end of file +module.exports = router; \ No newline at end of file diff --git a/src/routes/data/bankingTransactions.js b/src/routes/data/bankingTransactions.js new file mode 100644 index 0000000..20460b8 --- /dev/null +++ b/src/routes/data/bankingTransactions.js @@ -0,0 +1,160 @@ +const express = require('express'); +const { authenticateToken } = require('../../middleware/auth'); + +const router = express.Router(); + +// Get banking account transactions for a specific transaction +router.get('/banking-transactions/:transactionId', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { transactionId } = req.params; + + const query = ` + SELECT + bat.*, + k.name as assigned_kreditor_name, + k.kreditorId as assigned_kreditor_id_code + FROM fibdash.BankingAccountTransactions bat + LEFT JOIN fibdash.Kreditor k ON bat.assigned_kreditor_id = k.id + WHERE bat.transaction_id = @transactionId OR bat.csv_transaction_id = @transactionId + `; + + const result = await executeQuery(query, { transactionId: parseInt(transactionId, 10) }); + + res.json(result.recordset); + } catch (error) { + console.error('Error fetching banking account transactions:', error); + res.status(500).json({ error: 'Failed to fetch banking account transactions' }); + } +}); + +// Create banking account transaction assignment +router.post('/banking-transactions', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { transaction_id, csv_transaction_id, banking_iban, assigned_kreditor_id, notes, assigned_by } = req.body; + + if ((!transaction_id && !csv_transaction_id) || !banking_iban || !assigned_kreditor_id) { + return res.status(400).json({ + error: 'Transaction ID (or CSV Transaction ID), banking IBAN, and assigned kreditor ID are required' + }); + } + + const checkQuery = ` + SELECT id FROM fibdash.BankingAccountTransactions + WHERE transaction_id = @transaction_id OR csv_transaction_id = @csv_transaction_id + `; + + const checkResult = await executeQuery(checkQuery, { + transaction_id: transaction_id || null, + csv_transaction_id: csv_transaction_id || null + }); + + if (checkResult.recordset.length > 0) { + return res.status(409).json({ error: 'Banking transaction assignment already exists' }); + } + + const insertQuery = ` + INSERT INTO fibdash.BankingAccountTransactions + (transaction_id, csv_transaction_id, banking_iban, assigned_kreditor_id, notes, assigned_by) + OUTPUT INSERTED.* + VALUES (@transaction_id, @csv_transaction_id, @banking_iban, @assigned_kreditor_id, @notes, @assigned_by) + `; + + const result = await executeQuery(insertQuery, { + transaction_id: transaction_id || null, + csv_transaction_id: csv_transaction_id || null, + banking_iban, + assigned_kreditor_id, + notes: notes || null, + assigned_by: assigned_by || null + }); + + res.status(201).json(result.recordset[0]); + } catch (error) { + console.error('Error creating banking account transaction:', error); + res.status(500).json({ error: 'Failed to create banking account transaction' }); + } +}); + +// Update banking account transaction assignment +router.put('/banking-transactions/:id', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { id } = req.params; + const { assigned_kreditor_id, notes, assigned_by } = req.body; + + if (!assigned_kreditor_id) { + return res.status(400).json({ error: 'Assigned kreditor ID is required' }); + } + + const updateQuery = ` + UPDATE fibdash.BankingAccountTransactions + SET assigned_kreditor_id = @assigned_kreditor_id, + notes = @notes, + assigned_by = @assigned_by, + assigned_date = GETDATE() + OUTPUT INSERTED.* + WHERE id = @id + `; + + const result = await executeQuery(updateQuery, { + assigned_kreditor_id, + notes: notes || null, + assigned_by: assigned_by || null, + id: parseInt(id, 10) + }); + + if (result.recordset.length === 0) { + return res.status(404).json({ error: 'Banking transaction assignment not found' }); + } + + res.json(result.recordset[0]); + } catch (error) { + console.error('Error updating banking account transaction:', error); + res.status(500).json({ error: 'Failed to update banking account transaction' }); + } +}); + +// Delete banking account transaction assignment +router.delete('/banking-transactions/:id', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { id } = req.params; + + const deleteQuery = ` + DELETE FROM fibdash.BankingAccountTransactions + WHERE id = @id + `; + + await executeQuery(deleteQuery, { id: parseInt(id, 10) }); + + res.json({ message: 'Banking transaction assignment deleted successfully' }); + } catch (error) { + console.error('Error deleting banking account transaction:', error); + res.status(500).json({ error: 'Failed to delete banking account transaction' }); + } +}); + +// Get all kreditors that can be assigned to banking transactions (non-banking kreditors) +router.get('/assignable-kreditors', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + + const query = ` + SELECT id, name, kreditorId + FROM fibdash.Kreditor + WHERE is_banking = 0 + ORDER BY name + `; + + const result = await executeQuery(query); + + res.json(result.recordset); + } catch (error) { + console.error('Error fetching assignable kreditors:', error); + res.status(500).json({ error: 'Failed to fetch assignable kreditors' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/data/csvImport.js b/src/routes/data/csvImport.js new file mode 100644 index 0000000..be8f4ca --- /dev/null +++ b/src/routes/data/csvImport.js @@ -0,0 +1,373 @@ +const express = require('express'); +const { authenticateToken } = require('../../middleware/auth'); + +const router = express.Router(); + +// Test CSV import endpoint (no auth for testing) - ACTUALLY IMPORTS TO DATABASE +router.post('/test-csv-import', async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { transactions, filename, batchId } = req.body; + + if (!transactions || !Array.isArray(transactions)) { + return res.status(400).json({ error: 'Transactions array is required' }); + } + + const importBatchId = batchId || 'test_import_' + Date.now(); + let successCount = 0; + let errorCount = 0; + const errors = []; + + for (let i = 0; i < transactions.length; i++) { + const transaction = transactions[i]; + + try { + const validationErrors = []; + + if (!transaction['Buchungstag'] || transaction['Buchungstag'].trim() === '') { + validationErrors.push('Buchungstag is required'); + } + + if (!transaction['Betrag'] || transaction['Betrag'].toString().trim() === '') { + validationErrors.push('Betrag is required'); + } + + if (validationErrors.length > 0) { + errors.push({ + row: i + 1, + error: 'Validation failed: ' + validationErrors.join(', '), + transaction: transaction + }); + errorCount++; + continue; + } + + let parsedDate = null; + if (transaction['Buchungstag']) { + const dateStr = transaction['Buchungstag'].trim(); + const dateParts = dateStr.split(/[.\/\-]/); + if (dateParts.length === 3) { + const day = parseInt(dateParts[0], 10); + const month = parseInt(dateParts[1], 10) - 1; + let year = parseInt(dateParts[2], 10); + + if (year < 100) { + year += (year < 50) ? 2000 : 1900; + } + + parsedDate = new Date(year, month, day); + + if (isNaN(parsedDate.getTime())) { + parsedDate = null; + validationErrors.push('Invalid date format: ' + dateStr); + } + } + } + + let numericAmount = 0; + if (transaction['Betrag']) { + const amountStr = transaction['Betrag'].toString().replace(/[^\d,.-]/g, ''); + const normalizedAmount = amountStr.replace(',', '.'); + numericAmount = parseFloat(normalizedAmount) || 0; + } + + const insertQuery = ` + INSERT INTO fibdash.CSVTransactions + (buchungstag, wertstellung, umsatzart, betrag, betrag_original, waehrung, + beguenstigter_zahlungspflichtiger, kontonummer_iban, bic, verwendungszweck, + parsed_date, numeric_amount, import_batch_id, source_filename, source_row_number) + VALUES + (@buchungstag, @wertstellung, @umsatzart, @betrag, @betrag_original, @waehrung, + @beguenstigter_zahlungspflichtiger, @kontonummer_iban, @bic, @verwendungszweck, + @parsed_date, @numeric_amount, @import_batch_id, @source_filename, @source_row_number) + `; + + await executeQuery(insertQuery, { + buchungstag: transaction['Buchungstag'] || null, + wertstellung: transaction['Valutadatum'] || null, + umsatzart: transaction['Buchungstext'] || null, + betrag: numericAmount, + betrag_original: transaction['Betrag'] || null, + waehrung: transaction['Waehrung'] || null, + beguenstigter_zahlungspflichtiger: transaction['Beguenstigter/Zahlungspflichtiger'] || null, + kontonummer_iban: transaction['Kontonummer/IBAN'] || null, + bic: transaction['BIC (SWIFT-Code)'] || null, + verwendungszweck: transaction['Verwendungszweck'] || null, + parsed_date: parsedDate, + numeric_amount: numericAmount, + import_batch_id: importBatchId, + source_filename: filename || 'test_import', + source_row_number: i + 1 + }); + + successCount++; + } catch (error) { + console.error('Error importing transaction ' + (i + 1) + ':', error); + errors.push({ + row: i + 1, + error: error.message, + transaction: transaction + }); + errorCount++; + } + } + + res.json({ + success: true, + batchId: importBatchId, + imported: successCount, + errors: errorCount, + details: errors.length > 0 ? errors : undefined, + paypalTransaction: transactions.find(t => t['Kontonummer/IBAN'] === 'LU89751000135104200E') + }); + + } catch (error) { + console.error('Test import error:', error); + res.status(500).json({ error: 'Test import failed' }); + } +}); + +// Import CSV transactions to database +router.post('/import-csv-transactions', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { transactions, filename, batchId, headers } = req.body; + + if (!transactions || !Array.isArray(transactions)) { + return res.status(400).json({ error: 'Transactions array is required' }); + } + + const expectedHeaders = [ + 'Auftragskonto', + 'Buchungstag', + 'Valutadatum', + 'Buchungstext', + 'Verwendungszweck', + 'Glaeubiger ID', + 'Mandatsreferenz', + 'Kundenreferenz (End-to-End)', + 'Sammlerreferenz', + 'Lastschrift Ursprungsbetrag', + 'Auslagenersatz Ruecklastschrift', + 'Beguenstigter/Zahlungspflichtiger', + 'Kontonummer/IBAN', + 'BIC (SWIFT-Code)', + 'Betrag', + 'Waehrung', + 'Info' + ]; + + if (headers && Array.isArray(headers)) { + const missingHeaders = expectedHeaders.filter(expected => + !headers.some(header => header.trim() === expected) + ); + + if (missingHeaders.length > 0) { + return res.status(400).json({ + error: 'Invalid CSV format - missing required headers', + missing: missingHeaders, + expected: expectedHeaders, + received: headers + }); + } + } + + if (transactions.length === 0) { + return res.status(400).json({ error: 'No transaction data found' }); + } + + const importBatchId = batchId || 'import_' + Date.now(); + let successCount = 0; + let errorCount = 0; + const errors = []; + + for (let i = 0; i < transactions.length; i++) { + const transaction = transactions[i]; + + try { + const validationErrors = []; + + if (!transaction['Buchungstag'] || transaction['Buchungstag'].trim() === '') { + validationErrors.push('Buchungstag is required'); + } + + if (!transaction['Betrag'] || transaction['Betrag'].toString().trim() === '') { + validationErrors.push('Betrag is required'); + } + + if (!transaction['Beguenstigter/Zahlungspflichtiger'] || transaction['Beguenstigter/Zahlungspflichtiger'].trim() === '') { + validationErrors.push('Beguenstigter/Zahlungspflichtiger is required'); + } + + if (validationErrors.length > 2) { + console.log('Skipping invalid row ' + (i + 1) + ':', validationErrors); + continue; + } + + if (validationErrors.length > 0) { + errors.push({ + row: i + 1, + error: 'Validation failed: ' + validationErrors.join(', '), + transaction: transaction + }); + errorCount++; + continue; + } + let parsedDate = null; + if (transaction['Buchungstag']) { + const dateStr = transaction['Buchungstag'].trim(); + const dateParts = dateStr.split(/[.\/\-]/); + if (dateParts.length === 3) { + const day = parseInt(dateParts[0], 10); + const month = parseInt(dateParts[1], 10) - 1; + let year = parseInt(dateParts[2], 10); + + if (year < 100) { + year += (year < 50) ? 2000 : 1900; + } + + parsedDate = new Date(year, month, day); + + if (isNaN(parsedDate.getTime()) || + parsedDate.getDate() !== day || + parsedDate.getMonth() !== month || + parsedDate.getFullYear() !== year) { + parsedDate = null; + validationErrors.push('Invalid date format: ' + dateStr); + } + } else { + validationErrors.push('Invalid date format: ' + dateStr); + } + } + + let numericAmount = 0; + if (transaction['Betrag']) { + const amountStr = transaction['Betrag'].toString().replace(/[^\d,.-]/g, ''); + const normalizedAmount = amountStr.replace(',', '.'); + numericAmount = parseFloat(normalizedAmount) || 0; + } + + const insertQuery = ` + INSERT INTO fibdash.CSVTransactions + (buchungstag, wertstellung, umsatzart, betrag, betrag_original, waehrung, + beguenstigter_zahlungspflichtiger, kontonummer_iban, bic, verwendungszweck, + parsed_date, numeric_amount, import_batch_id, source_filename, source_row_number) + VALUES + (@buchungstag, @wertstellung, @umsatzart, @betrag, @betrag_original, @waehrung, + @beguenstigter_zahlungspflichtiger, @kontonummer_iban, @bic, @verwendungszweck, + @parsed_date, @numeric_amount, @import_batch_id, @source_filename, @source_row_number) + `; + + await executeQuery(insertQuery, { + buchungstag: transaction['Buchungstag'] || null, + wertstellung: transaction['Valutadatum'] || null, + umsatzart: transaction['Buchungstext'] || null, + betrag: numericAmount, + betrag_original: transaction['Betrag'] || null, + waehrung: transaction['Waehrung'] || null, + beguenstigter_zahlungspflichtiger: transaction['Beguenstigter/Zahlungspflichtiger'] || null, + kontonummer_iban: transaction['Kontonummer/IBAN'] || null, + bic: transaction['BIC (SWIFT-Code)'] || null, + verwendungszweck: transaction['Verwendungszweck'] || null, + parsed_date: parsedDate, + numeric_amount: numericAmount, + import_batch_id: importBatchId, + source_filename: filename || null, + source_row_number: i + 1 + }); + + successCount++; + } catch (error) { + console.error('Error importing transaction ' + (i + 1) + ':', error); + errors.push({ + row: i + 1, + error: error.message, + transaction: transaction + }); + errorCount++; + } + } + + res.json({ + success: true, + batchId: importBatchId, + imported: successCount, + errors: errorCount, + details: errors.length > 0 ? errors : undefined + }); + + } catch (error) { + console.error('Error importing CSV transactions:', error); + res.status(500).json({ error: 'Failed to import CSV transactions' }); + } +}); + +// Get imported CSV transactions +router.get('/csv-transactions', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { batchId, limit = 100, offset = 0 } = req.query; + + let query = ` + SELECT + csv.*, + k.name as kreditor_name, + k.kreditorId as kreditor_id, + k.is_banking as kreditor_is_banking, + bat.assigned_kreditor_id, + ak.name as assigned_kreditor_name + FROM fibdash.CSVTransactions csv + LEFT JOIN fibdash.Kreditor k ON csv.kontonummer_iban = k.iban + LEFT JOIN fibdash.BankingAccountTransactions bat ON csv.id = bat.csv_transaction_id + LEFT JOIN fibdash.Kreditor ak ON bat.assigned_kreditor_id = ak.id + `; + + const params = {}; + + if (batchId) { + query += ' WHERE csv.import_batch_id = @batchId'; + params.batchId = batchId; + } + + query += ' ORDER BY csv.parsed_date DESC, csv.id DESC'; + query += ' OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY'; + + params.offset = parseInt(offset, 10); + params.limit = parseInt(limit, 10); + + const result = await executeQuery(query, params); + + res.json(result.recordset); + } catch (error) { + console.error('Error fetching CSV transactions:', error); + res.status(500).json({ error: 'Failed to fetch CSV transactions' }); + } +}); + +// Get CSV import batches +router.get('/csv-import-batches', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + + const query = ` + SELECT + import_batch_id, + source_filename, + MIN(import_date) as import_date, + COUNT(*) as transaction_count, + SUM(CASE WHEN is_processed = 1 THEN 1 ELSE 0 END) as processed_count + FROM fibdash.CSVTransactions + GROUP BY import_batch_id, source_filename + ORDER BY MIN(import_date) DESC + `; + + const result = await executeQuery(query); + + res.json(result.recordset); + } catch (error) { + console.error('Error fetching import batches:', error); + res.status(500).json({ error: 'Failed to fetch import batches' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/data/datev.js b/src/routes/data/datev.js new file mode 100644 index 0000000..3956187 --- /dev/null +++ b/src/routes/data/datev.js @@ -0,0 +1,71 @@ +const express = require('express'); +const { authenticateToken } = require('../../middleware/auth'); + +const router = express.Router(); + +// DATEV helpers (ported from original file) +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/:timeRange', authenticateToken, async (req, res) => { + try { + const { timeRange } = req.params; + // TODO: Update to use database queries instead of CSV file + res.status(501).json({ error: 'DATEV export temporarily disabled - use database-based queries' }); + return; + } catch (error) { + console.error('Error generating DATEV export:', error); + res.status(500).json({ error: 'Failed to generate DATEV export' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/data/helpers/jtl.js b/src/routes/data/helpers/jtl.js new file mode 100644 index 0000000..12898c6 --- /dev/null +++ b/src/routes/data/helpers/jtl.js @@ -0,0 +1,67 @@ +const { executeQuery } = require('../../../config/database'); + +// Get database transactions for JTL comparison +async function getJTLTransactions() { + try { + 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); + const transactions = result.recordset || []; + + // Get PDF documents for each transaction + const pdfQuery = `SELECT kUmsatzBeleg, kZahlungsabgleichUmsatz, textContent, markDown, extraction, datevlink FROM tUmsatzBeleg`; + const pdfResult = await executeQuery(pdfQuery); + + for (const item of pdfResult.recordset) { + for (const transaction of transactions) { + if (item.kZahlungsabgleichUmsatz == transaction.kZahlungsabgleichUmsatz) { + if (!transaction.pdfs) transaction.pdfs = []; + transaction.pdfs.push({ + kUmsatzBeleg: item.kUmsatzBeleg, + content: item.textContent, + markDown: item.markDown, + extraction: item.extraction, + datevlink: item.datevlink + }); + } + } + } + + // Get links for each transaction + const linksQuery = ` + SELECT kZahlungsabgleichUmsatzLink, kZahlungsabgleichUmsatz, linktarget, linktype, note, + tPdfObjekt.kPdfObjekt, tPdfObjekt.textContent, tPdfObjekt.markDown, + tPdfObjekt.extraction + FROM tZahlungsabgleichUmsatzLink + LEFT JOIN tPdfObjekt ON (tZahlungsabgleichUmsatzLink.linktarget = tPdfObjekt.kLieferantenbestellung) + WHERE linktype = 'kLieferantenBestellung' + `; + const linksResult = await executeQuery(linksQuery); + + for (const item of linksResult.recordset) { + for (const transaction of transactions) { + if (item.kZahlungsabgleichUmsatz == transaction.kZahlungsabgleichUmsatz) { + if (!transaction.links) transaction.links = []; + transaction.links.push(item); + } + } + } + + return transactions; + } catch (error) { + console.error('Error fetching JTL transactions:', error); + throw error; + } +} + +module.exports = { + getJTLTransactions +}; \ No newline at end of file diff --git a/src/routes/data/index.js b/src/routes/data/index.js new file mode 100644 index 0000000..fd2f28b --- /dev/null +++ b/src/routes/data/index.js @@ -0,0 +1,22 @@ +const express = require('express'); + +const months = require('./months'); +const transactions = require('./transactions'); +const datev = require('./datev'); +const pdf = require('./pdf'); +const kreditors = require('./kreditors'); +const bankingTransactions = require('./bankingTransactions'); +const csvImport = require('./csvImport'); + +const router = express.Router(); + +// Mount sub-routers preserving original paths +router.use(months); +router.use(transactions); +router.use(datev); +router.use(pdf); +router.use(kreditors); +router.use(bankingTransactions); +router.use(csvImport); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/data/kreditors.js b/src/routes/data/kreditors.js new file mode 100644 index 0000000..91398ef --- /dev/null +++ b/src/routes/data/kreditors.js @@ -0,0 +1,181 @@ +const express = require('express'); +const { authenticateToken } = require('../../middleware/auth'); + +const router = express.Router(); + +// Get all kreditors +router.get('/kreditors', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const query = ` + SELECT id, iban, name, kreditorId + FROM fibdash.Kreditor + ORDER BY name ASC, iban ASC + `; + + const result = await executeQuery(query); + res.json(result.recordset || []); + } catch (error) { + console.error('Error fetching kreditors:', error); + res.status(500).json({ error: 'Failed to fetch kreditors' }); + } +}); + +// Get kreditor by ID +router.get('/kreditors/:id', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { id } = req.params; + + const query = ` + SELECT id, iban, name, kreditorId, is_banking + FROM fibdash.Kreditor + WHERE id = @id + `; + + const result = await executeQuery(query, { id: parseInt(id, 10) }); + + if (result.recordset.length === 0) { + return res.status(404).json({ error: 'Kreditor not found' }); + } + + res.json(result.recordset[0]); + } catch (error) { + console.error('Error fetching kreditor:', error); + res.status(500).json({ error: 'Failed to fetch kreditor' }); + } +}); + +// Create new kreditor +router.post('/kreditors', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { iban, name, kreditorId, is_banking } = req.body; + + const isBanking = is_banking || false; + + if (!name || !kreditorId) { + return res.status(400).json({ error: 'Name and kreditorId are required' }); + } + + if (!isBanking && (!iban || iban.trim() === '')) { + return res.status(400).json({ error: 'IBAN is required (except for banking accounts)' }); + } + + if (iban && iban.trim() !== '') { + const checkQuery = ` + SELECT id FROM fibdash.Kreditor + WHERE iban = @iban + `; + + const checkResult = await executeQuery(checkQuery, { iban }); + + if (checkResult.recordset.length > 0) { + return res.status(409).json({ error: 'Kreditor with this IBAN already exists' }); + } + } + + const insertQuery = ` + INSERT INTO fibdash.Kreditor (iban, name, kreditorId, is_banking) + OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.is_banking + VALUES (@iban, @name, @kreditorId, @is_banking) + `; + + const result = await executeQuery(insertQuery, { + iban: iban || null, + name, + kreditorId, + is_banking: isBanking + }); + + res.status(201).json(result.recordset[0]); + } catch (error) { + console.error('Error creating kreditor:', error); + res.status(500).json({ error: 'Failed to create kreditor' }); + } +}); + +// Update kreditor +router.put('/kreditors/:id', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { id } = req.params; + const { iban, name, kreditorId, is_banking } = req.body; + + const isBanking = is_banking || false; + + if (!name || !kreditorId) { + return res.status(400).json({ error: 'Name and kreditorId are required' }); + } + + if (!isBanking && (!iban || iban.trim() === '')) { + return res.status(400).json({ error: 'IBAN is required (except for banking accounts)' }); + } + + const checkQuery = `SELECT id FROM fibdash.Kreditor WHERE id = @id`; + const checkResult = await executeQuery(checkQuery, { id: parseInt(id, 10) }); + + if (checkResult.recordset.length === 0) { + return res.status(404).json({ error: 'Kreditor not found' }); + } + + if (iban && iban.trim() !== '') { + const conflictQuery = ` + SELECT id FROM fibdash.Kreditor + WHERE iban = @iban AND id != @id + `; + + const conflictResult = await executeQuery(conflictQuery, { iban, id: parseInt(id, 10) }); + + if (conflictResult.recordset.length > 0) { + return res.status(409).json({ error: 'Another kreditor with this IBAN already exists' }); + } + } + + const updateQuery = ` + UPDATE fibdash.Kreditor + SET iban = @iban, name = @name, kreditorId = @kreditorId, is_banking = @is_banking + OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.is_banking + WHERE id = @id + `; + + const result = await executeQuery(updateQuery, { + iban: iban || null, + name, + kreditorId, + is_banking: isBanking, + id: parseInt(id, 10) + }); + + res.json(result.recordset[0]); + } catch (error) { + console.error('Error updating kreditor:', error); + res.status(500).json({ error: 'Failed to update kreditor' }); + } +}); + +// Delete kreditor (hard delete) +router.delete('/kreditors/:id', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { id } = req.params; + + const query = ` + DELETE FROM fibdash.Kreditor + WHERE id = @id + `; + + const result = await executeQuery(query, { id: parseInt(id, 10) }); + + if (result.rowsAffected[0] === 0) { + return res.status(404).json({ error: 'Kreditor not found' }); + } + + res.json({ message: 'Kreditor deleted successfully' }); + } catch (error) { + console.error('Error deleting kreditor:', error); + res.status(500).json({ error: 'Failed to delete kreditor' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/data/months.js b/src/routes/data/months.js new file mode 100644 index 0000000..bebf5be --- /dev/null +++ b/src/routes/data/months.js @@ -0,0 +1,33 @@ +const express = require('express'); +const { authenticateToken } = require('../../middleware/auth'); + +const router = express.Router(); + +// Get available months from database +router.get('/months', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + + const query = ` + SELECT DISTINCT + FORMAT(combined.date_col, 'yyyy-MM') as month_year + FROM ( + SELECT buchungsdatum as date_col FROM fibdash.AccountingItems WHERE buchungsdatum IS NOT NULL + UNION ALL + SELECT parsed_date as date_col FROM fibdash.CSVTransactions WHERE parsed_date IS NOT NULL + ) combined + WHERE combined.date_col IS NOT NULL + ORDER BY month_year DESC + `; + + const result = await executeQuery(query); + const months = result.recordset.map(row => row.month_year); + + res.json({ months }); + } catch (error) { + console.error('Error getting months:', error); + res.status(500).json({ error: 'Failed to load months' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/data/pdf.js b/src/routes/data/pdf.js new file mode 100644 index 0000000..45ba395 --- /dev/null +++ b/src/routes/data/pdf.js @@ -0,0 +1,72 @@ +const express = require('express'); +const { authenticateToken } = require('../../middleware/auth'); + +const router = express.Router(); + +// Get PDF from tUmsatzBeleg +router.get('/pdf/umsatzbeleg/:kUmsatzBeleg', authenticateToken, async (req, res) => { + try { + const { kUmsatzBeleg } = req.params; + const { executeQuery } = require('../../config/database'); + + const query = ` + SELECT content, datevlink + FROM dbo.tUmsatzBeleg + WHERE kUmsatzBeleg = @kUmsatzBeleg AND content IS NOT NULL + `; + + const result = await executeQuery(query, { + kUmsatzBeleg: parseInt(kUmsatzBeleg, 10) + }); + + if (!result.recordset || result.recordset.length === 0) { + return res.status(404).json({ error: 'PDF not found' }); + } + + const pdfData = result.recordset[0]; + const filename = 'Umsatzbeleg_' + kUmsatzBeleg + '_' + (pdfData.datevlink || 'document') + '.pdf'; + + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', 'inline; filename="' + filename + '"'); + res.send(pdfData.content); + + } catch (error) { + console.error('Error fetching PDF from tUmsatzBeleg:', error); + res.status(500).json({ error: 'Failed to fetch PDF' }); + } +}); + +// Get PDF from tPdfObjekt +router.get('/pdf/pdfobject/:kPdfObjekt', authenticateToken, async (req, res) => { + try { + const { kPdfObjekt } = req.params; + const { executeQuery } = require('../../config/database'); + + const query = ` + SELECT content, datevlink, kLieferantenbestellung + FROM dbo.tPdfObjekt + WHERE kPdfObjekt = @kPdfObjekt AND content IS NOT NULL + `; + + const result = await executeQuery(query, { + kPdfObjekt: parseInt(kPdfObjekt, 10) + }); + + if (!result.recordset || result.recordset.length === 0) { + return res.status(404).json({ error: 'PDF not found' }); + } + + const pdfData = result.recordset[0]; + const filename = 'PdfObjekt_' + kPdfObjekt + '_LB' + pdfData.kLieferantenbestellung + '_' + (pdfData.datevlink || 'document') + '.pdf'; + + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', 'inline; filename="' + filename + '"'); + res.send(pdfData.content); + + } catch (error) { + console.error('Error fetching PDF from tPdfObjekt:', error); + res.status(500).json({ error: 'Failed to fetch PDF' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/data/transactions.js b/src/routes/data/transactions.js new file mode 100644 index 0000000..914bc32 --- /dev/null +++ b/src/routes/data/transactions.js @@ -0,0 +1,348 @@ +const express = require('express'); +const { authenticateToken } = require('../../middleware/auth'); +const { getJTLTransactions } = require('./helpers/jtl'); + +const router = express.Router(); + +// Get transactions for a specific time period (month, quarter, or year) +router.get('/transactions/:timeRange', authenticateToken, async (req, res) => { + try { + const { timeRange } = req.params; + + const { executeQuery } = require('../../config/database'); + const query = ` + SELECT + csv.buchungstag as 'Buchungstag', + csv.wertstellung as 'Valutadatum', + csv.umsatzart as 'Buchungstext', + csv.verwendungszweck as 'Verwendungszweck', + csv.beguenstigter_zahlungspflichtiger as 'Beguenstigter/Zahlungspflichtiger', + csv.kontonummer_iban as 'Kontonummer/IBAN', + csv.bic as 'BIC (SWIFT-Code)', + csv.betrag_original as 'Betrag', + csv.waehrung as 'Waehrung', + csv.numeric_amount as numericAmount, + csv.parsed_date, + FORMAT(csv.parsed_date, 'yyyy-MM') as monthYear, + jtl.kZahlungsabgleichUmsatz as jtlId, + CASE WHEN jtl.kZahlungsabgleichUmsatz IS NOT NULL THEN 1 ELSE 0 END as hasJTL, + k.name as kreditor_name, + k.kreditorId as kreditor_id, + k.is_banking as kreditor_is_banking, + bat.assigned_kreditor_id, + ak.name as assigned_kreditor_name, + 0 as isJTLOnly, + 1 as isFromCSV, + ub.textContent as jtl_document_data, + ub.kUmsatzBeleg, + ub.datevlink + FROM fibdash.CSVTransactions csv + LEFT JOIN eazybusiness.dbo.tZahlungsabgleichUmsatz jtl ON ( + ABS(csv.numeric_amount - jtl.fBetrag) < 0.01 AND + ABS(DATEDIFF(day, csv.parsed_date, jtl.dBuchungsdatum)) <= 1 + ) + LEFT JOIN eazybusiness.dbo.tUmsatzBeleg ub ON ub.kZahlungsabgleichUmsatz = jtl.kZahlungsabgleichUmsatz + LEFT JOIN fibdash.Kreditor k ON csv.kontonummer_iban = k.iban + LEFT JOIN fibdash.BankingAccountTransactions bat ON csv.id = bat.csv_transaction_id + LEFT JOIN fibdash.Kreditor ak ON bat.assigned_kreditor_id = ak.id + + UNION ALL + + SELECT + FORMAT(jtl.dBuchungsdatum, 'dd.MM.yy') as 'Buchungstag', + FORMAT(jtl.dBuchungsdatum, 'dd.MM.yy') as 'Valutadatum', + 'JTL Transaction' as 'Buchungstext', + jtl.cVerwendungszweck as 'Verwendungszweck', + jtl.cName as 'Beguenstigter/Zahlungspflichtiger', + '' as 'Kontonummer/IBAN', + '' as 'BIC (SWIFT-Code)', + FORMAT(jtl.fBetrag, 'N2', 'de-DE') as 'Betrag', + '' as 'Waehrung', + jtl.fBetrag as numericAmount, + jtl.dBuchungsdatum as parsed_date, + FORMAT(jtl.dBuchungsdatum, 'yyyy-MM') as monthYear, + jtl.kZahlungsabgleichUmsatz as jtlId, + 1 as hasJTL, + NULL as kreditor_name, + NULL as kreditor_id, + NULL as kreditor_is_banking, + NULL as assigned_kreditor_id, + NULL as assigned_kreditor_name, + 1 as isJTLOnly, + 0 as isFromCSV, + ub.textContent as jtl_document_data, + ub.kUmsatzBeleg, + ub.datevlink + FROM eazybusiness.dbo.tZahlungsabgleichUmsatz jtl + LEFT JOIN eazybusiness.dbo.tUmsatzBeleg ub ON ub.kZahlungsabgleichUmsatz = jtl.kZahlungsabgleichUmsatz + WHERE NOT EXISTS ( + SELECT 1 FROM fibdash.CSVTransactions csv + WHERE ABS(csv.numeric_amount - jtl.fBetrag) < 0.01 + AND ABS(DATEDIFF(day, csv.parsed_date, jtl.dBuchungsdatum)) <= 1 + ) + + ORDER BY parsed_date DESC + `; + + const result = await executeQuery(query); + + // Get links data separately to avoid duplicate rows + const linksQuery = ` + SELECT + zul.kZahlungsabgleichUmsatz, + zul.linktarget, + zul.linktype, + zul.note, + po.kPdfObjekt, + po.textContent, + po.markDown, + po.extraction + FROM eazybusiness.dbo.tZahlungsabgleichUmsatzLink zul + LEFT JOIN eazybusiness.dbo.tPdfObjekt po ON zul.linktarget = po.kLieferantenbestellung + WHERE zul.linktype = 'kLieferantenBestellung' + `; + const linksResult = await executeQuery(linksQuery); + const linksData = linksResult.recordset || []; + + const transactions = result.recordset.map(transaction => ({ + ...transaction, + parsedDate: new Date(transaction.parsed_date), + hasJTL: Boolean(transaction.hasJTL), + isFromCSV: true, + jtlDatabaseAvailable: true, + hasKreditor: !!transaction.kreditor_name, + kreditor: transaction.kreditor_name ? { + name: transaction.kreditor_name, + kreditorId: transaction.kreditor_id, + is_banking: Boolean(transaction.kreditor_is_banking) + } : null, + assigned_kreditor: transaction.assigned_kreditor_name ? { + name: transaction.assigned_kreditor_name, + id: transaction.assigned_kreditor_id + } : null, + pdfs: transaction.jtl_document_data ? [{ + content: transaction.jtl_document_data, + kUmsatzBeleg: transaction.kUmsatzBeleg, + datevlink: transaction.datevlink + }] : [], + links: transaction.jtlId ? linksData.filter(link => + link.kZahlungsabgleichUmsatz === transaction.jtlId + ) : [] + })); + + let filteredTransactions = []; + + if (timeRange.includes('-Q')) { + const [year, quarterPart] = timeRange.split('-Q'); + const quarter = parseInt(quarterPart, 10); + const startMonth = (quarter - 1) * 3 + 1; + const endMonth = startMonth + 2; + + filteredTransactions = transactions.filter(t => { + if (!t.monthYear) return false; + const [tYear, tMonth] = t.monthYear.split('-'); + const monthNum = parseInt(tMonth, 10); + return tYear === year && monthNum >= startMonth && monthNum <= endMonth; + }); + } else if (timeRange.length === 4) { + filteredTransactions = transactions.filter(t => { + if (!t.monthYear) return false; + const [tYear] = t.monthYear.split('-'); + return tYear === timeRange; + }); + } else { + filteredTransactions = transactions.filter(t => t.monthYear === timeRange); + } + + const monthTransactions = filteredTransactions + .sort((a, b) => b.parsedDate - a.parsedDate); + + // Get JTL transactions for comparison + let jtlTransactions = []; + let jtlDatabaseAvailable = false; + try { + jtlTransactions = await getJTLTransactions(); + jtlDatabaseAvailable = true; + console.log('DEBUG: JTL database connected, found', jtlTransactions.length, 'transactions'); + } catch (error) { + console.log('JTL database not available, continuing without JTL data:', error.message); + jtlDatabaseAvailable = false; + } + + // Filter JTL transactions for the selected time period + let jtlMonthTransactions = []; + + if (timeRange.includes('-Q')) { + const [year, quarterPart] = timeRange.split('-Q'); + const quarter = parseInt(quarterPart, 10); + const startMonth = (quarter - 1) * 3 + 1; + const endMonth = startMonth + 2; + + jtlMonthTransactions = jtlTransactions.filter(jtl => { + const jtlDate = new Date(jtl.dBuchungsdatum); + const jtlMonth = jtlDate.getMonth() + 1; + return jtlDate.getFullYear() === parseInt(year, 10) && + jtlMonth >= startMonth && jtlMonth <= endMonth; + }); + } else if (timeRange.length === 4) { + jtlMonthTransactions = jtlTransactions.filter(jtl => { + const jtlDate = new Date(jtl.dBuchungsdatum); + return jtlDate.getFullYear() === parseInt(timeRange, 10); + }); + } else { + const [year, month] = timeRange.split('-'); + jtlMonthTransactions = jtlTransactions.filter(jtl => { + const jtlDate = new Date(jtl.dBuchungsdatum); + return jtlDate.getFullYear() === parseInt(year, 10) && + jtlDate.getMonth() === parseInt(month, 10) - 1; + }); + } + + // Get Kreditor information for IBAN lookup + let kreditorData = []; + try { + const kreditorQuery = `SELECT id, iban, name, kreditorId, is_banking FROM fibdash.Kreditor`; + const kreditorResult = await executeQuery(kreditorQuery); + kreditorData = kreditorResult.recordset || []; + } catch (error) { + console.log('Kreditor database not available, continuing without Kreditor data'); + } + + // Add JTL status and Kreditor information to each CSV transaction + const transactionsWithJTL = monthTransactions.map((transaction, index) => { + const amount = transaction.numericAmount; + const transactionDate = transaction.parsedDate; + + if (index === 0) { + console.log('DEBUG First CSV transaction:', { + amount: amount, + transactionDate: transactionDate, + jtlMonthTransactionsCount: jtlMonthTransactions.length + }); + if (jtlMonthTransactions.length > 0) { + console.log('DEBUG First JTL transaction:', { + amount: parseFloat(jtlMonthTransactions[0].fBetrag), + date: new Date(jtlMonthTransactions[0].dBuchungsdatum) + }); + } + } + + const jtlMatch = jtlMonthTransactions.find(jtl => { + const jtlAmount = parseFloat(jtl.fBetrag) || 0; + const jtlDate = new Date(jtl.dBuchungsdatum); + + const amountMatch = Math.abs(amount - jtlAmount) < 0.01; + const dateMatch = transactionDate && jtlDate && + transactionDate.getFullYear() === jtlDate.getFullYear() && + transactionDate.getMonth() === jtlDate.getMonth() && + transactionDate.getDate() === jtlDate.getDate(); + + if (index === 0 && (amountMatch || dateMatch)) { + console.log('DEBUG Potential match for first transaction:', { + csvAmount: amount, + jtlAmount: jtlAmount, + amountMatch: amountMatch, + csvDate: transactionDate, + jtlDate: jtlDate, + dateMatch: dateMatch, + bothMatch: amountMatch && dateMatch + }); + } + + return amountMatch && dateMatch; + }); + + const transactionIban = transaction['Kontonummer/IBAN']; + const kreditorMatch = transactionIban ? kreditorData.find(k => k.iban === transactionIban) : null; + + return { + ...transaction, + hasJTL: jtlDatabaseAvailable ? !!jtlMatch : undefined, + jtlId: jtlMatch ? jtlMatch.kZahlungsabgleichUmsatz : null, + isFromCSV: true, + jtlDatabaseAvailable, + pdfs: jtlMatch ? jtlMatch.pdfs || [] : [], + links: jtlMatch ? jtlMatch.links || [] : [], + kreditor: kreditorMatch ? { + id: kreditorMatch.id, + name: kreditorMatch.name, + kreditorId: kreditorMatch.kreditorId, + iban: kreditorMatch.iban, + is_banking: Boolean(kreditorMatch.is_banking) + } : null, + hasKreditor: !!kreditorMatch + }; + }); + + const unmatchedJTLTransactions = jtlMonthTransactions + .filter(jtl => { + const jtlAmount = parseFloat(jtl.fBetrag) || 0; + const jtlDate = new Date(jtl.dBuchungsdatum); + + 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 => ({ + '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 || '', + 'Kontonummer/IBAN': '', + 'Betrag': jtl.fBetrag ? jtl.fBetrag.toString().replace('.', ',') : '0,00', + numericAmount: parseFloat(jtl.fBetrag) || 0, + parsedDate: new Date(jtl.dBuchungsdatum), + monthYear: timeRange, + hasJTL: true, + jtlId: jtl.kZahlungsabgleichUmsatz, + isFromCSV: false, + isJTLOnly: true, + pdfs: jtl.pdfs || [], + links: jtl.links || [], + kreditor: null, + hasKreditor: false + })); + + const summary = { + totalTransactions: filteredTransactions.length, + totalIncome: filteredTransactions + .filter(t => t.numericAmount > 0) + .reduce((sum, t) => sum + t.numericAmount, 0), + totalExpenses: filteredTransactions + .filter(t => t.numericAmount < 0) + .reduce((sum, t) => sum + Math.abs(t.numericAmount), 0), + netAmount: filteredTransactions.reduce((sum, t) => sum + t.numericAmount, 0), + timeRange: timeRange, + jtlDatabaseAvailable: true, + jtlMatches: filteredTransactions.filter(t => t.hasJTL === true && t.isFromCSV).length, + jtlMissing: filteredTransactions.filter(t => t.hasJTL === false && t.isFromCSV).length, + jtlOnly: filteredTransactions.filter(t => t.isJTLOnly === true).length, + csvOnly: filteredTransactions.filter(t => t.hasJTL === false && t.isFromCSV).length + }; + + res.json({ + transactions: filteredTransactions, + summary + }); + } catch (error) { + console.error('Error getting transactions:', error); + res.status(500).json({ error: 'Failed to load transactions' }); + } +}); + +module.exports = router; \ No newline at end of file