This commit is contained in:
2026-02-18 10:09:57 -06:00
parent acb588425a
commit 48fa86916b
4 changed files with 276 additions and 575 deletions

View File

@@ -1,15 +1,12 @@
// payment-modal.js — ES Module für das Payment Recording Modal
// Ermöglicht: Auswahl mehrerer Rechnungen, Check/ACH, Deposit To Konto
// Fixes: Correct CSS class 'modal', local DB payment storage
// ============================================================
// State
// ============================================================
let bankAccounts = [];
let paymentMethods = [];
let selectedInvoices = []; // Array of invoice objects
let isOpen = false;
// Cache QBO reference data (nur einmal laden)
let selectedInvoices = [];
let dataLoaded = false;
// ============================================================
@@ -35,109 +32,72 @@ async function loadQboData() {
}
// ============================================================
// Open / Close Modal
// Open / Close
// ============================================================
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);
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();
ensureModalElement();
renderModalContent();
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
// DOM
// ============================================================
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() {
function ensureModalElement() {
let modal = document.getElementById('payment-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'payment-modal';
modal.className = 'modal-overlay';
// 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('');
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 =>
// 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="modal-content" style="max-width: 700px;">
<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">
@@ -149,18 +109,8 @@ function renderModal() {
<!-- 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>
<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 -->
@@ -179,7 +129,7 @@ function renderModal() {
<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}
${methodOptions}
</select>
</div>
<div>
@@ -244,52 +194,43 @@ function renderInvoiceList() {
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);
}
}
function removeInvoiceFromPayment(invoiceId) {
selectedInvoices = selectedInvoices.filter(inv => inv.id !== invoiceId);
renderInvoiceList();
updateTotal();
}
// ============================================================
// Submit Payment
// Submit
// ============================================================
async function submitPayment() {
if (selectedInvoices.length === 0) {
alert('Bitte mindestens eine Rechnung auswählen.');
return;
}
if (selectedInvoices.length === 0) 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;
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) {
alert('Bitte ein Zahlungsdatum angeben.');
return;
}
if (!methodId || !depositToId) {
alert('Bitte Payment Method und Deposit To auswählen.');
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 von $${total.toFixed(2)} für Rechnung(en) ${invoiceNums} an QBO senden?`)) {
return;
}
if (!confirm(`Payment $${total.toFixed(2)} für #${invoiceNums} an QBO senden?`)) return;
const submitBtn = document.getElementById('payment-submit-btn');
const origText = submitBtn.innerHTML;
submitBtn.innerHTML = '⏳ Wird gesendet...';
submitBtn.disabled = true;
@@ -302,7 +243,9 @@ async function submitPayment() {
payment_date: paymentDate,
reference_number: reference,
payment_method_id: methodId,
deposit_to_account_id: depositToId
payment_method_name: methodName,
deposit_to_account_id: depositToId,
deposit_to_account_name: depositToName
})
});
@@ -311,12 +254,7 @@ async function submitPayment() {
if (response.ok) {
alert(`${result.message}`);
closePaymentModal();
// Invoice-Liste aktualisieren
if (window.invoiceView) {
window.invoiceView.loadInvoices();
} else if (typeof window.loadInvoices === 'function') {
window.loadInvoices();
}
if (window.invoiceView) window.invoiceView.loadInvoices();
} else {
alert(`❌ Fehler: ${result.error}`);
}
@@ -324,33 +262,18 @@ async function submitPayment() {
console.error('Payment error:', error);
alert('Netzwerkfehler beim Payment.');
} finally {
submitBtn.innerHTML = origText;
submitBtn.innerHTML = '💰 Record Payment in QBO';
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
// Expose
// ============================================================
window.paymentModal = {
open: openPaymentModal,
close: closePaymentModal,
submit: submitPayment,
addInvoice: addInvoiceToPayment,
removeInvoice: removeInvoiceFromPayment,
addById: addInvoiceById
removeInvoice: removeInvoiceFromPayment
};