diff --git a/src/routes/data/transactions.js b/src/routes/data/transactions.js index 14cfd9d..02141c4 100644 --- a/src/routes/data/transactions.js +++ b/src/routes/data/transactions.js @@ -10,6 +10,25 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => { const { timeRange } = req.params; const { executeQuery } = require('../../config/database'); + + // Build WHERE clause based on timeRange format + let timeWhereClause = ''; + if (timeRange.includes('-Q')) { + // Quarter format: 2025-Q2 + const [year, quarterPart] = timeRange.split('-Q'); + const quarter = parseInt(quarterPart, 10); + const startMonth = (quarter - 1) * 3 + 1; + const endMonth = startMonth + 2; + timeWhereClause = `WHERE YEAR(csv.parsed_date) = ${year} AND MONTH(csv.parsed_date) BETWEEN ${startMonth} AND ${endMonth}`; + } else if (timeRange.length === 4) { + // Year format: 2025 + timeWhereClause = `WHERE YEAR(csv.parsed_date) = ${timeRange}`; + } else { + // Month format: 2025-07 + const [year, month] = timeRange.split('-'); + timeWhereClause = `WHERE YEAR(csv.parsed_date) = ${year} AND MONTH(csv.parsed_date) = ${parseInt(month, 10)}`; + } + const query = ` SELECT csv.id as id, @@ -47,6 +66,7 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => { 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 + ${timeWhereClause} UNION ALL @@ -84,6 +104,12 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => { WHERE ABS(csv.numeric_amount - jtl.fBetrag) < 0.01 AND ABS(DATEDIFF(day, csv.parsed_date, jtl.dBuchungsdatum)) <= 1 ) + ${timeRange.includes('-Q') ? + `AND YEAR(jtl.dBuchungsdatum) = ${timeRange.split('-Q')[0]} AND MONTH(jtl.dBuchungsdatum) BETWEEN ${(parseInt(timeRange.split('-Q')[1], 10) - 1) * 3 + 1} AND ${(parseInt(timeRange.split('-Q')[1], 10) - 1) * 3 + 3}` : + timeRange.length === 4 ? + `AND YEAR(jtl.dBuchungsdatum) = ${timeRange}` : + `AND YEAR(jtl.dBuchungsdatum) = ${timeRange.split('-')[0]} AND MONTH(jtl.dBuchungsdatum) = ${parseInt(timeRange.split('-')[1], 10)}` + } ORDER BY parsed_date DESC `; @@ -163,213 +189,32 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => { links: [...new Set(transaction.links.map(l => JSON.stringify(l)))].map(l => JSON.parse(l)) })); - 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 + // Transactions are already filtered by the SQL query, so we just need to sort them + const monthTransactions = transactions .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 - })); + // Since transactions are already filtered and joined with JTL data in SQL, + // we don't need the complex post-processing logic anymore const summary = { - totalTransactions: filteredTransactions.length, - totalIncome: filteredTransactions + totalTransactions: transactions.length, + totalIncome: transactions .filter(t => t.numericAmount > 0) .reduce((sum, t) => sum + t.numericAmount, 0), - totalExpenses: filteredTransactions + totalExpenses: transactions .filter(t => t.numericAmount < 0) .reduce((sum, t) => sum + Math.abs(t.numericAmount), 0), - netAmount: filteredTransactions.reduce((sum, t) => sum + t.numericAmount, 0), + netAmount: transactions.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 + jtlMatches: transactions.filter(t => t.hasJTL === true && t.isFromCSV).length, + jtlMissing: transactions.filter(t => t.hasJTL === false && t.isFromCSV).length, + jtlOnly: transactions.filter(t => t.isJTLOnly === true).length, + csvOnly: transactions.filter(t => t.hasJTL === false && t.isFromCSV).length }; res.json({ - transactions: filteredTransactions, + transactions: transactions, summary }); } catch (error) {