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>
}
@@ -5,7 +5,10 @@
ViewData["PageIcon"] = "bi-receipt-cutoff";
}
<div class="d-flex justify-content-end mb-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<a asp-action="RecurringDetection" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-robot me-1"></i>Detect Recurring Bills
</a>
<div class="btn-group">
<a asp-controller="Bills" asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>New Bill
@@ -0,0 +1,58 @@
@{
ViewData["Title"] = "Recurring Bill Detection";
ViewData["PageIcon"] = "bi-arrow-repeat";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="fw-semibold mb-1"><i class="bi bi-arrow-repeat text-primary me-2"></i>Recurring Bill Detection</h4>
<p class="text-muted small mb-0">Claude analyzes your last 12 months of bills to find recurring payment patterns and help you anticipate upcoming expenses.</p>
</div>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Bills
</a>
</div>
<form id="scanForm" method="post" asp-action="RunRecurringDetection">
@Html.AntiForgeryToken()
<div class="card shadow-sm mb-4 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 Analysis</span>
<span class="text-muted small ms-2">Scans up to 12 months of bills grouped by vendor to detect patterns.</span>
</div>
<button id="scanBtn" type="submit" class="btn btn-primary ms-auto">
<i class="bi bi-magic me-1"></i>Detect Recurring Bills
</button>
</div>
</div>
</form>
<div id="resultArea" class="d-none">
<div id="spinnerArea" class="text-center py-5 d-none">
<div class="spinner-border text-primary" style="width:2.5rem;height:2.5rem;" role="status"></div>
<p class="text-muted mt-3">Claude is reviewing your bill history…</p>
</div>
<div id="errorArea" class="alert alert-danger alert-permanent d-none"></div>
<div id="insightsArea" class="alert alert-info alert-permanent d-none mb-3">
<i class="bi bi-lightbulb me-2"></i><span id="insightsList"></span>
</div>
<div id="noPatterns" class="card shadow-sm d-none">
<div class="card-body text-center py-5 text-muted">
<i class="bi bi-search fs-1 d-block mb-2"></i>
<p class="mb-0 fw-semibold">No recurring patterns detected</p>
<p class="small">Need at least 2 occurrences of a vendor bill at a similar cadence. Add more bill history and try again.</p>
</div>
</div>
<div id="patternsArea" class="d-none">
<div class="row g-3" id="patternCards"></div>
</div>
</div>
@section Scripts {
<script src="/js/recurring-detection.js"></script>
}
@@ -240,3 +240,42 @@ else
<i class="bi bi-info-circle me-1"></i>
Generated @DateTime.Now.ToString("MMM d, yyyy h:mm tt") · Includes all open invoices (excluding Draft and Voided). Age calculated from due date.
</div>
@if (Model.Customers.Any())
{
<!-- AI Late Payment Prediction -->
<div class="card shadow-sm mt-4 border-0 no-print" id="aiRiskCard">
<div class="card-header d-flex align-items-center gap-2">
<i class="bi bi-robot text-primary"></i>
<span class="fw-semibold">AI Payment Risk Prediction</span>
<button id="aiRiskBtn" class="btn btn-sm btn-outline-primary ms-auto">
<i class="bi bi-magic me-1"></i>Predict Payment Risk
</button>
</div>
<div class="card-body d-none" id="aiRiskBody">
<div id="aiRiskSpinner" class="text-center py-3 d-none">
<div class="spinner-border text-primary" role="status"></div>
<p class="text-muted mt-2 small">Claude is analyzing payment behavior…</p>
</div>
<div id="aiRiskError" class="alert alert-danger alert-permanent d-none"></div>
<div id="aiRiskInsights" class="text-muted small mb-3"></div>
<div id="aiRiskTable" class="table-responsive d-none">
<table class="table table-sm align-middle">
<thead class="table-light">
<tr>
<th>Customer</th>
<th>Risk</th>
<th>Est. Days to Payment</th>
<th>Reasoning</th>
</tr>
</thead>
<tbody id="aiRiskRows"></tbody>
</table>
</div>
</div>
</div>
}
@section Scripts {
<script src="/js/ar-aging-ai.js"></script>
}
@@ -0,0 +1,122 @@
@using System.Text.Json
@{
ViewData["Title"] = "Ask Your Financials";
ViewData["PageIcon"] = "bi-chat-dots";
var context = ViewBag.Context as PowderCoating.Application.DTOs.AI.FinancialQueryContext;
var contextJson = JsonSerializer.Serialize(context);
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="fw-semibold mb-1"><i class="bi bi-chat-dots text-primary me-2"></i>Ask Your Financials</h4>
<p class="text-muted small mb-0">Ask Claude a plain-English question about your business finances. Data as of @DateTime.Today.ToString("MMMM d, yyyy").</p>
</div>
<a asp-action="Landing" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Reports
</a>
</div>
<div class="row g-4">
<div class="col-lg-8">
<!-- Query input -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<label class="form-label fw-semibold">Your question</label>
<div class="input-group">
<input type="text" id="queryInput" class="form-control form-control-lg"
placeholder="e.g. What did we spend on powder last quarter?"
autocomplete="off" />
<button id="queryBtn" class="btn btn-primary px-4" type="button">
<i class="bi bi-send me-1"></i>Ask
</button>
</div>
<div class="mt-2 d-flex flex-wrap gap-2" id="suggestionChips">
<span class="text-muted small me-1">Try:</span>
</div>
</div>
</div>
<!-- Answer area -->
<div id="answerArea" class="d-none">
<div class="card shadow-sm border-primary border-opacity-25">
<div class="card-header bg-primary-subtle text-primary-emphasis fw-semibold">
<i class="bi bi-robot me-1"></i>Claude's Answer
</div>
<div class="card-body">
<div id="answerSpinner" class="text-center py-3 d-none">
<div class="spinner-border text-primary" role="status"></div>
<p class="text-muted mt-2 small">Analyzing your financials…</p>
</div>
<div id="answerError" class="alert alert-danger alert-permanent d-none"></div>
<p id="answerText" class="mb-3 fs-6 d-none"></p>
<div id="factsArea" class="d-none">
<p class="small fw-semibold text-muted mb-1">Supporting data:</p>
<ul id="factsList" class="list-unstyled small text-muted mb-0"></ul>
</div>
<div id="followUpArea" class="mt-3 pt-3 border-top d-none">
<span class="text-muted small me-2">Follow-up suggestion:</span>
<button id="followUpBtn" class="btn btn-sm btn-outline-primary"></button>
</div>
</div>
</div>
<!-- History -->
<div id="historyArea" class="mt-3 d-none">
<p class="text-muted small fw-semibold mb-2"><i class="bi bi-clock-history me-1"></i>Earlier questions this session</p>
<div id="historyList" class="d-flex flex-column gap-2"></div>
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Snapshot -->
<div class="card shadow-sm mb-3">
<div class="card-header fw-semibold text-muted small">
<i class="bi bi-graph-up me-1"></i>YTD Snapshot
</div>
<div class="card-body pb-2">
<div class="d-flex justify-content-between small mb-2">
<span class="text-muted">Revenue</span>
<span class="fw-medium">@context?.TotalRevenueYtd.ToString("C0")</span>
</div>
<div class="d-flex justify-content-between small mb-2">
<span class="text-muted">Expenses</span>
<span class="fw-medium">@context?.TotalExpensesYtd.ToString("C0")</span>
</div>
<div class="d-flex justify-content-between small mb-2 border-top pt-2">
<span class="fw-semibold">Net Income</span>
<span class="fw-bold @(context?.NetIncomeYtd >= 0 ? "text-success" : "text-danger")">
@context?.NetIncomeYtd.ToString("C0")
</span>
</div>
<div class="d-flex justify-content-between small mb-2 border-top pt-2">
<span class="text-muted">AR Outstanding</span>
<span class="fw-medium text-warning">@context?.ArOutstanding.ToString("C0")</span>
</div>
<div class="d-flex justify-content-between small mb-2">
<span class="text-muted">AP Outstanding</span>
<span class="fw-medium text-danger">@context?.ApOutstanding.ToString("C0")</span>
</div>
</div>
</div>
<!-- Tips -->
<div class="card shadow-sm border-0 bg-light">
<div class="card-body small">
<p class="fw-semibold text-muted mb-2"><i class="bi bi-lightbulb me-1 text-warning"></i>Tips</p>
<ul class="list-unstyled text-muted mb-0 small">
<li class="mb-1">Ask about specific time periods: "last month", "Q1", "this year"</li>
<li class="mb-1">Compare periods: "compared to last quarter"</li>
<li class="mb-1">Ask about vendors, categories, or customers</li>
<li>Claude only uses data it was given — it won't invent figures</li>
</ul>
</div>
</div>
</div>
</div>
<input type="hidden" id="contextJson" value="@Html.Raw(System.Text.Encodings.Web.HtmlEncoder.Default.Encode(contextJson))" />
@section Scripts {
<script src="/js/financial-query.js"></script>
}
@@ -123,6 +123,22 @@
<p>AI scans recent bills and expense trends for duplicate entries, unusual amounts, and accounts running over their historical average.</p>
<div class="report-arrow">Run analysis <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="FinancialQuery" class="report-card">
<div class="report-card-icon" style="background:#eff6ff;color:#2563eb;">
<i class="bi bi-chat-dots"></i>
</div>
<h5>Ask Your Financials</h5>
<p>Ask Claude plain-English questions about your revenue, expenses, and AR. Answers are grounded in your actual financial data.</p>
<div class="report-arrow">Ask a question <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="ArAging" class="report-card">
<div class="report-card-icon" style="background:#fef3c7;color:#d97706;">
<i class="bi bi-hourglass-split"></i>
</div>
<h5>AR Aging + Risk Prediction</h5>
<p>View outstanding invoices by aging bucket, then run AI payment risk scoring to prioritize your follow-up calls.</p>
<div class="report-arrow">View aging <i class="bi bi-arrow-right"></i></div>
</a>
</div>
</div>