diff --git a/public/js/modals/invoice-modal.js b/public/js/modals/invoice-modal.js index 7a38b55..cbde3b6 100644 --- a/public/js/modals/invoice-modal.js +++ b/public/js/modals/invoice-modal.js @@ -42,6 +42,67 @@ function applyCustomerTaxStatus(customerId) { } } +function updateRecurringChildUi(invoice = null) { + const recurringCb = document.getElementById('invoice-recurring'); + if (!recurringCb) return; + + const recurringWrapper = recurringCb.closest('label') || recurringCb.parentElement; + if (!recurringWrapper) return; + + let labelText = document.getElementById('invoice-recurring-label-text'); + let childBadge = document.getElementById('invoice-recurring-child-badge'); + let childNote = document.getElementById('invoice-recurring-child-note'); + + // First-time setup: wrap/change the visible label text + if (!labelText) { + const textNode = Array.from(recurringWrapper.childNodes).find(node => + node.nodeType === Node.TEXT_NODE && node.textContent.trim().includes('Recurring') + ); + + if (textNode) { + const span = document.createElement('span'); + span.id = 'invoice-recurring-label-text'; + span.textContent = 'Recurring'; + recurringWrapper.replaceChild(span, textNode); + labelText = span; + } + } + + if (!childBadge) { + childBadge = document.createElement('span'); + childBadge.id = 'invoice-recurring-child-badge'; + childBadge.className = 'ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800 hidden'; + childBadge.textContent = 'Child invoice'; + recurringWrapper.appendChild(childBadge); + } + + if (!childNote) { + childNote = document.createElement('div'); + childNote.id = 'invoice-recurring-child-note'; + childNote.className = 'mt-1 ml-8 text-xs text-gray-500 hidden'; + recurringWrapper.parentElement.appendChild(childNote); + } + + const isChild = !!invoice?.recurring_source_id; + + if (isChild) { + labelText.textContent = 'Recurring child invoice'; + childBadge.classList.remove('hidden'); + + const sourceNumber = invoice.recurring_source_invoice_number + || invoice.source_invoice_number + || invoice.recurring_source_id; + + childNote.textContent = `This invoice was generated from recurring invoice #${sourceNumber} and will not create further recurring invoices.`; + childNote.classList.remove('hidden'); + } else { + labelText.textContent = 'Recurring'; + childBadge.classList.add('hidden'); + childNote.classList.add('hidden'); + childNote.textContent = ''; + } +} + export async function openInvoiceModal(invoiceId = null) { currentInvoiceId = invoiceId; if (invoiceId) { @@ -112,6 +173,9 @@ async function loadInvoiceForEdit(invoiceId) { if (recurringGroup) { recurringGroup.style.display = recurringCb.checked ? 'block' : 'none'; } + + // Make recurring child invoices obvious in the UI + updateRecurringChildUi(data.invoice); } // Load items @@ -136,6 +200,11 @@ function prepareNewInvoice() { const recurringGroup = document.getElementById('invoice-recurring-group'); if (recurringCb) recurringCb.checked = false; if (recurringGroup) recurringGroup.style.display = 'none'; + + const recurringInterval = document.getElementById('invoice-recurring-interval'); + if (recurringCb) recurringCb.disabled = false; + if (recurringInterval) recurringInterval.disabled = false; + updateRecurringChildUi(null); resetItemCounter(); setDefaultDate(); diff --git a/src/routes/invoices.js b/src/routes/invoices.js index 7207e4e..37ac2e5 100644 --- a/src/routes/invoices.js +++ b/src/routes/invoices.js @@ -84,11 +84,30 @@ router.get('/:id', async (req, res) => { const { id } = req.params; try { const invoiceResult = await pool.query(` - SELECT i.*, c.name as customer_name, c.qbo_id as customer_qbo_id, - c.email, 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 + SELECT + i.*, + c.name as customer_name, + c.qbo_id as customer_qbo_id, + c.email, + c.line1, + c.line2, + c.line3, + c.line4, + c.city, + c.state, + c.zip_code, + c.account_number, + src.invoice_number AS recurring_source_invoice_number, + COALESCE(( + SELECT SUM(pi.amount) + FROM payment_invoices pi + WHERE pi.invoice_id = i.id + ), 0) as amount_paid FROM invoices i - LEFT JOIN customers c ON i.customer_id = c.id + LEFT JOIN customers c + ON i.customer_id = c.id + LEFT JOIN invoices src + ON src.id = i.recurring_source_id WHERE i.id = $1 `, [id]); @@ -269,7 +288,7 @@ router.put('/:id', async (req, res) => { [customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, scheduled_send_date || null, bill_to_name || null, id] ); } - + // Preserve existing next_recurring_date when editing an already-recurring invoice. // Otherwise editing an old invoice can move next_recurring_date backwards and create duplicates. const existingRecurringResult = await client.query(