This commit is contained in:
2026-02-19 22:02:22 -06:00
parent 49aeff8cb6
commit 410faee6d1
4 changed files with 142 additions and 149 deletions

View File

@@ -216,8 +216,10 @@ async function loadCustomers() {
}
}
// --- 1. renderCustomers() — ERSETZE komplett ---
// Zeigt QBO-Status, Credit-Betrag und Downpayment-Button
// =====================================================
// 1. renderCustomers() — ERSETZE komplett
// Zeigt QBO-Status und Export-Button in der Kundenliste
// =====================================================
function renderCustomers() {
const tbody = document.getElementById('customers-list');
@@ -228,41 +230,25 @@ function renderCustomers() {
if (cityStateZip) fullAddress += (fullAddress ? ', ' : '') + cityStateZip;
// QBO Status
let qboCol;
if (customer.qbo_id) {
qboCol = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800" title="QBO ID: ${customer.qbo_id}">QBO ✓</span>`;
} else {
qboCol = `<button onclick="exportCustomerToQbo(${customer.id})" class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-100 text-orange-800 hover:bg-orange-200 cursor-pointer">QBO Export</button>`;
}
// Downpayment button (only if in QBO)
const downpayBtn = customer.qbo_id
? `<button onclick="openDownpaymentModal(${customer.id}, '${customer.qbo_id}', '${customer.name.replace(/'/g, "\\'")}')" class="text-emerald-600 hover:text-emerald-800">Downpayment</button>`
: '';
// Credit placeholder (loaded async)
const creditSpan = customer.qbo_id
? `<span id="customer-credit-${customer.id}" class="text-xs text-gray-400">...</span>`
: '';
const qboStatus = customer.qbo_id
? `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800" title="QBO ID: ${customer.qbo_id}">QBO ✓</span>`
: `<button onclick="exportCustomerToQbo(${customer.id})" class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-100 text-orange-800 hover:bg-orange-200 cursor-pointer" title="Kunde nach QBO exportieren">QBO Export</button>`;
return `
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
${customer.name} ${qboCol} ${creditSpan}
${customer.name} ${qboStatus}
</td>
<td class="px-6 py-4 text-sm text-gray-500">${fullAddress || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${customer.account_number || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button onclick="editCustomer(${customer.id})" class="text-blue-600 hover:text-blue-900">Edit</button>
${downpayBtn}
<button onclick="deleteCustomer(${customer.id})" class="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>`;
}).join('');
// Load credits async for QBO customers
loadCustomerCredits();
}
// --- 2. Credits async laden ---
async function loadCustomerCredits() {
const qboCustomers = customers.filter(c => c.qbo_id);

View File

@@ -167,13 +167,14 @@ function renderInvoiceRow(invoice) {
// --- BUTTONS (Edit | QBO | PDF HTML | Payment | Del) ---
const editBtn = `<button onclick="window.invoiceView.edit(${invoice.id})" class="text-blue-600 hover:text-blue-900">Edit</button>`;
// QBO Button — nur aktiv wenn Kunde eine qbo_id hat
// QBO Button — Export oder Sync
const customerHasQbo = !!invoice.customer_qbo_id;
let qboBtn;
if (hasQbo) {
qboBtn = `<span class="text-gray-400 text-xs cursor-pointer" title="In QBO (ID: ${invoice.qbo_id}) — Click to reset" onclick="window.invoiceView.resetQbo(${invoice.id})">✓ QBO</span>`;
// Already in QBO — show sync button + reset option
qboBtn = `<button onclick="window.invoiceView.syncToQBO(${invoice.id})" class="text-purple-600 hover:text-purple-900" title="Sync changes to QBO (ID: ${invoice.qbo_id})">⟳ QBO Sync</button>`;
} else if (!customerHasQbo) {
qboBtn = `<span class="text-gray-300 text-xs cursor-not-allowed" title="Kunde muss erst nach QBO exportiert werden">QBO ⚠</span>`;
qboBtn = `<span class="text-gray-300 text-xs cursor-not-allowed" title="Customer must be exported to QBO first">QBO ⚠</span>`;
} else {
qboBtn = `<button onclick="window.invoiceView.exportToQBO(${invoice.id})" class="text-orange-600 hover:text-orange-900" title="Export to QuickBooks">QBO Export</button>`;
}
@@ -349,6 +350,18 @@ export async function exportToQBO(id) {
finally { if (typeof hideSpinner === 'function') hideSpinner(); }
}
export async function syncToQBO(id) {
if (!confirm('Sync changes to QuickBooks Online?')) return;
if (typeof showSpinner === 'function') showSpinner('Syncing invoice to QBO...');
try {
const r = await fetch(`/api/invoices/${id}/update-qbo`, { method: 'POST' });
const d = await r.json();
if (r.ok) { alert(`${d.message}`); loadInvoices(); }
else alert(`${d.error}`);
} catch (e) { alert('Network error.'); }
finally { if (typeof hideSpinner === 'function') hideSpinner(); }
}
export async function resetQbo(id) {
if (!confirm('QBO-Verknüpfung zurücksetzen?\nRechnung muss zuerst in QBO gelöscht sein!')) return;
try {
@@ -385,6 +398,6 @@ export async function remove(id) {
// ============================================================
window.invoiceView = {
viewPDF, viewHTML, exportToQBO, resetQbo, markPaid, markUnpaid, edit, remove,
viewPDF, viewHTML, exportToQBO, syncToQBO, resetQbo, markPaid, markUnpaid, edit, remove,
loadInvoices, renderInvoiceView, setStatus
};

View File

@@ -1,11 +1,10 @@
// payment-modal.js — ES Module v3
// Invoice payments only: multi-invoice, partial, editable amounts
// Downpayment is handled separately in customer view
// payment-modal.js — ES Module v3 (clean)
// Invoice payments: multi-invoice, partial, overpay
// No downpayment functionality
let bankAccounts = [];
let paymentMethods = [];
let selectedInvoices = []; // { invoice, payAmount }
let customerCredit = 0; // Unapplied credit from QBO
let dataLoaded = false;
// ============================================================
@@ -32,7 +31,6 @@ async function loadQboData() {
export async function openPaymentModal(invoiceIds = []) {
await loadQboData();
selectedInvoices = [];
customerCredit = 0;
for (const id of invoiceIds) {
try {
@@ -47,18 +45,6 @@ export async function openPaymentModal(invoiceIds = []) {
} catch (e) { console.error('Error loading invoice:', id, e); }
}
// Check customer credit if we have invoices
if (selectedInvoices.length > 0) {
const custQboId = selectedInvoices[0].invoice.customer_qbo_id;
if (custQboId) {
try {
const res = await fetch(`/api/qbo/customer-credit/${custQboId}`);
const data = await res.json();
customerCredit = data.credit || 0;
} catch (e) { /* ignore */ }
}
}
ensureModalElement();
renderModalContent();
document.getElementById('payment-modal').classList.add('active');
@@ -68,7 +54,6 @@ export function closePaymentModal() {
const modal = document.getElementById('payment-modal');
if (modal) modal.classList.remove('active');
selectedInvoices = [];
customerCredit = 0;
}
// ============================================================
@@ -146,22 +131,10 @@ function renderModalContent() {
const accountOptions = bankAccounts.map(a => `<option value="${a.id}">${a.name}</option>`).join('');
const filtered = paymentMethods.filter(p => /check|ach/i.test(p.name));
const methods = (filtered.length > 0 ? filtered : paymentMethods);
const methods = filtered.length > 0 ? filtered : paymentMethods;
const methodOptions = methods.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
const today = new Date().toISOString().split('T')[0];
// Credit banner
let creditBanner = '';
if (customerCredit > 0) {
creditBanner = `
<div class="bg-green-50 border border-green-200 rounded-lg p-3 mb-4">
<p class="text-sm text-green-800">
💰 <strong>Customer has $${customerCredit.toFixed(2)} unapplied credit.</strong>
This can be applied in QBO when processing the payment.
</p>
</div>`;
}
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-4">
@@ -173,8 +146,6 @@ function renderModalContent() {
</button>
</div>
${creditBanner}
<!-- Invoice List -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Invoices</label>
@@ -285,13 +256,11 @@ function updateTotal() {
const payTotal = selectedInvoices.reduce((s, si) => s + si.payAmount, 0);
const invTotal = selectedInvoices.reduce((s, si) => s + parseFloat(si.invoice.total), 0);
totalEl.textContent = `$${payTotal.toFixed(2)}`;
if (noteEl) {
if (payTotal > invTotal && invTotal > 0) {
const overpay = payTotal - invTotal;
noteEl.textContent = `⚠️ Overpayment of $${overpay.toFixed(2)} will be stored as customer credit in QBO.`;
noteEl.textContent = `⚠️ Overpayment of $${(payTotal - invTotal).toFixed(2)} will be stored as customer credit in QBO.`;
noteEl.classList.remove('hidden');
} else {
noteEl.classList.add('hidden');
@@ -336,7 +305,6 @@ async function submitPayment() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: 'invoice',
invoice_payments: selectedInvoices.map(si => ({
invoice_id: si.invoice.id,
amount: si.payAmount