This commit is contained in:
2026-05-04 14:54:16 -05:00
parent fcfc66a3b9
commit 6735118b2f
2 changed files with 93 additions and 5 deletions

View File

@@ -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) { export async function openInvoiceModal(invoiceId = null) {
currentInvoiceId = invoiceId; currentInvoiceId = invoiceId;
if (invoiceId) { if (invoiceId) {
@@ -112,6 +173,9 @@ async function loadInvoiceForEdit(invoiceId) {
if (recurringGroup) { if (recurringGroup) {
recurringGroup.style.display = recurringCb.checked ? 'block' : 'none'; recurringGroup.style.display = recurringCb.checked ? 'block' : 'none';
} }
// Make recurring child invoices obvious in the UI
updateRecurringChildUi(data.invoice);
} }
// Load items // Load items
@@ -136,6 +200,11 @@ function prepareNewInvoice() {
const recurringGroup = document.getElementById('invoice-recurring-group'); const recurringGroup = document.getElementById('invoice-recurring-group');
if (recurringCb) recurringCb.checked = false; if (recurringCb) recurringCb.checked = false;
if (recurringGroup) recurringGroup.style.display = 'none'; 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(); resetItemCounter();
setDefaultDate(); setDefaultDate();

View File

@@ -84,11 +84,30 @@ router.get('/:id', async (req, res) => {
const { id } = req.params; const { id } = req.params;
try { try {
const invoiceResult = await pool.query(` const invoiceResult = await pool.query(`
SELECT i.*, c.name as customer_name, c.qbo_id as customer_qbo_id, SELECT
c.email, c.line1, c.line2, c.line3, c.line4, c.city, c.state, c.zip_code, c.account_number, i.*,
COALESCE((SELECT SUM(pi.amount) FROM payment_invoices pi WHERE pi.invoice_id = i.id), 0) as amount_paid 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 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 WHERE i.id = $1
`, [id]); `, [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] [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. // 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. // Otherwise editing an old invoice can move next_recurring_date backwards and create duplicates.
const existingRecurringResult = await client.query( const existingRecurringResult = await client.query(