279 lines
11 KiB
JavaScript
279 lines
11 KiB
JavaScript
// payment-modal.js — ES Module für das Payment Recording Modal
|
|
// Fixes: Correct CSS class 'modal', local DB payment storage
|
|
|
|
// ============================================================
|
|
// State
|
|
// ============================================================
|
|
let bankAccounts = [];
|
|
let paymentMethods = [];
|
|
let selectedInvoices = [];
|
|
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
|
|
// ============================================================
|
|
|
|
export async function openPaymentModal(invoiceIds = []) {
|
|
await loadQboData();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
ensureModalElement();
|
|
renderModalContent();
|
|
document.getElementById('payment-modal').classList.add('active');
|
|
}
|
|
|
|
export function closePaymentModal() {
|
|
const modal = document.getElementById('payment-modal');
|
|
if (modal) modal.classList.remove('active');
|
|
selectedInvoices = [];
|
|
}
|
|
|
|
// ============================================================
|
|
// DOM
|
|
// ============================================================
|
|
|
|
function ensureModalElement() {
|
|
let modal = document.getElementById('payment-modal');
|
|
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);
|
|
}
|
|
}
|
|
|
|
function renderModalContent() {
|
|
const modal = document.getElementById('payment-modal');
|
|
if (!modal) return;
|
|
|
|
const accountOptions = bankAccounts.map(acc =>
|
|
`<option value="${acc.id}">${acc.name}</option>`
|
|
).join('');
|
|
|
|
// Zeige Check und ACH bevorzugt, aber alle als Fallback
|
|
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 =>
|
|
`<option value="${pm.id}">${pm.name}</option>`
|
|
).join('');
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
modal.innerHTML = `
|
|
<div class="bg-white rounded-lg shadow-2xl w-full max-w-2xl mx-auto p-8">
|
|
<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</label>
|
|
<div id="payment-invoice-list" class="border border-gray-200 rounded-lg max-h-48 overflow-y-auto"></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}
|
|
</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)}`;
|
|
}
|
|
|
|
function removeInvoiceFromPayment(invoiceId) {
|
|
selectedInvoices = selectedInvoices.filter(inv => inv.id !== invoiceId);
|
|
renderInvoiceList();
|
|
updateTotal();
|
|
}
|
|
|
|
// ============================================================
|
|
// Submit
|
|
// ============================================================
|
|
|
|
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');
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
const submitBtn = document.getElementById('payment-submit-btn');
|
|
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,
|
|
payment_method_name: methodName,
|
|
deposit_to_account_id: depositToId,
|
|
deposit_to_account_name: depositToName
|
|
})
|
|
});
|
|
|
|
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 beim Payment.');
|
|
} finally {
|
|
submitBtn.innerHTML = '💰 Record Payment in QBO';
|
|
submitBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Expose
|
|
// ============================================================
|
|
|
|
window.paymentModal = {
|
|
open: openPaymentModal,
|
|
close: closePaymentModal,
|
|
submit: submitPayment,
|
|
removeInvoice: removeInvoiceFromPayment
|
|
}; |