// 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 = `
${isUnapplied ? renderUnappliedSection() : renderInvoiceSection()}
`;
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 `
`;
}).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
};