This commit is contained in:
Andreas Knuth 2026-02-20 10:09:01 -06:00
parent 444e8555f3
commit 7ba4eef5db
3 changed files with 53 additions and 14 deletions

View File

@ -147,7 +147,12 @@ function renderInvoiceRow(invoice) {
: `<span class="text-gray-400 italic text-xs">Draft</span>`; : `<span class="text-gray-400 italic text-xs">Draft</span>`;
let statusBadge = ''; let statusBadge = '';
const amountPaid = parseFloat(invoice.amount_paid) || 0;
const balance = parseFloat(invoice.balance) ?? (parseFloat(invoice.total) - amountPaid);
const isPartiallyPaid = !paid && amountPaid > 0 && balance > 0;
if (paid) statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800" title="Paid ${formatDate(invoice.paid_date)}">Paid</span>`; if (paid) statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800" title="Paid ${formatDate(invoice.paid_date)}">Paid</span>`;
else if (isPartiallyPaid) statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800" title="Paid: $${amountPaid.toFixed(2)} / Balance: $${balance.toFixed(2)}">Partial $${amountPaid.toFixed(2)}</span>`;
else if (overdue) statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-red-100 text-red-800" title="${daysSince(invoice.invoice_date)} days">Overdue</span>`; else if (overdue) statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-red-100 text-red-800" title="${daysSince(invoice.invoice_date)} days">Overdue</span>`;
// Send Date // Send Date
@ -203,7 +208,12 @@ function renderInvoiceRow(invoice) {
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${formatDate(invoice.invoice_date)}</td> <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${formatDate(invoice.invoice_date)}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${sendDateDisplay}</td> <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${sendDateDisplay}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${invoice.terms}</td> <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${invoice.terms}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 font-semibold">$${parseFloat(invoice.total).toFixed(2)}</td> <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 font-semibold">
${isPartiallyPaid
? `<span class="text-yellow-700">$${balance.toFixed(2)}</span> <span class="text-gray-400 text-xs line-through">$${parseFloat(invoice.total).toFixed(2)}</span>`
: `$${parseFloat(invoice.total).toFixed(2)}`
}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium space-x-1"> <td class="px-4 py-3 whitespace-nowrap text-sm font-medium space-x-1">
${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${paidBtn} ${delBtn} ${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${paidBtn} ${delBtn}
</td> </td>

View File

@ -37,9 +37,12 @@ export async function openPaymentModal(invoiceIds = []) {
const res = await fetch(`/api/invoices/${id}`); const res = await fetch(`/api/invoices/${id}`);
const data = await res.json(); const data = await res.json();
if (data.invoice) { if (data.invoice) {
const total = parseFloat(data.invoice.total);
const amountPaid = parseFloat(data.invoice.amount_paid) || 0;
const balance = total - amountPaid;
selectedInvoices.push({ selectedInvoices.push({
invoice: data.invoice, invoice: data.invoice,
payAmount: parseFloat(data.invoice.total) payAmount: balance > 0 ? balance : total
}); });
} }
} catch (e) { console.error('Error loading invoice:', id, e); } } catch (e) { console.error('Error loading invoice:', id, e); }
@ -82,9 +85,13 @@ async function addInvoiceById() {
const detailRes = await fetch(`/api/invoices/${match.id}`); const detailRes = await fetch(`/api/invoices/${match.id}`);
const detailData = await detailRes.json(); const detailData = await detailRes.json();
const detailInv = detailData.invoice;
const detailTotal = parseFloat(detailInv.total);
const detailPaid = parseFloat(detailInv.amount_paid) || 0;
const detailBalance = detailTotal - detailPaid;
selectedInvoices.push({ selectedInvoices.push({
invoice: detailData.invoice, invoice: detailInv,
payAmount: parseFloat(detailData.invoice.total) payAmount: detailBalance > 0 ? detailBalance : detailTotal
}); });
renderInvoiceList(); renderInvoiceList();
@ -223,8 +230,14 @@ function renderInvoiceList() {
container.innerHTML = selectedInvoices.map(si => { container.innerHTML = selectedInvoices.map(si => {
const inv = si.invoice; const inv = si.invoice;
const total = parseFloat(inv.total); const total = parseFloat(inv.total);
const isPartial = si.payAmount < total; const amountPaid = parseFloat(inv.amount_paid) || 0;
const isOver = si.payAmount > total; const balance = total - amountPaid;
const isPartial = si.payAmount < balance;
const isOver = si.payAmount > balance;
const paidInfo = amountPaid > 0
? `<span class="text-green-600 text-xs ml-1">Paid: $${amountPaid.toFixed(2)}</span>`
: '';
return ` return `
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-100 last:border-0 hover:bg-gray-50"> <div class="flex items-center justify-between px-4 py-3 border-b border-gray-100 last:border-0 hover:bg-gray-50">
@ -232,6 +245,7 @@ function renderInvoiceList() {
<span class="font-medium text-gray-900">#${inv.invoice_number || 'Draft'}</span> <span class="font-medium text-gray-900">#${inv.invoice_number || 'Draft'}</span>
<span class="text-gray-500 text-sm ml-2 truncate">${inv.customer_name || ''}</span> <span class="text-gray-500 text-sm ml-2 truncate">${inv.customer_name || ''}</span>
<span class="text-gray-400 text-xs ml-2">(Total: $${total.toFixed(2)})</span> <span class="text-gray-400 text-xs ml-2">(Total: $${total.toFixed(2)})</span>
${paidInfo}
${isPartial ? '<span class="text-xs text-yellow-600 ml-1 font-semibold">Partial</span>' : ''} ${isPartial ? '<span class="text-xs text-yellow-600 ml-1 font-semibold">Partial</span>' : ''}
${isOver ? '<span class="text-xs text-blue-600 ml-1 font-semibold">Overpay</span>' : ''} ${isOver ? '<span class="text-xs text-blue-600 ml-1 font-semibold">Overpay</span>' : ''}
</div> </div>

View File

@ -403,12 +403,19 @@ app.delete('/api/quotes/:id', async (req, res) => {
app.get('/api/invoices', async (req, res) => { app.get('/api/invoices', async (req, res) => {
try { try {
const result = await pool.query(` const result = await pool.query(`
SELECT i.*, c.name as customer_name, c.qbo_id as customer_qbo_id SELECT i.*, c.name as customer_name, c.qbo_id as customer_qbo_id,
COALESCE((SELECT SUM(pi.amount) FROM payment_invoices pi WHERE pi.invoice_id = i.id), 0) as amount_paid
FROM invoices i FROM invoices i
LEFT JOIN customers c ON i.customer_id = c.id LEFT JOIN customers c ON i.customer_id = c.id
ORDER BY i.created_at DESC ORDER BY i.created_at DESC
`); `);
res.json(result.rows); // balance berechnen
const rows = result.rows.map(r => ({
...r,
amount_paid: parseFloat(r.amount_paid) || 0,
balance: (parseFloat(r.total) || 0) - (parseFloat(r.amount_paid) || 0)
}));
res.json(rows);
} catch (error) { } catch (error) {
console.error('Error fetching invoices:', error); console.error('Error fetching invoices:', error);
res.status(500).json({ error: 'Error fetching invoices' }); res.status(500).json({ error: 'Error fetching invoices' });
@ -429,9 +436,10 @@ app.get('/api/invoices/next-number', async (req, res) => {
app.get('/api/invoices/:id', async (req, res) => { app.get('/api/invoices/:id', async (req, res) => {
const { id } = req.params; const { id } = req.params;
try { try {
// KORRIGIERT: c.line1, c.line2, c.line3, c.line4 statt c.street
const invoiceResult = await pool.query(` const invoiceResult = await pool.query(`
SELECT i.*, c.name as customer_name, c.line1, c.line2, c.line3, c.line4, c.city, c.state, c.zip_code, c.account_number SELECT i.*, c.name as customer_name, c.qbo_id as customer_qbo_id,
c.line1, c.line2, c.line3, c.line4, c.city, c.state, c.zip_code, c.account_number,
COALESCE((SELECT SUM(pi.amount) FROM payment_invoices pi WHERE pi.invoice_id = i.id), 0) as amount_paid
FROM invoices i FROM invoices i
LEFT JOIN customers c ON i.customer_id = c.id LEFT JOIN customers c ON i.customer_id = c.id
WHERE i.id = $1 WHERE i.id = $1
@ -441,13 +449,17 @@ app.get('/api/invoices/:id', async (req, res) => {
return res.status(404).json({ error: 'Invoice not found' }); return res.status(404).json({ error: 'Invoice not found' });
} }
const invoice = invoiceResult.rows[0];
invoice.amount_paid = parseFloat(invoice.amount_paid) || 0;
invoice.balance = (parseFloat(invoice.total) || 0) - invoice.amount_paid;
const itemsResult = await pool.query( const itemsResult = await pool.query(
'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order', 'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order',
[id] [id]
); );
res.json({ res.json({
invoice: invoiceResult.rows[0], invoice: invoice,
items: itemsResult.rows items: itemsResult.rows
}); });
} catch (error) { } catch (error) {
@ -456,6 +468,9 @@ app.get('/api/invoices/:id', async (req, res) => {
} }
}); });
app.post('/api/invoices', async (req, res) => { app.post('/api/invoices', async (req, res) => {
const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, created_from_quote_id, scheduled_send_date } = req.body; const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, created_from_quote_id, scheduled_send_date } = req.body;