Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Reports/ArAging.cshtml
T
spouliot 959e323f3a 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>
2026-05-10 19:22:49 -04:00

282 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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">130 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">3160 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">6190 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">130 Days</th>
<th class="text-end">3160 Days</th>
<th class="text-end">6190 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>
}