This commit is contained in:
2026-03-19 16:28:37 -05:00
parent 5a7ba66c27
commit 229e658831
9 changed files with 687 additions and 46 deletions

View File

@@ -184,14 +184,22 @@ function renderInvoiceRow(invoice) {
const balance = parseFloat(invoice.balance) ?? ((parseFloat(invoice.total) || 0) - amountPaid);
const partial = isPartiallyPaid(invoice);
const stripeIndicator = invoice.stripe_payment_link_id
? (invoice.stripe_payment_status === 'paid'
? ' <span title="Stripe payment received" class="text-purple-500 text-xs">💳✓</span>'
: ' <span title="Stripe payment link active" class="text-purple-400 text-xs">💳</span>')
: '';
const invNumDisplay = invoice.invoice_number
? invoice.invoice_number
? invoice.invoice_number + stripeIndicator
: `<span class="text-gray-400 italic text-xs">Draft</span>`;
// Status Badge (left side, next to invoice number)
let statusBadge = '';
if (paid && invoice.payment_status === 'Deposited') {
statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-800" title="Deposited ${formatDate(invoice.paid_date)}">Deposited</span>`;
} else if (paid && invoice.payment_status === 'Stripe') {
statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-purple-100 text-purple-800" title="Stripe payment ${formatDate(invoice.paid_date)}">Stripe</span>`;
} else 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 (partial) {
@@ -258,20 +266,29 @@ function renderInvoiceRow(invoice) {
// Mark Sent button (right side) — only when open, not paid/partial
let sendBtn = '';
// if (hasQbo && !paid && !overdue && invoice.email_status !== 'sent') {
// sendBtn = `<button onclick="window.invoiceView.setEmailStatus(${invoice.id}, 'sent')" class="text-indigo-600 hover:text-indigo-800 text-xs font-medium" title="Mark as sent to customer">📤 Mark Sent</button>`;
// }
if (hasQbo && !paid && !overdue) {
sendBtn = `
<button onclick="window.invoiceView.setEmailStatus(${invoice.id}, 'sent')" class="text-gray-600 hover:text-gray-900 text-xs font-medium mr-4" title="Nur Status ändern">
✔️ Mark Sent
</button>
<button onclick="window.emailModal.open(${invoice.id})" class="text-indigo-600 hover:text-indigo-800 text-xs font-medium" title="E-Mail via SES versenden">
📧 Send Email
</button>
`; }
if (hasQbo && !paid && !overdue && invoice.email_status !== 'sent') {
sendBtn = `<button onclick="window.invoiceView.setEmailStatus(${invoice.id}, 'sent')" class="text-indigo-600 hover:text-indigo-800 text-xs font-medium" title="Mark as sent to customer">📤 Mark Sent</button>`;
}
// if (hasQbo && !paid && !overdue) {
// sendBtn = `
// <button onclick="window.invoiceView.setEmailStatus(${invoice.id}, 'sent')" class="text-gray-600 hover:text-gray-900 text-xs font-medium mr-4" title="Nur Status ändern">
// ✔️ Mark Sent
// </button>
// <button onclick="window.emailModal.open(${invoice.id})" class="text-indigo-600 hover:text-indigo-800 text-xs font-medium" title="E-Mail via SES versenden">
// 📧 Send Email
// </button>
// `; }
const delBtn = `<button onclick="window.invoiceView.remove(${invoice.id})" class="text-red-600 hover:text-red-900">Del</button>`;
const stripeEmailBtn = (!paid && hasQbo)
? `<button onclick="window.emailModal.open(${invoice.id})" title="Email with Stripe Payment Link" class="px-2 py-1 bg-purple-100 text-purple-700 rounded hover:bg-purple-200 text-xs font-semibold">💳 Pay Link</button>`
: '';
const stripeCheckBtn = (invoice.stripe_payment_link_id && !paid)
? `<button onclick="window.invoiceView.checkStripePayment(${invoice.id})" title="Check Stripe Payment Status" class="px-2 py-1 bg-purple-50 text-purple-600 rounded hover:bg-purple-100 text-xs font-semibold">🔍 Check</button>`
: '';
const rowClass = paid ? (invoice.payment_status === 'Deposited' ? 'bg-blue-50/50' : 'bg-green-50/50') : partial ? 'bg-yellow-50/30' : overdue ? 'bg-red-50/50' : '';
return `
@@ -283,7 +300,7 @@ function renderInvoiceRow(invoice) {
<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">${amountDisplay}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium space-x-1">
${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${sendBtn} ${paidBtn} ${delBtn}
${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${sendBtn} ${stripeEmailBtn} ${stripeCheckBtn} ${paidBtn} ${delBtn}
</td>
</tr>`;
}
@@ -518,12 +535,55 @@ export async function remove(id) {
try { const r = await fetch(`/api/invoices/${id}`, { method: 'DELETE' }); if (r.ok) loadInvoices(); }
catch (e) { console.error(e); }
}
async function checkStripePayment(invoiceId) {
if (typeof showSpinner === 'function') showSpinner('Checking Stripe payment status...');
try {
const response = await fetch(`/api/invoices/${invoiceId}/check-payment`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (response.ok) {
if (result.paid) {
let msg = `${result.message}`;
if (result.qbo) {
if (result.qbo.error) {
msg += `\n\n⚠️ QBO booking failed: ${result.qbo.error}`;
} else {
msg += `\n\n📗 QBO Payment recorded (ID: ${result.qbo.paymentId})`;
if (result.qbo.feeBooked) msg += '\n📗 Processing fee booked';
}
}
if (!result.fullyPaid) {
msg += '\n\n⚠ Partial payment — invoice is not fully paid yet.';
}
alert(msg);
loadInvoices(); // Refresh the list
} else if (result.alreadyProcessed) {
alert(' Stripe payment was already recorded for this invoice.');
} else if (result.status === 'processing') {
alert('⏳ ACH payment is processing (typically 3-5 business days).\n\nCheck again later.');
} else {
alert(' No payment received yet.\n\nThe customer may not have clicked the payment link, or the payment is still being processed.');
}
} else {
alert(`❌ Error: ${result.error}`);
}
} catch (e) {
console.error('Check Stripe payment error:', e);
alert('Network error checking payment status.');
} finally {
if (typeof hideSpinner === 'function') hideSpinner();
}
}
// ============================================================
// Expose
// ============================================================
window.invoiceView = {
viewPDF, viewHTML, syncFromQBO, resetQbo, markPaid, setEmailStatus, edit, remove,
loadInvoices, renderInvoiceView, setStatus
loadInvoices, renderInvoiceView, setStatus, checkStripePayment
};