Add 4 AI bookkeeping features

Feature 7: Bank Rec Auto-Match — AiSuggestMatches endpoint scores uncleared
transactions vs statement ending balance; AI Auto-Match panel in Reconcile.cshtml
with confidence highlights and Apply All button.

Feature 8: Late Payment Prediction — PredictLatePayments endpoint scores open AR
customers by risk (high/medium/low) using historical avg-days-to-pay + late rate;
rendered as badge table in AR Aging view via ar-aging-ai.js.

Feature 9: Natural Language Financial Queries — FinancialQuery GET page + RunFinancialQuery
POST; 12-month context snapshot pre-loaded; answers grounded in real data with
supporting facts, follow-up suggestions, session history, and example chips.

Feature 10: Recurring Bill Detection — RunRecurringDetection scans 12 months of bills
for vendor payment patterns (monthly/quarterly/annual); card grid view in Bills/RecurringDetection.cshtml
with confidence badges, next-expected-date, and suggested actions.

Supporting: 4 new DTO groups in AccountingAiDtos.cs, 4 method signatures in
IAccountingAiService.cs, 4 implementations in AccountingAiService.cs, 4 new
AiFeatures constants, 2 new Landing page AI report cards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 19:22:49 -04:00
parent e2f9e9ae4f
commit 959e323f3a
16 changed files with 1679 additions and 4 deletions
@@ -115,6 +115,27 @@
</div>
</div>
<!-- AI Auto-Match panel -->
<div class="card shadow-sm mb-3 border-0 bg-light">
<div class="card-body d-flex align-items-center gap-3 flex-wrap">
<div>
<span class="fw-semibold"><i class="bi bi-robot text-primary me-1"></i>AI Auto-Match</span>
<span class="text-muted small ms-2">Let Claude suggest which transactions to clear based on amounts and dates.</span>
</div>
<button id="aiMatchBtn" class="btn btn-outline-primary btn-sm ms-auto" type="button">
<i class="bi bi-magic me-1"></i>Suggest Matches
</button>
</div>
<div id="aiMatchResult" class="d-none px-3 pb-3">
<div id="aiMatchInsights" class="mb-2 text-muted small"></div>
<div id="aiMatchActions" class="d-flex gap-2 flex-wrap">
<button id="aiMatchAccept" class="btn btn-sm btn-success d-none">
<i class="bi bi-check-all me-1"></i>Apply All Suggestions
</button>
</div>
</div>
</div>
<form asp-action="Complete" method="post" id="completeForm">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@recon?.Id" />
@@ -184,6 +205,88 @@
});
recalculate();
// ── AI Auto-Match ──────────────────────────────────────────────────────────
let aiSuggestions = [];
document.getElementById('aiMatchBtn')?.addEventListener('click', async function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Analyzing…';
try {
const resp = await fetch('/BankReconciliations/AiSuggestMatches', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'RequestVerificationToken': token
},
body: new URLSearchParams({ reconId })
});
const data = await resp.json();
const resultEl = document.getElementById('aiMatchResult');
const insightsEl = document.getElementById('aiMatchInsights');
resultEl.classList.remove('d-none');
if (!data.success) {
insightsEl.innerHTML = `<span class="text-danger"><i class="bi bi-exclamation-triangle me-1"></i>${data.errorMessage || 'AI unavailable.'}</span>`;
return;
}
aiSuggestions = data.suggestedCleared || [];
// Highlight suggested rows
aiSuggestions.forEach(s => {
const row = document.querySelector(`.recon-row[data-type="${s.entityType}"][data-id="${s.entityId}"]`);
if (row) {
row.classList.add('table-info');
const td = row.querySelector('td:last-child');
if (td) {
const pct = Math.round(s.confidence * 100);
td.insertAdjacentHTML('afterend', `<td class="small text-info" style="white-space:nowrap">${pct}% — ${s.reason}</td>`);
}
}
});
// Insights
const insights = data.insights || [];
insightsEl.innerHTML = insights.map(i => `<i class="bi bi-lightbulb me-1 text-warning"></i>${i}`).join('<br>');
if (aiSuggestions.length > 0) {
document.getElementById('aiMatchAccept').classList.remove('d-none');
} else {
insightsEl.innerHTML += '<br><span class="text-muted">No high-confidence suggestions found — review items manually.</span>';
}
} catch (err) {
document.getElementById('aiMatchInsights').innerHTML = '<span class="text-danger">Error contacting AI service.</span>';
document.getElementById('aiMatchResult').classList.remove('d-none');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-magic me-1"></i>Suggest Matches';
}
});
document.getElementById('aiMatchAccept')?.addEventListener('click', async function() {
for (const s of aiSuggestions) {
const row = document.querySelector(`.recon-row[data-type="${s.entityType}"][data-id="${s.entityId}"]`);
if (!row) continue;
const cb = row.querySelector('.cleared-checkbox');
if (!cb || cb.checked) continue;
cb.checked = true;
// Persist via the existing toggle endpoint
try {
await fetch('/BankReconciliations/ToggleCleared', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'RequestVerificationToken': token },
body: new URLSearchParams({ reconId, entityType: s.entityType, entityId: s.entityId, isCleared: true })
});
} catch {}
}
recalculate();
this.textContent = 'Applied';
this.disabled = true;
});
})();
</script>
}