// payment-modal.js — ES Module v2 // Supports: Multi-invoice, partial payments, unapplied (downpayments), editable amounts let bankAccounts = []; let paymentMethods = []; let selectedInvoices = []; // { invoice, payAmount } let dataLoaded = false; let paymentMode = 'invoice'; // 'invoice' | 'unapplied' // ============================================================ // 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 (e) { console.error('Error loading QBO data:', e); } } // ============================================================ // Open / Close // ============================================================ 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({ invoice: data.invoice, payAmount: parseFloat(data.invoice.total) }); } } 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 // ============================================================ function ensureModalElement() { let modal = document.getElementById('payment-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'payment-modal'; 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); } } function renderModalContent() { const modal = document.getElementById('payment-modal'); if (!modal) return; const accountOptions = bankAccounts.map(acc => `` ).join(''); 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 => `` ).join(''); const today = new Date().toISOString().split('T')[0]; const isUnapplied = paymentMode === 'unapplied'; const title = isUnapplied ? '💰 Record Downpayment' : '💰 Record Payment'; modal.innerHTML = `

${title}

${isUnapplied ? renderUnappliedSection() : renderInvoiceSection()}
Total Payment: $0.00
`; 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 — bitte unten hinzufügen
`; return; } 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' : ''}
$
`; }).join(''); } function updateTotal() { const totalEl = document.getElementById('payment-total'); if (!totalEl) return; 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)}`; } // ============================================================ // Submit // ============================================================ async function submitPayment() { const paymentDate = document.getElementById('payment-date').value; const reference = document.getElementById('payment-reference').value; const methodSelect = document.getElementById('payment-method'); const depositSelect = document.getElementById('payment-deposit-to'); const methodId = methodSelect.value; const methodName = methodSelect.options[methodSelect.selectedIndex]?.text || ''; const depositToId = depositSelect.value; const depositToName = depositSelect.options[depositSelect.selectedIndex]?.text || ''; if (!paymentDate || !methodId || !depositToId) { alert('Bitte alle Felder ausfüllen.'); 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...'; submitBtn.disabled = true; if (typeof showSpinner === 'function') showSpinner('Erstelle Payment in QBO...'); try { const response = await fetch('/api/qbo/record-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const result = await response.json(); if (response.ok) { alert(`✅ ${result.message}`); closePaymentModal(); if (window.invoiceView) window.invoiceView.loadInvoices(); } else { alert(`❌ Fehler: ${result.error}`); } } catch (error) { console.error('Payment error:', error); alert('Netzwerkfehler.'); } finally { submitBtn.innerHTML = '💰 Record Payment in QBO'; submitBtn.disabled = false; if (typeof hideSpinner === 'function') hideSpinner(); } } function switchMode(mode) { paymentMode = mode; renderModalContent(); } // ============================================================ // Expose // ============================================================ window.paymentModal = { open: openPaymentModal, openDownpayment: openDownpaymentModal, close: closePaymentModal, submit: submitPayment, addById: addInvoiceById, removeInvoice: removeInvoice, updateAmount: updatePayAmount, updateTotal: updateTotal, switchMode: switchMode };