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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user