diff --git a/public/payment-modal.js b/public/payment-modal.js index 94172c8..0682252 100644 --- a/public/payment-modal.js +++ b/public/payment-modal.js @@ -1,67 +1,142 @@ -// payment-modal.js — ES Module für das Payment Recording Modal -// Fixes: Correct CSS class 'modal', local DB payment storage +// payment-modal.js — ES Module v2 +// Supports: Multi-invoice, partial payments, unapplied (downpayments), editable amounts -// ============================================================ -// State -// ============================================================ let bankAccounts = []; let paymentMethods = []; -let selectedInvoices = []; +let selectedInvoices = []; // { invoice, payAmount } let dataLoaded = false; +let paymentMode = 'invoice'; // 'invoice' | 'unapplied' // ============================================================ -// Load QBO Reference Data +// Load QBO Data // ============================================================ async function loadQboData() { if (dataLoaded) return; - try { const [accRes, pmRes] = await Promise.all([ fetch('/api/qbo/accounts'), fetch('/api/qbo/payment-methods') ]); - if (accRes.ok) bankAccounts = await accRes.json(); if (pmRes.ok) paymentMethods = await pmRes.json(); - dataLoaded = true; - } catch (error) { - console.error('Error loading QBO reference data:', error); - } + } catch (e) { console.error('Error loading QBO data:', e); } } // ============================================================ // Open / Close // ============================================================ -export async function openPaymentModal(invoiceIds = []) { +export async function openPaymentModal(invoiceIds = [], mode = 'invoice') { await loadQboData(); - + paymentMode = mode; selectedInvoices = []; + for (const id of invoiceIds) { try { const res = await fetch(`/api/invoices/${id}`); const data = await res.json(); if (data.invoice) { - selectedInvoices.push(data.invoice); + selectedInvoices.push({ + invoice: data.invoice, + payAmount: parseFloat(data.invoice.total) + }); } - } catch (e) { - console.error('Error loading invoice:', id, e); - } + } catch (e) { console.error('Error loading invoice:', id, e); } } - + ensureModalElement(); renderModalContent(); document.getElementById('payment-modal').classList.add('active'); } +export function openDownpaymentModal() { + openPaymentModal([], 'unapplied'); +} + export function closePaymentModal() { const modal = document.getElementById('payment-modal'); if (modal) modal.classList.remove('active'); selectedInvoices = []; } +// ============================================================ +// Add / Remove Invoices +// ============================================================ + +async function addInvoiceById() { + const input = document.getElementById('payment-add-invoice-id'); + const searchVal = input.value.trim(); + if (!searchVal) return; + + // Suche nach Invoice-Nummer oder ID + try { + const res = await fetch('/api/invoices'); + const allInvoices = await res.json(); + + const match = allInvoices.find(inv => + String(inv.id) === searchVal || + String(inv.invoice_number) === searchVal + ); + + if (!match) { + alert(`Keine Rechnung mit Nr/ID "${searchVal}" gefunden.`); + return; + } + + // Validierungen + if (!match.qbo_id) { + alert('Diese Rechnung ist noch nicht in QBO.'); + return; + } + if (match.paid_date) { + alert('Diese Rechnung ist bereits bezahlt.'); + return; + } + if (selectedInvoices.find(si => si.invoice.id === match.id)) { + alert('Diese Rechnung ist bereits in der Liste.'); + return; + } + if (selectedInvoices.length > 0 && match.customer_id !== selectedInvoices[0].invoice.customer_id) { + alert('Alle Rechnungen müssen zum selben Kunden gehören.'); + return; + } + + // Details laden + const detailRes = await fetch(`/api/invoices/${match.id}`); + const detailData = await detailRes.json(); + + selectedInvoices.push({ + invoice: detailData.invoice, + payAmount: parseFloat(detailData.invoice.total) + }); + + renderInvoiceList(); + updateTotal(); + input.value = ''; + } catch (e) { + console.error('Error adding invoice:', e); + alert('Fehler beim Suchen.'); + } +} + +function removeInvoice(invoiceId) { + selectedInvoices = selectedInvoices.filter(si => si.invoice.id !== invoiceId); + renderInvoiceList(); + updateTotal(); +} + +function updatePayAmount(invoiceId, newAmount) { + const si = selectedInvoices.find(s => s.invoice.id === invoiceId); + if (si) { + const val = parseFloat(newAmount) || 0; + const max = parseFloat(si.invoice.total); + si.payAmount = Math.min(val, max); // Nicht mehr als Total + } + updateTotal(); +} + // ============================================================ // DOM // ============================================================ @@ -71,7 +146,6 @@ function ensureModalElement() { if (!modal) { modal = document.createElement('div'); modal.id = 'payment-modal'; - // Verwende GLEICHE Klasse wie die existierenden Modals modal.className = 'modal fixed inset-0 bg-black bg-opacity-50 z-50 justify-center items-start pt-10 overflow-y-auto'; document.body.appendChild(modal); } @@ -81,25 +155,26 @@ function renderModalContent() { const modal = document.getElementById('payment-modal'); if (!modal) return; - const accountOptions = bankAccounts.map(acc => + const accountOptions = bankAccounts.map(acc => `` ).join(''); - // Zeige Check und ACH bevorzugt, aber alle als Fallback - const filteredMethods = paymentMethods.filter(pm => + const filteredMethods = paymentMethods.filter(pm => pm.name.toLowerCase().includes('check') || pm.name.toLowerCase().includes('ach') ); const methodsToShow = filteredMethods.length > 0 ? filteredMethods : paymentMethods; - const methodOptions = methodsToShow.map(pm => + const methodOptions = methodsToShow.map(pm => `` ).join(''); const today = new Date().toISOString().split('T')[0]; + const isUnapplied = paymentMode === 'unapplied'; + const title = isUnapplied ? '💰 Record Downpayment' : '💰 Record Payment'; modal.innerHTML = `
-
-

💰 Record Payment

+
+

${title}

- -
- -
+ +
+ +
+ ${isUnapplied ? renderUnappliedSection() : renderInvoiceSection()} +
@@ -127,14 +210,14 @@ function renderModalContent() {
-
- @@ -151,57 +234,114 @@ function renderModalContent() {
- +
-
- `; +
`; - renderInvoiceList(); + if (!isUnapplied) { + renderInvoiceList(); + } updateTotal(); } +function renderInvoiceSection() { + return ` +
+ +
+
+ + +
+
`; +} + +function renderUnappliedSection() { + return ` +
+
+

+ Downpayment / Vorabzahlung: Das Geld wird als Kundenguthaben in QBO verbucht + und kann später einer Rechnung zugeordnet werden. +

+
+
+
+ + +
+
+ + +
+
+
`; +} + function renderInvoiceList() { const container = document.getElementById('payment-invoice-list'); if (!container) return; if (selectedInvoices.length === 0) { - container.innerHTML = `
Keine Rechnungen ausgewählt
`; + container.innerHTML = `
Keine Rechnungen — bitte unten hinzufügen
`; return; } - container.innerHTML = selectedInvoices.map(inv => ` -
-
+ container.innerHTML = selectedInvoices.map(si => { + const inv = si.invoice; + const total = parseFloat(inv.total); + const isPartial = si.payAmount < total; + + return ` +
+
#${inv.invoice_number || 'Draft'} ${inv.customer_name || ''} + (Total: $${total.toFixed(2)}) + ${isPartial ? 'Partial' : ''}
-
- $${parseFloat(inv.total).toFixed(2)} - +
+ $ + +
-
- `).join(''); +
`; + }).join(''); } function updateTotal() { const totalEl = document.getElementById('payment-total'); if (!totalEl) return; - const total = selectedInvoices.reduce((sum, inv) => sum + parseFloat(inv.total), 0); - totalEl.textContent = `$${total.toFixed(2)}`; -} -function removeInvoiceFromPayment(invoiceId) { - selectedInvoices = selectedInvoices.filter(inv => inv.id !== invoiceId); - renderInvoiceList(); - updateTotal(); + let total = 0; + if (paymentMode === 'unapplied') { + const amountInput = document.getElementById('payment-unapplied-amount'); + total = parseFloat(amountInput?.value) || 0; + } else { + total = selectedInvoices.reduce((sum, si) => sum + si.payAmount, 0); + } + + totalEl.textContent = `$${total.toFixed(2)}`; } // ============================================================ @@ -209,8 +349,6 @@ function removeInvoiceFromPayment(invoiceId) { // ============================================================ async function submitPayment() { - if (selectedInvoices.length === 0) return; - const paymentDate = document.getElementById('payment-date').value; const reference = document.getElementById('payment-reference').value; const methodSelect = document.getElementById('payment-method'); @@ -225,10 +363,59 @@ async function submitPayment() { return; } - const total = selectedInvoices.reduce((sum, inv) => sum + parseFloat(inv.total), 0); - const invoiceNums = selectedInvoices.map(i => i.invoice_number || `ID:${i.id}`).join(', '); - - if (!confirm(`Payment $${total.toFixed(2)} für #${invoiceNums} an QBO senden?`)) return; + let body; + + if (paymentMode === 'unapplied') { + // --- Downpayment --- + const custSelect = document.getElementById('payment-customer'); + const customerId = custSelect?.value; + const customerQboId = custSelect?.selectedOptions[0]?.getAttribute('data-qbo-id'); + const amount = parseFloat(document.getElementById('payment-unapplied-amount')?.value) || 0; + + if (!customerId || !customerQboId) { alert('Bitte Kunde wählen.'); return; } + if (amount <= 0) { alert('Bitte Betrag eingeben.'); return; } + + body = { + mode: 'unapplied', + customer_id: parseInt(customerId), + customer_qbo_id: customerQboId, + total_amount: amount, + payment_date: paymentDate, + reference_number: reference, + payment_method_id: methodId, + payment_method_name: methodName, + deposit_to_account_id: depositToId, + deposit_to_account_name: depositToName + }; + + if (!confirm(`Downpayment $${amount.toFixed(2)} an QBO senden?`)) return; + + } else { + // --- Normal / Partial / Multi --- + if (selectedInvoices.length === 0) { alert('Bitte Rechnungen hinzufügen.'); return; } + + const total = selectedInvoices.reduce((s, si) => s + si.payAmount, 0); + const nums = selectedInvoices.map(si => `#${si.invoice.invoice_number || si.invoice.id}`).join(', '); + const hasPartial = selectedInvoices.some(si => si.payAmount < parseFloat(si.invoice.total)); + + let msg = `Payment $${total.toFixed(2)} für ${nums} an QBO senden?`; + if (hasPartial) msg += '\n⚠️ Enthält Teilzahlung(en).'; + if (!confirm(msg)) return; + + body = { + mode: 'invoice', + invoice_payments: selectedInvoices.map(si => ({ + invoice_id: si.invoice.id, + amount: si.payAmount + })), + payment_date: paymentDate, + reference_number: reference, + payment_method_id: methodId, + payment_method_name: methodName, + deposit_to_account_id: depositToId, + deposit_to_account_name: depositToName + }; + } const submitBtn = document.getElementById('payment-submit-btn'); submitBtn.innerHTML = '⏳ Wird gesendet...'; @@ -239,17 +426,8 @@ async function submitPayment() { const response = await fetch('/api/qbo/record-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - invoice_ids: selectedInvoices.map(inv => inv.id), - payment_date: paymentDate, - reference_number: reference, - payment_method_id: methodId, - payment_method_name: methodName, - deposit_to_account_id: depositToId, - deposit_to_account_name: depositToName - }) + body: JSON.stringify(body) }); - const result = await response.json(); if (response.ok) { @@ -261,7 +439,7 @@ async function submitPayment() { } } catch (error) { console.error('Payment error:', error); - alert('Netzwerkfehler beim Payment.'); + alert('Netzwerkfehler.'); } finally { submitBtn.innerHTML = '💰 Record Payment in QBO'; submitBtn.disabled = false; @@ -269,13 +447,23 @@ async function submitPayment() { } } +function switchMode(mode) { + paymentMode = mode; + renderModalContent(); +} + // ============================================================ // Expose // ============================================================ window.paymentModal = { open: openPaymentModal, + openDownpayment: openDownpaymentModal, close: closePaymentModal, submit: submitPayment, - removeInvoice: removeInvoiceFromPayment + addById: addInvoiceById, + removeInvoice: removeInvoice, + updateAmount: updatePayAmount, + updateTotal: updateTotal, + switchMode: switchMode }; \ No newline at end of file diff --git a/server.js b/server.js index 3c639cf..b8a717f 100644 --- a/server.js +++ b/server.js @@ -1612,12 +1612,12 @@ app.patch('/api/invoices/:id/reset-qbo', async (req, res) => { }); // ===================================================== -// QBO PAYMENT ENDPOINTS — In server.js einfügen -// Speichert Payments sowohl in lokaler DB als auch in QBO +// QBO PAYMENT ENDPOINTS v2 — In server.js einfügen +// Supports: multi-invoice, partial, unapplied (downpayment) // ===================================================== -// --- 1. Bank-Konten aus QBO laden (für "Deposit To" Dropdown) --- +// --- Bank-Konten aus QBO (für Deposit To) --- app.get('/api/qbo/accounts', async (req, res) => { try { const oauthClient = getOAuthClient(); @@ -1631,23 +1631,16 @@ app.get('/api/qbo/accounts', async (req, res) => { url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(query)}`, method: 'GET' }); - const data = response.getJson ? response.getJson() : response.json; - const accounts = (data.QueryResponse?.Account || []).map(acc => ({ - id: acc.Id, - name: acc.Name, - fullName: acc.FullyQualifiedName || acc.Name - })); - - res.json(accounts); + res.json((data.QueryResponse?.Account || []).map(a => ({ id: a.Id, name: a.Name }))); } catch (error) { console.error('Error fetching QBO accounts:', error); - res.status(500).json({ error: 'Error fetching bank accounts: ' + error.message }); + res.status(500).json({ error: error.message }); } }); -// --- 2. Payment Methods aus QBO laden --- +// --- Payment Methods aus QBO --- app.get('/api/qbo/payment-methods', async (req, res) => { try { const oauthClient = getOAuthClient(); @@ -1661,37 +1654,34 @@ app.get('/api/qbo/payment-methods', async (req, res) => { url: `${baseUrl}/v3/company/${companyId}/query?query=${encodeURI(query)}`, method: 'GET' }); - const data = response.getJson ? response.getJson() : response.json; - const methods = (data.QueryResponse?.PaymentMethod || []).map(pm => ({ - id: pm.Id, - name: pm.Name - })); - - res.json(methods); + res.json((data.QueryResponse?.PaymentMethod || []).map(p => ({ id: p.Id, name: p.Name }))); } catch (error) { console.error('Error fetching payment methods:', error); - res.status(500).json({ error: 'Error fetching payment methods: ' + error.message }); + res.status(500).json({ error: error.message }); } }); -// --- 3. Payment erstellen: Lokal + QBO --- +// --- Record Payment (multi-invoice, partial, unapplied) --- app.post('/api/qbo/record-payment', async (req, res) => { const { - invoice_ids, // Array von lokalen Invoice IDs - payment_date, // 'YYYY-MM-DD' - reference_number, // Check # oder ACH Referenz - payment_method_id, // QBO PaymentMethod ID - payment_method_name, // 'Check' oder 'ACH' (für lokale DB) - deposit_to_account_id, // QBO Bank Account ID - deposit_to_account_name // Bankname (für lokale DB) + mode, // 'invoice' | 'unapplied' + // Mode 'invoice': + invoice_payments, // [{ invoice_id, amount }] + // Mode 'unapplied': + customer_id, // Lokale Kunden-ID + customer_qbo_id, // QBO Customer ID + total_amount, // Betrag + // Gemeinsam: + payment_date, + reference_number, + payment_method_id, + payment_method_name, + deposit_to_account_id, + deposit_to_account_name } = req.body; - if (!invoice_ids || invoice_ids.length === 0) { - return res.status(400).json({ error: 'Keine Rechnungen ausgewählt.' }); - } - const dbClient = await pool.connect(); try { @@ -1701,54 +1691,93 @@ app.post('/api/qbo/record-payment', async (req, res) => { ? 'https://quickbooks.api.intuit.com' : 'https://sandbox-quickbooks.api.intuit.com'; - // Lokale Invoices laden - const invoicesResult = await dbClient.query( - `SELECT i.*, c.qbo_id as customer_qbo_id, c.name as customer_name - FROM invoices i - LEFT JOIN customers c ON i.customer_id = c.id - WHERE i.id = ANY($1)`, - [invoice_ids] - ); - const invoicesData = invoicesResult.rows; + let qboPayment; + let localCustomerId; + let totalAmt; + let invoicesData = []; - // Validierung - const notInQbo = invoicesData.filter(inv => !inv.qbo_id); - if (notInQbo.length > 0) { - return res.status(400).json({ - error: `Nicht in QBO: ${notInQbo.map(i => i.invoice_number || `ID:${i.id}`).join(', ')}` + if (mode === 'unapplied') { + // ---- DOWNPAYMENT: kein LinkedTxn ---- + if (!customer_qbo_id || !total_amount) { + return res.status(400).json({ error: 'Kunde und Betrag erforderlich.' }); + } + + localCustomerId = customer_id; + totalAmt = parseFloat(total_amount); + + qboPayment = { + CustomerRef: { value: customer_qbo_id }, + TotalAmt: totalAmt, + TxnDate: payment_date, + PaymentRefNum: reference_number || '', + PaymentMethodRef: { value: payment_method_id }, + DepositToAccountRef: { value: deposit_to_account_id } + // Kein Line[] → Unapplied Payment + }; + + console.log(`💰 Downpayment: $${totalAmt.toFixed(2)} für Kunde QBO ${customer_qbo_id}`); + + } else { + // ---- INVOICE PAYMENT (normal, partial, multi) ---- + if (!invoice_payments || invoice_payments.length === 0) { + return res.status(400).json({ error: 'Keine Rechnungen ausgewählt.' }); + } + + const ids = invoice_payments.map(ip => ip.invoice_id); + const result = await dbClient.query( + `SELECT i.*, c.qbo_id as customer_qbo_id, c.name as customer_name + FROM invoices i + LEFT JOIN customers c ON i.customer_id = c.id + WHERE i.id = ANY($1)`, [ids] + ); + invoicesData = result.rows; + + // Validierung + const notInQbo = invoicesData.filter(inv => !inv.qbo_id); + if (notInQbo.length > 0) { + return res.status(400).json({ + error: `Nicht in QBO: ${notInQbo.map(i => i.invoice_number || `ID:${i.id}`).join(', ')}` + }); + } + + const custIds = [...new Set(invoicesData.map(inv => inv.customer_qbo_id))]; + if (custIds.length > 1) { + return res.status(400).json({ error: 'Alle Rechnungen müssen zum selben Kunden gehören.' }); + } + + localCustomerId = invoicesData[0].customer_id; + + // Beträge zuordnen + const paymentMap = new Map(invoice_payments.map(ip => [ip.invoice_id, parseFloat(ip.amount)])); + totalAmt = invoice_payments.reduce((s, ip) => s + parseFloat(ip.amount), 0); + + qboPayment = { + CustomerRef: { value: custIds[0] }, + TotalAmt: totalAmt, + TxnDate: payment_date, + PaymentRefNum: reference_number || '', + PaymentMethodRef: { value: payment_method_id }, + DepositToAccountRef: { value: deposit_to_account_id }, + Line: invoicesData.map(inv => ({ + Amount: paymentMap.get(inv.id) || parseFloat(inv.total), + LinkedTxn: [{ TxnId: inv.qbo_id, TxnType: 'Invoice' }] + })) + }; + + const hasPartial = invoicesData.some(inv => { + const payAmt = paymentMap.get(inv.id) || 0; + return payAmt < parseFloat(inv.total); }); + + console.log(`💰 Payment: $${totalAmt.toFixed(2)} für ${invoicesData.length} Rechnung(en)${hasPartial ? ' (Teilzahlung)' : ''}`); } - const customerIds = [...new Set(invoicesData.map(inv => inv.customer_qbo_id))]; - if (customerIds.length > 1) { - return res.status(400).json({ error: 'Alle Rechnungen müssen zum selben Kunden gehören.' }); - } - - const customerQboId = customerIds[0]; - const customerId = invoicesData[0].customer_id; - const totalAmount = invoicesData.reduce((sum, inv) => sum + parseFloat(inv.total), 0); - - // ----- QBO Payment Objekt ----- - const payment = { - CustomerRef: { value: customerQboId }, - TotalAmt: totalAmount, - TxnDate: payment_date, - PaymentRefNum: reference_number || '', - PaymentMethodRef: { value: payment_method_id }, - DepositToAccountRef: { value: deposit_to_account_id }, - Line: invoicesData.map(inv => ({ - Amount: parseFloat(inv.total), - LinkedTxn: [{ TxnId: inv.qbo_id, TxnType: 'Invoice' }] - })) - }; - - console.log(`💰 QBO Payment: $${totalAmount.toFixed(2)} für ${invoicesData.length} Rechnung(en)`); - + // --- QBO senden --- const response = await makeQboApiCall({ url: `${baseUrl}/v3/company/${companyId}/payment`, method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payment) + body: JSON.stringify(qboPayment) }); const data = response.getJson ? response.getJson() : response.json; @@ -1756,45 +1785,58 @@ app.post('/api/qbo/record-payment', async (req, res) => { if (!data.Payment) { console.error('❌ QBO Payment Fehler:', JSON.stringify(data)); return res.status(500).json({ - error: 'QBO Payment fehlgeschlagen: ' + JSON.stringify(data.Fault?.Error?.[0]?.Message || data) + error: 'QBO Fehler: ' + (data.Fault?.Error?.[0]?.Message || JSON.stringify(data)) }); } const qboPaymentId = data.Payment.Id; console.log(`✅ QBO Payment ID: ${qboPaymentId}`); - // ----- Lokal in DB speichern ----- + // --- Lokal speichern --- await dbClient.query('BEGIN'); - // Payment-Datensatz - const paymentResult = await dbClient.query( + const payResult = await dbClient.query( `INSERT INTO payments (payment_date, reference_number, payment_method, deposit_to_account, total_amount, customer_id, qbo_payment_id) - VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, - [payment_date, reference_number || null, payment_method_name || 'Check', deposit_to_account_name || '', totalAmount, customerId, qboPaymentId] + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, + [payment_date, reference_number || null, payment_method_name || 'Check', + deposit_to_account_name || '', totalAmt, localCustomerId, qboPaymentId] ); - const localPaymentId = paymentResult.rows[0].id; + const localPaymentId = payResult.rows[0].id; - // Invoices mit Payment verknüpfen + als bezahlt markieren - for (const inv of invoicesData) { - await dbClient.query( - `INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3)`, - [localPaymentId, inv.id, parseFloat(inv.total)] - ); - await dbClient.query( - `UPDATE invoices SET paid_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2`, - [payment_date, inv.id] - ); + // Invoices verknüpfen + als bezahlt markieren + if (mode !== 'unapplied' && invoice_payments) { + for (const ip of invoice_payments) { + const payAmt = parseFloat(ip.amount); + const inv = invoicesData.find(i => i.id === ip.invoice_id); + const invTotal = inv ? parseFloat(inv.total) : 0; + const isFullyPaid = payAmt >= invTotal; + + await dbClient.query( + 'INSERT INTO payment_invoices (payment_id, invoice_id, amount) VALUES ($1, $2, $3)', + [localPaymentId, ip.invoice_id, payAmt] + ); + + if (isFullyPaid) { + // Voll bezahlt → paid_date setzen + await dbClient.query( + 'UPDATE invoices SET paid_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', + [payment_date, ip.invoice_id] + ); + } + // Teilzahlung → paid_date bleibt NULL (Rechnung noch offen) + } } await dbClient.query('COMMIT'); + const modeLabel = mode === 'unapplied' ? 'Downpayment' : 'Payment'; res.json({ success: true, payment_id: localPaymentId, qbo_payment_id: qboPaymentId, - total: totalAmount, - invoices_paid: invoicesData.length, - message: `Payment $${totalAmount.toFixed(2)} erfasst (QBO: ${qboPaymentId}, Lokal: ${localPaymentId}).` + total: totalAmt, + invoices_paid: mode === 'unapplied' ? 0 : invoice_payments.length, + message: `${modeLabel} $${totalAmt.toFixed(2)} erfasst (QBO: ${qboPaymentId}).` }); } catch (error) { @@ -1807,16 +1849,16 @@ app.post('/api/qbo/record-payment', async (req, res) => { }); -// --- 4. Lokale Payments auflisten (optional, für spätere Übersicht) --- +// --- Lokale Payments auflisten --- app.get('/api/payments', async (req, res) => { try { const result = await pool.query(` SELECT p.*, c.name as customer_name, - json_agg(json_build_object( + COALESCE(json_agg(json_build_object( 'invoice_id', pi.invoice_id, 'amount', pi.amount, 'invoice_number', i.invoice_number - )) as invoices + )) FILTER (WHERE pi.id IS NOT NULL), '[]') as invoices FROM payments p LEFT JOIN customers c ON p.customer_id = c.id LEFT JOIN payment_invoices pi ON pi.payment_id = p.id