diff --git a/public/js/modals/invoice-modal.js b/public/js/modals/invoice-modal.js index 7afd681..7a38b55 100644 --- a/public/js/modals/invoice-modal.js +++ b/public/js/modals/invoice-modal.js @@ -98,9 +98,20 @@ async function loadInvoiceForEdit(invoiceId) { const recurringInterval = document.getElementById('invoice-recurring-interval'); const recurringGroup = document.getElementById('invoice-recurring-group'); if (recurringCb) { - recurringCb.checked = data.invoice.is_recurring || false; - if (recurringInterval) recurringInterval.value = data.invoice.recurring_interval || 'monthly'; - if (recurringGroup) recurringGroup.style.display = data.invoice.is_recurring ? 'block' : 'none'; + const isGeneratedRecurringChild = !!data.invoice.recurring_source_id; + const canBeRecurringMaster = !isGeneratedRecurringChild; + + recurringCb.checked = canBeRecurringMaster && (data.invoice.is_recurring || false); + recurringCb.disabled = !canBeRecurringMaster; + + if (recurringInterval) { + recurringInterval.value = data.invoice.recurring_interval || 'monthly'; + recurringInterval.disabled = !canBeRecurringMaster; + } + + if (recurringGroup) { + recurringGroup.style.display = recurringCb.checked ? 'block' : 'none'; + } } // Load items diff --git a/src/routes/invoices.js b/src/routes/invoices.js index a746dbd..7207e4e 100644 --- a/src/routes/invoices.js +++ b/src/routes/invoices.js @@ -269,11 +269,46 @@ 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] ); } - const next_recurring_date = is_recurring ? calculateNextRecurringDate(invoice_date, recurring_interval) : null; - await client.query( - 'UPDATE invoices SET is_recurring = $1, recurring_interval = $2, next_recurring_date = $3 WHERE id = $4', - [is_recurring || false, recurring_interval || null, next_recurring_date, 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( + 'SELECT is_recurring, next_recurring_date, recurring_source_id FROM invoices WHERE id = $1', + [id] ); + + const existingRecurring = existingRecurringResult.rows[0]; + + let next_recurring_date = null; + + // Automatically generated child invoices should not become recurring masters. + // recurring_source_id != null means: this invoice was created from another recurring invoice. + const isGeneratedRecurringChild = !!existingRecurring?.recurring_source_id; + + if (isGeneratedRecurringChild) { + next_recurring_date = null; + } else if (is_recurring) { + next_recurring_date = existingRecurring?.next_recurring_date + ? existingRecurring.next_recurring_date + : calculateNextRecurringDate(invoice_date, recurring_interval); + } + + await client.query( + ` + UPDATE invoices + SET is_recurring = $1, + recurring_interval = $2, + next_recurring_date = $3 + WHERE id = $4 + `, + [ + isGeneratedRecurringChild ? false : (is_recurring || false), + isGeneratedRecurringChild ? null : (recurring_interval || null), + next_recurring_date, + id + ] + ); + // Delete and re-insert items await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]); for (let i = 0; i < items.length; i++) {