Phase G: Add Recurring Transactions (BackgroundService + CRUD UI)

- RecurringTemplate entity with Frequency/IntervalCount/NextFireDate/EndDate/MaxOccurrences/TemplateData JSON
- RecurringFrequency + RecurringTemplateType enums
- RecurringTransactionService BackgroundService: hourly check, creates Draft bills or immediate expenses, advances NextFireDate, auto-deactivates on limits
- RecurringTemplatesController: Index/Create/Edit/ToggleActive/Delete/GenerateNow (on-demand fire)
- Three views + external JS for type-toggle and dynamic bill line items
- Finance sidebar nav: Recurring Transactions
- Migration: AddRecurringTemplates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:08:36 -04:00
parent d3a5d827f9
commit 42eff3357e
16 changed files with 12046 additions and 6 deletions
@@ -0,0 +1,56 @@
// Recurring template create/edit form — type toggle + dynamic bill line items
function initRecurringForm(initialType) {
const radios = document.querySelectorAll('input[name="TemplateType"]');
radios.forEach(r => r.addEventListener('change', () => updateSections(parseInt(r.value))));
updateSections(initialType);
}
function updateSections(type) {
const billSection = document.getElementById('billSection');
const expSection = document.getElementById('expenseSection');
if (type === 1) {
billSection.classList.remove('d-none');
expSection.classList.add('d-none');
} else {
billSection.classList.add('d-none');
expSection.classList.remove('d-none');
}
}
function addBillLine() {
const container = document.getElementById('billLines');
const idx = container.querySelectorAll('.bill-line').length;
const accountOptions = (window.allExpenseAccounts || [])
.map(a => `<option value="${a.value}">${a.text}</option>`)
.join('');
const html = `
<div class="row g-2 mb-2 bill-line">
<div class="col-4">
<select name="LineItems[${idx}].AccountId" class="form-select form-select-sm">
<option value="">— Account —</option>${accountOptions}
</select>
</div>
<div class="col-4">
<input name="LineItems[${idx}].Description" type="text" class="form-control form-control-sm"
placeholder="Description" />
</div>
<div class="col-1">
<input name="LineItems[${idx}].Quantity" type="number" step="0.01" min="0"
class="form-control form-control-sm" placeholder="Qty" value="1" />
</div>
<div class="col-2">
<input name="LineItems[${idx}].UnitPrice" type="number" step="0.01" min="0"
class="form-control form-control-sm" placeholder="Price" />
</div>
<div class="col-1">
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="this.closest('.bill-line').remove()">
<i class="bi bi-x"></i>
</button>
</div>
</div>`;
container.insertAdjacentHTML('beforeend', html);
}