959e323f3a
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>
282 lines
13 KiB
Plaintext
282 lines
13 KiB
Plaintext
@model PowderCoating.Application.DTOs.Accounting.ArAgingReportDto
|
||
@{
|
||
ViewData["Title"] = "AR Aging";
|
||
ViewData["PageIcon"] = "bi-hourglass-split";
|
||
var today = DateTime.Today;
|
||
}
|
||
|
||
<style>
|
||
@@media print {
|
||
.no-print { display: none !important; }
|
||
.card { border: 1px solid #dee2e6 !important; box-shadow: none !important; }
|
||
body { font-size: 11px; }
|
||
.table { font-size: 11px; }
|
||
}
|
||
.aging-current { color: #198754; }
|
||
.aging-1-30 { color: #fd7e14; }
|
||
.aging-31-60 { color: #dc6c02; }
|
||
.aging-61-90 { color: #dc3545; }
|
||
.aging-over90 { color: #842029; font-weight: 700; }
|
||
.customer-row { background: #f8f9fa; font-weight: 600; }
|
||
</style>
|
||
|
||
<!-- Header -->
|
||
<div class="d-flex align-items-center gap-2 mb-3 no-print">
|
||
<a asp-action="Index" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
|
||
<p class="text-muted mb-0">As of @Model.AsOf.ToString("MMMM d, yyyy") · @Model.Customers.Sum(c => c.Invoices.Count) open invoices</p>
|
||
<div class="ms-auto d-flex gap-2">
|
||
<a href="@Url.Action("ArAgingPdf", new { asOf = Model.AsOf.ToString("yyyy-MM-dd") })"
|
||
class="btn btn-sm btn-outline-danger no-print" target="_blank">
|
||
<i class="bi bi-file-pdf me-1"></i>Download PDF
|
||
</a>
|
||
<a href="@Url.Action("ArAgingPdf", new { asOf = Model.AsOf.ToString("yyyy-MM-dd"), inline = true })"
|
||
class="btn btn-sm btn-outline-secondary no-print" target="_blank">
|
||
<i class="bi bi-printer me-1"></i>Print
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Date filter -->
|
||
<div class="card shadow-sm mb-4 no-print">
|
||
<div class="card-body py-3">
|
||
<form method="get" class="row g-2 align-items-end">
|
||
<div class="col-auto">
|
||
<label class="form-label form-label-sm mb-1">As of Date</label>
|
||
<input type="date" name="asOf" class="form-control form-control-sm" value="@Model.AsOf.ToString("yyyy-MM-dd")" />
|
||
</div>
|
||
<div class="col-auto">
|
||
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-funnel me-1"></i>Run Report</button>
|
||
</div>
|
||
<div class="col-auto ms-2">
|
||
<div class="btn-group btn-group-sm">
|
||
<a href="@Url.Action("ArAging", new { asOf = today.ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">Today</a>
|
||
<a href="@Url.Action("ArAging", new { asOf = new DateTime(today.Year, today.Month, 1).AddDays(-1).ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">End of Last Month</a>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Print header -->
|
||
<div class="text-center mb-4 d-none d-print-block">
|
||
<h4 class="fw-bold">@Model.CompanyName</h4>
|
||
<h5>Accounts Receivable Aging</h5>
|
||
<p class="text-muted">As of @Model.AsOf.ToString("MMMM d, yyyy")</p>
|
||
</div>
|
||
|
||
<!-- Aging summary cards -->
|
||
<div class="row g-3 mb-4">
|
||
<div class="col-6 col-lg">
|
||
<div class="card shadow-sm text-center h-100">
|
||
<div class="card-body py-3">
|
||
<div class="h6 text-success mb-1">@Model.TotalCurrent.ToString("C0")</div>
|
||
<div class="text-muted small">Current</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-lg">
|
||
<div class="card shadow-sm text-center h-100">
|
||
<div class="card-body py-3">
|
||
<div class="h6 aging-1-30 mb-1">@Model.Total1to30.ToString("C0")</div>
|
||
<div class="text-muted small">1–30 Days</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-lg">
|
||
<div class="card shadow-sm text-center h-100">
|
||
<div class="card-body py-3">
|
||
<div class="h6 aging-31-60 mb-1">@Model.Total31to60.ToString("C0")</div>
|
||
<div class="text-muted small">31–60 Days</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-lg">
|
||
<div class="card shadow-sm text-center h-100">
|
||
<div class="card-body py-3">
|
||
<div class="h6 aging-61-90 mb-1">@Model.Total61to90.ToString("C0")</div>
|
||
<div class="text-muted small">61–90 Days</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-lg">
|
||
<div class="card shadow-sm text-center h-100">
|
||
<div class="card-body py-3">
|
||
<div class="h6 aging-over90 mb-1">@Model.TotalOver90.ToString("C0")</div>
|
||
<div class="text-muted small">Over 90 Days</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-lg">
|
||
<div class="card shadow-sm text-center h-100 border-primary border-opacity-25">
|
||
<div class="card-body py-3">
|
||
<div class="h6 text-primary fw-bold mb-1">@Model.TotalOutstanding.ToString("C0")</div>
|
||
<div class="text-muted small">Total Outstanding</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@if (!Model.Customers.Any())
|
||
{
|
||
<div class="card shadow-sm">
|
||
<div class="card-body text-center py-5 text-muted">
|
||
<i class="bi bi-check-circle text-success fs-1 d-block mb-2"></i>
|
||
<p class="mb-0 fw-semibold">All invoices are paid!</p>
|
||
<p class="small mb-0">No outstanding balances as of @Model.AsOf.ToString("MMMM d, yyyy").</p>
|
||
</div>
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<!-- Summary table -->
|
||
<div class="card shadow-sm mb-4">
|
||
<div class="card-header fw-semibold">
|
||
<i class="bi bi-table me-1"></i>Aging Summary by Customer
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-hover table-sm align-middle mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Customer</th>
|
||
<th class="text-end">Current</th>
|
||
<th class="text-end">1–30 Days</th>
|
||
<th class="text-end">31–60 Days</th>
|
||
<th class="text-end">61–90 Days</th>
|
||
<th class="text-end">Over 90</th>
|
||
<th class="text-end">Total</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var cust in Model.Customers)
|
||
{
|
||
<tr>
|
||
<td>
|
||
<a asp-controller="Customers" asp-action="Details" asp-route-id="@cust.CustomerId" class="text-decoration-none fw-medium">
|
||
@cust.CustomerName
|
||
</a>
|
||
<span class="badge bg-secondary ms-1">@cust.Invoices.Count inv.</span>
|
||
</td>
|
||
<td class="text-end aging-current">@(cust.TotalCurrent > 0 ? cust.TotalCurrent.ToString("C") : "—")</td>
|
||
<td class="text-end aging-1-30">@(cust.Total1to30 > 0 ? cust.Total1to30.ToString("C") : "—")</td>
|
||
<td class="text-end aging-31-60">@(cust.Total31to60 > 0 ? cust.Total31to60.ToString("C") : "—")</td>
|
||
<td class="text-end aging-61-90">@(cust.Total61to90 > 0 ? cust.Total61to90.ToString("C") : "—")</td>
|
||
<td class="text-end aging-over90">@(cust.TotalOver90 > 0 ? cust.TotalOver90.ToString("C") : "—")</td>
|
||
<td class="text-end fw-semibold">@cust.TotalBalance.ToString("C")</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
<tfoot class="table-light fw-bold">
|
||
<tr>
|
||
<td>Total</td>
|
||
<td class="text-end aging-current">@Model.TotalCurrent.ToString("C")</td>
|
||
<td class="text-end aging-1-30">@Model.Total1to30.ToString("C")</td>
|
||
<td class="text-end aging-31-60">@Model.Total31to60.ToString("C")</td>
|
||
<td class="text-end aging-61-90">@Model.Total61to90.ToString("C")</td>
|
||
<td class="text-end aging-over90">@Model.TotalOver90.ToString("C")</td>
|
||
<td class="text-end">@Model.TotalOutstanding.ToString("C")</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Detail by customer -->
|
||
<div class="card shadow-sm">
|
||
<div class="card-header fw-semibold">
|
||
<i class="bi bi-list-ul me-1"></i>Invoice Detail
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm align-middle mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Invoice</th>
|
||
<th>Date</th>
|
||
<th>Due Date</th>
|
||
<th class="text-end">Balance Due</th>
|
||
<th class="text-end">Age</th>
|
||
<th>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var cust in Model.Customers)
|
||
{
|
||
<tr class="customer-row">
|
||
<td colspan="6" class="py-2">@cust.CustomerName</td>
|
||
</tr>
|
||
@foreach (var inv in cust.Invoices.OrderBy(i => i.DaysOverdue))
|
||
{
|
||
string ageBadge = inv.DaysOverdue <= 0 ? "bg-success-subtle text-success"
|
||
: inv.DaysOverdue <= 30 ? "bg-warning-subtle text-warning"
|
||
: inv.DaysOverdue <= 60 ? "bg-orange-subtle text-warning"
|
||
: inv.DaysOverdue <= 90 ? "bg-danger-subtle text-danger"
|
||
: "bg-danger text-white";
|
||
string ageLabel = inv.DaysOverdue <= 0 ? "Current" : $"{inv.DaysOverdue}d overdue";
|
||
<tr>
|
||
<td class="ps-4">
|
||
<a asp-controller="Invoices" asp-action="Details" asp-route-id="@inv.InvoiceId" class="text-decoration-none fw-medium">
|
||
@inv.InvoiceNumber
|
||
</a>
|
||
</td>
|
||
<td class="text-muted small">@inv.InvoiceDate.ToString("MM/dd/yyyy")</td>
|
||
<td class="text-muted small">@(inv.DueDate?.ToString("MM/dd/yyyy") ?? "—")</td>
|
||
<td class="text-end fw-semibold @(inv.DaysOverdue > 30 ? "text-danger" : "")">@inv.BalanceDue.ToString("C")</td>
|
||
<td class="text-end"><span class="badge @ageBadge">@ageLabel</span></td>
|
||
<td></td>
|
||
</tr>
|
||
}
|
||
<tr class="table-light">
|
||
<td colspan="3" class="ps-4 fw-semibold text-end small">@cust.CustomerName subtotal</td>
|
||
<td class="text-end fw-semibold">@cust.TotalBalance.ToString("C")</td>
|
||
<td colspan="2"></td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<div class="text-muted small mt-2 no-print">
|
||
<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>
|
||
}
|