This commit is contained in:
2026-02-19 21:27:03 -06:00
parent 171450400a
commit 49aeff8cb6
3 changed files with 486 additions and 360 deletions

View File

@@ -216,6 +216,9 @@ async function loadCustomers() {
}
}
// --- 1. renderCustomers() — ERSETZE komplett ---
// Zeigt QBO-Status, Credit-Betrag und Downpayment-Button
function renderCustomers() {
const tbody = document.getElementById('customers-list');
tbody.innerHTML = customers.map(customer => {
@@ -225,25 +228,198 @@ function renderCustomers() {
if (cityStateZip) fullAddress += (fullAddress ? ', ' : '') + cityStateZip;
// QBO Status
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>`;
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>`
: '';
return `
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
${customer.name} ${qboStatus}
${customer.name} ${qboCol} ${creditSpan}
</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);
for (const cust of qboCustomers) {
const span = document.getElementById(`customer-credit-${cust.id}`);
if (!span) continue;
try {
const res = await fetch(`/api/qbo/customer-credit/${cust.qbo_id}`);
const data = await res.json();
if (data.credit > 0) {
span.innerHTML = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">Credit: $${data.credit.toFixed(2)}</span>`;
} else {
span.textContent = '';
}
} catch (e) {
span.textContent = '';
}
}
}
// --- 3. Downpayment Dialog ---
async function openDownpaymentModal(customerId, customerQboId, customerName) {
// Load QBO data if needed
let bankAccounts = [];
let paymentMethods = [];
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();
} catch (e) { console.error('Error loading QBO data:', e); }
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 methodOptions = methods.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
const today = new Date().toISOString().split('T')[0];
let modal = document.getElementById('downpayment-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'downpayment-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);
}
modal.innerHTML = `
<div class="bg-white rounded-lg shadow-2xl w-full max-w-lg mx-auto p-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-gray-800">💰 Record Downpayment</h2>
<button onclick="closeDownpaymentModal()" 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>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-4">
<p class="text-sm text-blue-800">
<strong>Customer:</strong> ${customerName}<br>
This will record an unapplied payment (credit) on the customer's QBO account.
</p>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Amount</label>
<input type="number" id="dp-amount" step="0.01" min="0.01" placeholder="0.00"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 text-lg font-semibold">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Payment Date</label>
<input type="date" id="dp-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 #</label>
<input type="text" id="dp-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="dp-method" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-white">${methodOptions}</select>
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Deposit To</label>
<select id="dp-deposit" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-white">${accountOptions}</select>
</div>
</div>
<div class="flex justify-end space-x-3">
<button onclick="closeDownpaymentModal()" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300">Cancel</button>
<button onclick="submitDownpayment(${customerId}, '${customerQboId}')" id="dp-submit-btn"
class="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-semibold">
💰 Record Downpayment
</button>
</div>
</div>`;
modal.classList.add('active');
document.getElementById('dp-amount').focus();
}
function closeDownpaymentModal() {
const modal = document.getElementById('downpayment-modal');
if (modal) modal.classList.remove('active');
}
async function submitDownpayment(customerId, customerQboId) {
const amount = parseFloat(document.getElementById('dp-amount').value);
const date = document.getElementById('dp-date').value;
const ref = document.getElementById('dp-reference').value;
const methodSelect = document.getElementById('dp-method');
const depositSelect = document.getElementById('dp-deposit');
if (!amount || amount <= 0) { alert('Please enter an amount.'); return; }
if (!date || !methodSelect.value || !depositSelect.value) { alert('Please fill in all fields.'); return; }
if (!confirm(`Record downpayment of $${amount.toFixed(2)}?`)) return;
const btn = document.getElementById('dp-submit-btn');
btn.innerHTML = '⏳ Processing...';
btn.disabled = true;
if (typeof showSpinner === 'function') showSpinner('Recording downpayment in QBO...');
try {
const response = await fetch('/api/qbo/record-downpayment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customer_id: customerId,
customer_qbo_id: customerQboId,
amount: amount,
payment_date: date,
reference_number: ref,
payment_method_id: methodSelect.value,
payment_method_name: methodSelect.options[methodSelect.selectedIndex]?.text || '',
deposit_to_account_id: depositSelect.value,
deposit_to_account_name: depositSelect.options[depositSelect.selectedIndex]?.text || ''
})
});
const result = await response.json();
if (response.ok) {
alert(`${result.message}`);
closeDownpaymentModal();
renderCustomers(); // Refresh credit display
} else {
alert(`❌ Error: ${result.error}`);
}
} catch (e) {
alert('Network error.');
} finally {
btn.innerHTML = '💰 Record Downpayment';
btn.disabled = false;
if (typeof hideSpinner === 'function') hideSpinner();
}
}
function openCustomerModal(customerId = null) {
currentCustomerId = customerId;
const modal = document.getElementById('customer-modal');