update
This commit is contained in:
parent
444e8555f3
commit
7ba4eef5db
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
31
server.js
31
server.js
|
|
@ -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,25 +436,30 @@ 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
|
||||||
`, [id]);
|
`, [id]);
|
||||||
|
|
||||||
if (invoiceResult.rows.length === 0) {
|
if (invoiceResult.rows.length === 0) {
|
||||||
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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue