invoice-system/public/payment-modal.js

356 lines
14 KiB
JavaScript

// payment-modal.js — ES Module für das Payment Recording Modal
// Ermöglicht: Auswahl mehrerer Rechnungen, Check/ACH, Deposit To Konto
// ============================================================
// State
// ============================================================
let bankAccounts = [];
let paymentMethods = [];
let selectedInvoices = []; // Array of invoice objects
let isOpen = false;
// Cache QBO reference data (nur einmal laden)
let dataLoaded = false;
// ============================================================
// Load QBO Reference 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);
}
}
// ============================================================
// Open / Close Modal
// ============================================================
export async function openPaymentModal(invoiceIds = []) {
// Lade QBO-Daten falls noch nicht geschehen
await loadQboData();
// Lade die ausgewählten Rechnungen
if (invoiceIds.length > 0) {
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);
}
} catch (e) {
console.error('Error loading invoice:', id, e);
}
}
}
renderModal();
document.getElementById('payment-modal').classList.add('active');
isOpen = true;
}
export function closePaymentModal() {
const modal = document.getElementById('payment-modal');
if (modal) modal.classList.remove('active');
isOpen = false;
selectedInvoices = [];
}
// ============================================================
// Add/Remove Invoices from selection
// ============================================================
export async function addInvoiceToPayment(invoiceId) {
if (selectedInvoices.find(inv => inv.id === invoiceId)) return; // already selected
try {
const res = await fetch(`/api/invoices/${invoiceId}`);
const data = await res.json();
if (data.invoice) {
// Validierung: Muss QBO-verknüpft sein
if (!data.invoice.qbo_id) {
alert('Diese Rechnung ist noch nicht in QBO. Bitte erst exportieren.');
return;
}
// Validierung: Alle müssen zum selben Kunden gehören
if (selectedInvoices.length > 0 && data.invoice.customer_id !== selectedInvoices[0].customer_id) {
alert('Alle Rechnungen eines Payments müssen zum selben Kunden gehören.');
return;
}
selectedInvoices.push(data.invoice);
renderInvoiceList();
updateTotal();
}
} catch (e) {
console.error('Error adding invoice:', e);
}
}
function removeInvoiceFromPayment(invoiceId) {
selectedInvoices = selectedInvoices.filter(inv => inv.id !== invoiceId);
renderInvoiceList();
updateTotal();
}
// ============================================================
// Rendering
// ============================================================
function renderModal() {
let modal = document.getElementById('payment-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'payment-modal';
modal.className = 'modal-overlay';
document.body.appendChild(modal);
}
const accountOptions = bankAccounts.map(acc =>
`<option value="${acc.id}">${acc.name}</option>`
).join('');
const methodOptions = paymentMethods
.filter(pm => ['Check', 'ACH'].includes(pm.name) || pm.name.toLowerCase().includes('check') || pm.name.toLowerCase().includes('ach'))
.map(pm => `<option value="${pm.id}">${pm.name}</option>`)
.join('');
// Falls keine Filter-Treffer, alle anzeigen
const allMethodOptions = paymentMethods.map(pm =>
`<option value="${pm.id}">${pm.name}</option>`
).join('');
const today = new Date().toISOString().split('T')[0];
modal.innerHTML = `
<div class="modal-content" style="max-width: 700px;">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-800">💰 Record Payment</h2>
<button onclick="window.paymentModal.close()" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Selected Invoices -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Invoices to pay</label>
<div id="payment-invoice-list" class="border border-gray-200 rounded-lg max-h-48 overflow-y-auto">
<!-- Wird dynamisch gefüllt -->
</div>
<div class="mt-2 flex items-center gap-2">
<input type="number" id="payment-add-invoice-id" placeholder="Invoice ID hinzufügen..."
class="flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm">
<button onclick="window.paymentModal.addById()"
class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700">
+ Add
</button>
</div>
</div>
<!-- Payment Details -->
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Payment Date</label>
<input type="date" id="payment-date" value="${today}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Reference # (Check / ACH)</label>
<input type="text" id="payment-reference" placeholder="Check # or ACH ref"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Payment Method</label>
<select id="payment-method"
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:ring-blue-500 focus:border-blue-500">
${methodOptions || allMethodOptions}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Deposit To</label>
<select id="payment-deposit-to"
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:ring-blue-500 focus:border-blue-500">
${accountOptions}
</select>
</div>
</div>
<!-- Total -->
<div class="bg-gray-50 p-4 rounded-lg mb-6">
<div class="flex justify-between items-center">
<span class="text-lg font-bold text-gray-700">Total Payment:</span>
<span id="payment-total" class="text-2xl font-bold text-blue-600">$0.00</span>
</div>
</div>
<!-- Actions -->
<div class="flex justify-end space-x-3">
<button onclick="window.paymentModal.close()"
class="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300">
Cancel
</button>
<button onclick="window.paymentModal.submit()" id="payment-submit-btn"
class="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-semibold">
💰 Record Payment in QBO
</button>
</div>
</div>
`;
renderInvoiceList();
updateTotal();
}
function renderInvoiceList() {
const container = document.getElementById('payment-invoice-list');
if (!container) return;
if (selectedInvoices.length === 0) {
container.innerHTML = `<div class="p-4 text-center text-gray-400 text-sm">Keine Rechnungen ausgewählt</div>`;
return;
}
container.innerHTML = selectedInvoices.map(inv => `
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100 last:border-0 hover:bg-gray-50">
<div>
<span class="font-medium text-gray-900">#${inv.invoice_number || 'Draft'}</span>
<span class="text-gray-500 text-sm ml-2">${inv.customer_name || ''}</span>
</div>
<div class="flex items-center gap-3">
<span class="font-semibold text-gray-900">$${parseFloat(inv.total).toFixed(2)}</span>
<button onclick="window.paymentModal.removeInvoice(${inv.id})"
class="text-red-400 hover:text-red-600 text-sm">✕</button>
</div>
</div>
`).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)}`;
// Submit-Button deaktivieren wenn keine Rechnungen
const submitBtn = document.getElementById('payment-submit-btn');
if (submitBtn) {
submitBtn.disabled = selectedInvoices.length === 0;
submitBtn.classList.toggle('opacity-50', selectedInvoices.length === 0);
}
}
// ============================================================
// Submit Payment
// ============================================================
async function submitPayment() {
if (selectedInvoices.length === 0) {
alert('Bitte mindestens eine Rechnung auswählen.');
return;
}
const paymentDate = document.getElementById('payment-date').value;
const reference = document.getElementById('payment-reference').value;
const methodId = document.getElementById('payment-method').value;
const depositToId = document.getElementById('payment-deposit-to').value;
if (!paymentDate) {
alert('Bitte ein Zahlungsdatum angeben.');
return;
}
if (!methodId || !depositToId) {
alert('Bitte Payment Method und Deposit To auswählen.');
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 von $${total.toFixed(2)} für Rechnung(en) ${invoiceNums} an QBO senden?`)) {
return;
}
const submitBtn = document.getElementById('payment-submit-btn');
const origText = submitBtn.innerHTML;
submitBtn.innerHTML = '⏳ Wird gesendet...';
submitBtn.disabled = true;
try {
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,
deposit_to_account_id: depositToId
})
});
const result = await response.json();
if (response.ok) {
alert(`${result.message}`);
closePaymentModal();
// Invoice-Liste aktualisieren
if (window.invoiceView) {
window.invoiceView.loadInvoices();
} else if (typeof window.loadInvoices === 'function') {
window.loadInvoices();
}
} else {
alert(`❌ Fehler: ${result.error}`);
}
} catch (error) {
console.error('Payment error:', error);
alert('Netzwerkfehler beim Payment.');
} finally {
submitBtn.innerHTML = origText;
submitBtn.disabled = false;
}
}
// ============================================================
// Helper: Add by ID from input field
// ============================================================
async function addInvoiceById() {
const input = document.getElementById('payment-add-invoice-id');
const id = parseInt(input.value);
if (!id) return;
await addInvoiceToPayment(id);
input.value = '';
}
// ============================================================
// Expose to window
// ============================================================
window.paymentModal = {
open: openPaymentModal,
close: closePaymentModal,
submit: submitPayment,
addInvoice: addInvoiceToPayment,
removeInvoice: removeInvoiceFromPayment,
addById: addInvoiceById
};