Sweep all .cshtml files for encoding corruption; add pre-commit guard

Replace all corruption variants with HTML entities across 226 view files:
- 3-char UTF-8-as-Win1252 sequences (ae-corruption)
- Standalone smart/curly quotes that break C# Razor expressions
- Partially re-corrupted variants where the 3rd byte was normalised to ASCII

tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the
script itself never contains a literal non-ASCII character; supports -DryRun

.githooks/pre-commit: blocks commits containing the ae-corruption byte
signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the
hook is repo-committed and active for all future work on this machine.

Build clean; 225 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 21:37:10 -04:00
parent 21b39161a3
commit a0bdd2b5b4
252 changed files with 1785 additions and 1633 deletions
@@ -30,7 +30,7 @@
<!-- Loading state -->
<div id="loadingState" class="text-center py-5 d-none">
<div class="spinner-border text-warning mb-3" role="status"></div>
<p class="text-muted">Scanning bills and expense accounts for anomalies<br><small>This usually takes 510 seconds.</small></p>
<p class="text-muted">Scanning bills and expense accounts for anomalies&hellip;<br><small>This usually takes 5&ndash;10 seconds.</small></p>
</div>
<!-- Error state -->
@@ -69,7 +69,7 @@
</div>
<div id="allClearBadge" class="d-none d-flex align-items-center gap-2 px-3 py-2 rounded" style="background:#f0fdf4;">
<i class="bi bi-shield-check text-success fs-5"></i>
<span class="text-success fw-semibold">All Clear no anomalies detected</span>
<span class="text-success fw-semibold">All Clear &mdash; no anomalies detected</span>
</div>
</div>
@@ -168,7 +168,7 @@
} catch (e) {
document.getElementById('loadingState').classList.add('d-none');
document.getElementById('errorMessage').textContent = 'Network error please try again.';
document.getElementById('errorMessage').textContent = 'Network error &mdash; please try again.';
document.getElementById('errorState').classList.remove('d-none');
}
}
@@ -78,7 +78,7 @@
<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 class="text-muted small">1&ndash;30 Days</div>
</div>
</div>
</div>
@@ -86,7 +86,7 @@
<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 class="text-muted small">31&ndash;60 Days</div>
</div>
</div>
</div>
@@ -94,7 +94,7 @@
<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 class="text-muted small">61&ndash;90 Days</div>
</div>
</div>
</div>
@@ -139,9 +139,9 @@ else
<tr>
<th>Vendor</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">1&ndash;30 Days</th>
<th class="text-end">31&ndash;60 Days</th>
<th class="text-end">61&ndash;90 Days</th>
<th class="text-end">Over 90</th>
<th class="text-end">Total</th>
</tr>
@@ -156,11 +156,11 @@ else
</a>
<span class="badge bg-secondary ms-1">@vend.Bills.Count bill@(vend.Bills.Count == 1 ? "" : "s")</span>
</td>
<td class="text-end aging-current">@(vend.TotalCurrent > 0 ? vend.TotalCurrent.ToString("C") : "")</td>
<td class="text-end aging-1-30">@(vend.Total1to30 > 0 ? vend.Total1to30.ToString("C") : "")</td>
<td class="text-end aging-31-60">@(vend.Total31to60 > 0 ? vend.Total31to60.ToString("C") : "")</td>
<td class="text-end aging-61-90">@(vend.Total61to90 > 0 ? vend.Total61to90.ToString("C") : "")</td>
<td class="text-end aging-over90">@(vend.TotalOver90 > 0 ? vend.TotalOver90.ToString("C") : "")</td>
<td class="text-end aging-current">@(vend.TotalCurrent > 0 ? vend.TotalCurrent.ToString("C") : "&mdash;")</td>
<td class="text-end aging-1-30">@(vend.Total1to30 > 0 ? vend.Total1to30.ToString("C") : "&mdash;")</td>
<td class="text-end aging-31-60">@(vend.Total31to60 > 0 ? vend.Total31to60.ToString("C") : "&mdash;")</td>
<td class="text-end aging-61-90">@(vend.Total61to90 > 0 ? vend.Total61to90.ToString("C") : "&mdash;")</td>
<td class="text-end aging-over90">@(vend.TotalOver90 > 0 ? vend.TotalOver90.ToString("C") : "&mdash;")</td>
<td class="text-end fw-semibold">@vend.TotalBalance.ToString("C")</td>
</tr>
}
@@ -218,7 +218,7 @@ else
</a>
</td>
<td class="text-muted small">@bill.BillDate.ToString("MM/dd/yyyy")</td>
<td class="text-muted small">@(bill.DueDate?.ToString("MM/dd/yyyy") ?? "")</td>
<td class="text-muted small">@(bill.DueDate?.ToString("MM/dd/yyyy") ?? "&mdash;")</td>
<td class="text-end fw-semibold @(bill.DaysOverdue > 30 ? "text-danger" : "")">@bill.BalanceDue.ToString("C")</td>
<td class="text-end"><span class="badge @ageBadge">@ageLabel</span></td>
<td></td>
@@ -78,7 +78,7 @@
<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 class="text-muted small">1&ndash;30 Days</div>
</div>
</div>
</div>
@@ -86,7 +86,7 @@
<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 class="text-muted small">31&ndash;60 Days</div>
</div>
</div>
</div>
@@ -94,7 +94,7 @@
<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 class="text-muted small">61&ndash;90 Days</div>
</div>
</div>
</div>
@@ -139,9 +139,9 @@ else
<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">1&ndash;30 Days</th>
<th class="text-end">31&ndash;60 Days</th>
<th class="text-end">61&ndash;90 Days</th>
<th class="text-end">Over 90</th>
<th class="text-end">Total</th>
</tr>
@@ -156,11 +156,11 @@ else
</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 aging-current">@(cust.TotalCurrent > 0 ? cust.TotalCurrent.ToString("C") : "&mdash;")</td>
<td class="text-end aging-1-30">@(cust.Total1to30 > 0 ? cust.Total1to30.ToString("C") : "&mdash;")</td>
<td class="text-end aging-31-60">@(cust.Total31to60 > 0 ? cust.Total31to60.ToString("C") : "&mdash;")</td>
<td class="text-end aging-61-90">@(cust.Total61to90 > 0 ? cust.Total61to90.ToString("C") : "&mdash;")</td>
<td class="text-end aging-over90">@(cust.TotalOver90 > 0 ? cust.TotalOver90.ToString("C") : "&mdash;")</td>
<td class="text-end fw-semibold">@cust.TotalBalance.ToString("C")</td>
</tr>
}
@@ -218,7 +218,7 @@ else
</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-muted small">@(inv.DueDate?.ToString("MM/dd/yyyy") ?? "&mdash;")</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>
@@ -255,7 +255,7 @@ else
<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>
<p class="text-muted mt-2 small">Claude is analyzing payment behavior&hellip;</p>
</div>
<div id="aiRiskError" class="alert alert-danger alert-permanent d-none"></div>
<div id="aiRiskInsights" class="text-muted small mb-3"></div>
@@ -230,7 +230,7 @@
@Model.TotalLiabilitiesAndEquity.ToString("C")
@if (!Model.IsBalanced)
{
<i class="bi bi-exclamation-triangle ms-1" title="Sheet does not balance check account setup"></i>
<i class="bi bi-exclamation-triangle ms-1" title="Sheet does not balance &mdash; check account setup"></i>
}
</td>
</tr>
@@ -105,7 +105,7 @@ else
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-bar-chart-line me-2 text-primary"></i>@budget.Name @reportYear
<i class="bi bi-bar-chart-line me-2 text-primary"></i>@budget.Name &mdash; @reportYear
</h5>
</div>
<div class="card-body p-0">
@@ -30,7 +30,7 @@
<!-- Loading state -->
<div id="loadingState" class="text-center py-5 d-none">
<div class="spinner-border text-success mb-3" role="status"></div>
<p class="text-muted">Analysing your AR, AP, and job pipeline<br><small>This usually takes 510 seconds.</small></p>
<p class="text-muted">Analysing your AR, AP, and job pipeline&hellip;<br><small>This usually takes 5&ndash;10 seconds.</small></p>
</div>
<!-- Error state -->
@@ -196,9 +196,9 @@
// Outlook banner
const outlookMap = {
strong: { cls: 'alert-success', icon: '💪', title: 'Strong Cash Position', sub: 'Your projected cash flow looks healthy across all three periods.' },
moderate: { cls: 'alert-info', icon: '👍', title: 'Moderate Cash Position', sub: 'Cash flow looks manageable a few items to watch.' },
tight: { cls: 'alert-warning', icon: '⚠️', title: 'Tight Cash Position', sub: 'Cash flow may be constrained consider following up on open invoices.' },
concerning: { cls: 'alert-danger', icon: '🚨', title: 'Concerning Cash Position', sub: 'Projected cash flow is under pressure immediate action may be needed.' }
moderate: { cls: 'alert-info', icon: '👍', title: 'Moderate Cash Position', sub: 'Cash flow looks manageable &mdash; a few items to watch.' },
tight: { cls: 'alert-warning', icon: '⚠️', title: 'Tight Cash Position', sub: 'Cash flow may be constrained &mdash; consider following up on open invoices.' },
concerning: { cls: 'alert-danger', icon: '🚨', title: 'Concerning Cash Position', sub: 'Projected cash flow is under pressure &mdash; immediate action may be needed.' }
};
const outlook = outlookMap[data.outlook] || outlookMap.moderate;
const banner = document.getElementById('outlookBanner');
@@ -225,7 +225,7 @@
} catch (e) {
document.getElementById('loadingState').classList.add('d-none');
document.getElementById('errorMessage').textContent = 'Network error please try again.';
document.getElementById('errorMessage').textContent = 'Network error &mdash; please try again.';
document.getElementById('errorState').classList.remove('d-none');
}
}
@@ -11,7 +11,7 @@
<div>
<h4 class="fw-bold mb-0"><i class="bi bi-water me-2 text-info"></i>Cash Flow Statement</h4>
<p class="text-muted small mb-0">
@Model.From.ToString("MMMM d, yyyy") @Model.To.ToString("MMMM d, yyyy")
@Model.From.ToString("MMMM d, yyyy") &ndash; @Model.To.ToString("MMMM d, yyyy")
&nbsp;·&nbsp; Direct Method (Cash Basis)
</p>
</div>
@@ -18,7 +18,7 @@
<div class="card-body py-2">
<div class="fs-4 fw-bold text-warning">@Model.AtRiskCount</div>
<div class="small text-muted">At Risk</div>
<div class="small text-muted">(3160 days)</div>
<div class="small text-muted">(31&ndash;60 days)</div>
</div>
</div>
</div>
@@ -27,7 +27,7 @@
<div class="card-body py-2">
<div class="fs-4 fw-bold" style="color:#fd7e14">@Model.LapsingCount</div>
<div class="small text-muted">Lapsing</div>
<div class="small text-muted">(6190 days)</div>
<div class="small text-muted">(61&ndash;90 days)</div>
</div>
</div>
</div>
@@ -101,15 +101,15 @@
@item.CustomerName
</a>
</td>
<td class="small">@(item.Email ?? "")</td>
<td class="small">@(item.Phone ?? "")</td>
<td class="small">@(item.Email ?? "&mdash;")</td>
<td class="small">@(item.Phone ?? "&mdash;")</td>
<td class="text-end">@item.TotalJobs</td>
<td class="text-end">@item.LifetimeRevenue.ToString("C")</td>
<td>@(item.LastJobDate?.ToString("MMM d, yyyy") ?? "")</td>
<td>@(item.LastJobDate?.ToString("MMM d, yyyy") ?? "&mdash;")</td>
<td class="text-end">
@if (item.DaysSinceLastJob < 0)
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
else
{
@@ -45,7 +45,7 @@
<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>
<p class="text-muted mt-2 small">Analyzing your financials&hellip;</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>
@@ -108,7 +108,7 @@
<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>
<li>Claude only uses data it was given &mdash; it won't invent figures</li>
</ul>
</div>
</div>
@@ -1338,7 +1338,7 @@
<div class="col-lg-8">
<div class="card border-0 shadow-sm h-100">
<div class="card-header border-0 bg-body py-3">
<h5 class="mb-0 fw-semibold">Profit & Loss Revenue vs Expenses</h5>
<h5 class="mb-0 fw-semibold">Profit & Loss &mdash; Revenue vs Expenses</h5>
</div>
<div class="card-body">
<canvas id="plChart" height="130"></canvas>
@@ -1604,7 +1604,7 @@
<div class="fw-medium">@color.DisplayLabel</div>
<div class="text-muted small">@color.SKU</div>
</td>
<td class="text-muted small">@(color.Manufacturer ?? "")</td>
<td class="text-muted small">@(color.Manufacturer ?? "&mdash;")</td>
<td class="text-end">
<div>@color.TotalLbsUsed.ToString("N1") lbs</div>
<div class="progress mt-1" style="height:4px; min-width:80px;">
@@ -1749,7 +1749,7 @@
@c.BalanceDue.ToString("C")
</td>
<td class="text-end text-muted">@c.AvgInvoiceValue.ToString("C")</td>
<td class="text-muted small">@(c.LastInvoiceDate?.ToString("MMM d, yyyy") ?? "")</td>
<td class="text-muted small">@(c.LastInvoiceDate?.ToString("MMM d, yyyy") ?? "&mdash;")</td>
<td class="pe-3">
<div class="d-flex align-items-center gap-2">
<div class="progress flex-grow-1" style="height:6px;">
@@ -1810,7 +1810,7 @@
<div class="card-body py-3">
<div class="fs-2 fw-bold text-warning">@retAtRisk</div>
<div class="small text-muted">At Risk</div>
<div class="small text-muted">3060 days</div>
<div class="small text-muted">30&ndash;60 days</div>
</div>
</div>
</div>
@@ -1819,7 +1819,7 @@
<div class="card-body py-3">
<div class="fs-2 fw-bold text-orange" style="color:#f97316;">@retLapsing</div>
<div class="small text-muted">Lapsing</div>
<div class="small text-muted">6090 days</div>
<div class="small text-muted">60&ndash;90 days</div>
</div>
</div>
</div>
@@ -1896,10 +1896,10 @@
<span class="badge @badgeClass">@r.RetentionStatus</span>
</td>
<td class="text-center small">
@(r.LastJobDate.HasValue ? r.LastJobDate.Value.ToString("MMM d, yyyy") : "")
@(r.LastJobDate.HasValue ? r.LastJobDate.Value.ToString("MMM d, yyyy") : "&mdash;")
</td>
<td class="text-center small">
@(r.DaysSinceLastJob >= 0 ? r.DaysSinceLastJob + "d" : "")
@(r.DaysSinceLastJob >= 0 ? r.DaysSinceLastJob + "d" : "&mdash;")
</td>
<td class="text-end">@r.TotalJobs</td>
<td class="text-end pe-3 fw-semibold">@r.LifetimeRevenue.ToString("C0")</td>
@@ -1954,7 +1954,7 @@
}
else
{
<h3 class="mb-0 fw-bold text-muted"></h3>
<h3 class="mb-0 fw-bold text-muted">&mdash;</h3>
}
</div>
</div>
@@ -2103,7 +2103,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
</tr>
@@ -2128,9 +2128,9 @@
<div class="tab-pane fade" id="invoice-aging-detail-tab">
@{
var iadCurrent = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "Current").ToList();
var iad1to30 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "130 Days").ToList();
var iad31to60 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "3160 Days").ToList();
var iad61to90 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "6190 Days").ToList();
var iad1to30 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "1&ndash;30 Days").ToList();
var iad31to60 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "31&ndash;60 Days").ToList();
var iad61to90 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "61&ndash;90 Days").ToList();
var iad90plus = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "90+ Days").ToList();
}
<div class="row g-3 mb-4">
@@ -2147,7 +2147,7 @@
<div class="card border-0 shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="fs-5 fw-bold text-warning">@iad1to30.Sum(i => i.BalanceDue).ToString("C0")</div>
<div class="small text-muted">130 Days</div>
<div class="small text-muted">1&ndash;30 Days</div>
<div class="small text-muted">@iad1to30.Count invoices</div>
</div>
</div>
@@ -2156,7 +2156,7 @@
<div class="card border-0 shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="fs-5 fw-bold" style="color:#f97316;">@iad31to60.Sum(i => i.BalanceDue).ToString("C0")</div>
<div class="small text-muted">3160 Days</div>
<div class="small text-muted">31&ndash;60 Days</div>
<div class="small text-muted">@iad31to60.Count invoices</div>
</div>
</div>
@@ -2165,7 +2165,7 @@
<div class="card border-0 shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="fs-5 fw-bold text-danger">@iad61to90.Sum(i => i.BalanceDue).ToString("C0")</div>
<div class="small text-muted">6190 Days</div>
<div class="small text-muted">61&ndash;90 Days</div>
<div class="small text-muted">@iad61to90.Count invoices</div>
</div>
</div>
@@ -2217,9 +2217,9 @@
{
var bucketBadge = inv.AgingBucket switch {
"Current" => "bg-success",
"130 Days" => "bg-warning text-dark",
"3160 Days" => "badge-orange",
"6190 Days" => "bg-danger",
"1&ndash;30 Days" => "bg-warning text-dark",
"31&ndash;60 Days" => "badge-orange",
"61&ndash;90 Days" => "bg-danger",
_ => "bg-danger"
};
<tr>
@@ -2235,7 +2235,7 @@
}
</td>
<td class="small">@inv.InvoiceDate.ToString("MMM d, yyyy")</td>
<td class="small">@(inv.DueDate.HasValue ? inv.DueDate.Value.ToString("MMM d, yyyy") : "")</td>
<td class="small">@(inv.DueDate.HasValue ? inv.DueDate.Value.ToString("MMM d, yyyy") : "&mdash;")</td>
<td class="text-end">@inv.Total.ToString("C")</td>
<td class="text-end text-success">@inv.AmountPaid.ToString("C")</td>
<td class="text-end fw-semibold @(inv.BalanceDue > 0 ? "text-danger" : "")">@inv.BalanceDue.ToString("C")</td>
@@ -2250,7 +2250,7 @@
}
</td>
<td class="pe-3">
<span class="badge @bucketBadge" style="@(inv.AgingBucket == "3160 Days" ? "background:#f97316;" : "")">@inv.AgingBucket</span>
<span class="badge @bucketBadge" style="@(inv.AgingBucket == "31&ndash;60 Days" ? "background:#f97316;" : "")">@inv.AgingBucket</span>
</td>
</tr>
}
@@ -2312,7 +2312,7 @@
{
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-body border-0 pt-3 pb-2">
<h6 class="mb-0 fw-semibold"><i class="bi bi-bar-chart-fill me-2 text-primary"></i>Top 10 Powders Purchased vs Consumed (lbs)</h6>
<h6 class="mb-0 fw-semibold"><i class="bi bi-bar-chart-fill me-2 text-primary"></i>Top 10 Powders &mdash; Purchased vs Consumed (lbs)</h6>
</div>
<div class="card-body">
<canvas id="powderConsumptionChart" height="100"></canvas>
@@ -2349,7 +2349,7 @@
<div class="small text-muted">@p.ColorCode @(!string.IsNullOrEmpty(p.SKU) ? $"· {p.SKU}" : "")</div>
}
</td>
<td class="small text-muted">@(p.Manufacturer ?? "")</td>
<td class="small text-muted">@(p.Manufacturer ?? "&mdash;")</td>
<td class="text-end">@p.TotalPurchasedLbs.ToString("N1")</td>
<td class="text-end">@p.TotalConsumedLbs.ToString("N1")</td>
<td class="text-end">
@@ -2399,7 +2399,7 @@
<div class="card-body py-3">
<div class="fs-2 fw-bold text-warning">@itLow</div>
<div class="small text-muted">Low</div>
<div class="small text-muted">730 days</div>
<div class="small text-muted">7&ndash;30 days</div>
</div>
</div>
</div>
@@ -3197,7 +3197,7 @@
// ── INVOICE AGING BAR ─────────────────────────────────────────────────────
if (document.getElementById('invoiceAgingChart')) {
@{
var agingLabels = new[] { "Current", "130 Days", "3160 Days", "6190 Days", "90+ Days" };
var agingLabels = new[] { "Current", "1&ndash;30 Days", "31&ndash;60 Days", "61&ndash;90 Days", "90+ Days" };
var agingTotals = agingLabels.Select(b =>
Model.InvoiceAgingDetail.Where(i => i.AgingBucket == b).Sum(i => i.BalanceDue)).ToArray();
}
@@ -52,7 +52,7 @@
<div class="small text-muted">@item.SKU</div>
}
</td>
<td>@(item.ColorName ?? "")</td>
<td>@(item.ColorName ?? "&mdash;")</td>
<td class="text-end">@item.CurrentStockLbs.ToString("N1")</td>
<td class="text-end">@item.TotalConsumedLbs.ToString("N1")</td>
<td class="text-end">@item.TotalPurchasedLbs.ToString("N1")</td>
@@ -23,9 +23,9 @@
<div class="col-sm-6 col-md-3">
<div class="card border-warning">
<div class="card-body py-2 text-center">
<div class="small text-muted">Overdue (130 days)</div>
<div class="small text-muted">Overdue (1&ndash;30 days)</div>
<div class="fs-5 fw-bold text-warning">
@Model.Items.Where(i => i.AgingBucket == "130 Days").Sum(i => i.BalanceDue).ToString("C")
@Model.Items.Where(i => i.AgingBucket == "1&ndash;30 Days").Sum(i => i.BalanceDue).ToString("C")
</div>
</div>
</div>
@@ -65,9 +65,9 @@
{
var bucketClass = item.AgingBucket switch {
"Current" => "text-success",
"130 Days" => "text-warning",
"3160 Days" => "text-orange",
"6190 Days" => "text-danger",
"1&ndash;30 Days" => "text-warning",
"31&ndash;60 Days" => "text-orange",
"61&ndash;90 Days" => "text-danger",
"90+ Days" => "fw-bold text-danger",
_ => ""
};
@@ -85,12 +85,12 @@
}
</td>
<td>@item.InvoiceDate.ToString("MMM d, yyyy")</td>
<td>@(item.DueDate?.ToString("MMM d, yyyy") ?? "")</td>
<td>@(item.DueDate?.ToString("MMM d, yyyy") ?? "&mdash;")</td>
<td class="text-end">@item.Total.ToString("C")</td>
<td class="text-end text-success">@item.AmountPaid.ToString("C")</td>
<td class="text-end fw-semibold">@item.BalanceDue.ToString("C")</td>
<td class="text-end @bucketClass">
@(item.DaysOverdue > 0 ? item.DaysOverdue.ToString() : "")
@(item.DaysOverdue > 0 ? item.DaysOverdue.ToString() : "&mdash;")
</td>
<td><span class="badge @bucketClass bg-opacity-10 border">@item.AgingBucket</span></td>
<td><span class="badge bg-secondary-subtle text-secondary">@item.StatusDisplay</span></td>
@@ -64,7 +64,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
</tr>
@@ -96,7 +96,7 @@
<i class="bi bi-speedometer2"></i>
</div>
<h5>KPI Dashboard</h5>
<p>High-level KPIs revenue, active jobs, customers, and job counts with monthly trends and equipment status.</p>
<p>High-level KPIs &mdash; revenue, active jobs, customers, and job counts with monthly trends and equipment status.</p>
<div class="report-arrow">View dashboard <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="Analytics" class="report-card">
@@ -185,7 +185,7 @@
<i class="bi bi-clock-history"></i>
</div>
<h5>AR Aging</h5>
<p>Outstanding customer balances by age current, 30, 60, and 90+ days. Exportable to PDF.</p>
<p>Outstanding customer balances by age &mdash; current, 30, 60, and 90+ days. Exportable to PDF.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="InvoiceAgingDetail" class="report-card">
@@ -209,7 +209,7 @@
<i class="bi bi-hourglass-split"></i>
</div>
<h5>AP Aging</h5>
<p>Outstanding vendor bills by age current, 30, 60, and 90+ days past due. Exportable to PDF.</p>
<p>Outstanding vendor bills by age &mdash; current, 30, 60, and 90+ days past due. Exportable to PDF.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="TrialBalance" class="report-card">
@@ -217,7 +217,7 @@
<i class="bi bi-list-columns-reverse"></i>
</div>
<h5>Trial Balance</h5>
<p>All active accounts with debit and credit balances validates that your books are in balance.</p>
<p>All active accounts with debit and credit balances &mdash; validates that your books are in balance.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="CashFlowStatement" class="report-card">
@@ -233,7 +233,7 @@
<i class="bi bi-file-earmark-text"></i>
</div>
<h5>1099-NEC Report</h5>
<p>Payments to 1099-eligible vendors by calendar year flags those exceeding the $600 reporting threshold.</p>
<p>Payments to 1099-eligible vendors by calendar year &mdash; flags those exceeding the $600 reporting threshold.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="BudgetVsActual" class="report-card">
@@ -241,7 +241,7 @@
<i class="bi bi-bar-chart-line"></i>
</div>
<h5>Budget vs. Actual</h5>
<p>Compare monthly budgeted amounts against real P&amp;L activity revenue, expenses, and net income variance.</p>
<p>Compare monthly budgeted amounts against real P&amp;L activity &mdash; revenue, expenses, and net income variance.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
</div>
@@ -289,7 +289,7 @@
<i class="bi bi-person-check"></i>
</div>
<h5>Customer Retention</h5>
<p>Customers segmented by recency active, at risk, lapsing, churned, or never ordered.</p>
<p>Customers segmented by recency &mdash; active, at risk, lapsing, churned, or never ordered.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
</div>
@@ -376,7 +376,7 @@
<i class="bi bi-phone-vibrate"></i>
</div>
<h5>SMS Consent Audit</h5>
<p>Per-customer TCPA consent status who opted in, who opted out, and when. Export to CSV for compliance records.</p>
<p>Per-customer TCPA consent status &mdash; who opted in, who opted out, and when. Export to CSV for compliance records.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
</div>
@@ -116,7 +116,7 @@
{
<tr class="@(i.QuantityOnHand == 0 ? "table-danger" : "table-warning")">
<td>@i.Name</td>
<td>@(i.ColorName ?? "")</td>
<td>@(i.ColorName ?? "&mdash;")</td>
<td class="text-end fw-semibold text-danger">@i.QuantityOnHand.ToString("N1")</td>
<td class="text-end">@i.ReorderPoint.ToString("N1")</td>
<td class="text-muted small">@i.UnitOfMeasure</td>
@@ -60,13 +60,13 @@
}
</td>
<td>
@(item.ColorName ?? "")
@(item.ColorName ?? "&mdash;")
@if (!string.IsNullOrEmpty(item.ColorCode))
{
<span class="badge bg-secondary-subtle text-secondary ms-1">@item.ColorCode</span>
}
</td>
<td class="text-muted">@(item.Manufacturer ?? "")</td>
<td class="text-muted">@(item.Manufacturer ?? "&mdash;")</td>
<td class="text-end">@item.TotalPurchasedLbs.ToString("N1")</td>
<td class="text-end">@item.TotalConsumedLbs.ToString("N1")</td>
<td class="text-end fw-semibold @varianceClass">@item.VarianceLbs.ToString("N1")</td>
@@ -79,8 +79,8 @@
<td>
@item.DisplayLabel
</td>
<td class="text-muted small">@(item.SKU ?? "")</td>
<td class="text-muted small">@(item.Manufacturer ?? "")</td>
<td class="text-muted small">@(item.SKU ?? "&mdash;")</td>
<td class="text-muted small">@(item.Manufacturer ?? "&mdash;")</td>
<td class="text-end fw-semibold">@item.TotalLbsUsed.ToString("N1")</td>
<td class="text-end">@item.TotalCost.ToString("C")</td>
<td class="text-end">@item.JobCount</td>
@@ -29,7 +29,7 @@
<!-- 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">Income Statement @Model.From.ToString("MMM d") @Model.To.ToString("MMM d, yyyy")</p>
<p class="text-muted mb-0">Income Statement &mdash; @Model.From.ToString("MMM d") &ndash; @Model.To.ToString("MMM d, yyyy")</p>
@if (Model.AccountingMethod == PowderCoating.Core.Enums.AccountingMethod.Cash)
{
<span class="badge bg-warning text-dark">Cash Basis</span>
@@ -80,7 +80,7 @@
<div class="text-center mb-4 d-none d-print-block">
<h4 class="fw-bold">@Model.CompanyName</h4>
<h5>Profit &amp; Loss</h5>
<p class="text-muted">@Model.From.ToString("MMMM d, yyyy") @Model.To.ToString("MMMM d, yyyy")</p>
<p class="text-muted">@Model.From.ToString("MMMM d, yyyy") &ndash; @Model.To.ToString("MMMM d, yyyy")</p>
</div>
<!-- KPI Summary -->
@@ -123,7 +123,7 @@
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-semibold"><i class="bi bi-file-earmark-bar-graph me-1"></i>Income Statement</span>
<span class="text-muted small">@Model.From.ToString("MMM d") @Model.To.ToString("MMM d, yyyy")</span>
<span class="text-muted small">@Model.From.ToString("MMM d") &ndash; @Model.To.ToString("MMM d, yyyy")</span>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
@@ -148,7 +148,7 @@
<tr>
<td class="ps-4">@line.AccountNumber <span class="text-muted">@line.AccountName</span></td>
<td class="text-end">@line.Amount.ToString("C")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "" : (line.Amount / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "&mdash;" : (line.Amount / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
</tr>
}
<tr class="report-subtotal-row">
@@ -169,18 +169,18 @@
<tr>
<td class="ps-4">@line.AccountNumber <span class="text-muted">@line.AccountName</span></td>
<td class="text-end">@line.Amount.ToString("C")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "" : (line.Amount / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "&mdash;" : (line.Amount / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
</tr>
}
<tr class="report-subtotal-row">
<td class="ps-4 fw-semibold">Total COGS</td>
<td class="text-end fw-semibold text-warning">(@Model.TotalCogs.ToString("C"))</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "" : (Model.TotalCogs / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "&mdash;" : (Model.TotalCogs / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
</tr>
<tr class="report-subtotal-row">
<td class="ps-2 fw-semibold">Gross Profit</td>
<td class="text-end fw-semibold @(Model.GrossProfit >= 0 ? "text-success" : "text-danger")">@Model.GrossProfit.ToString("C")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "" : Model.GrossMarginPercent.ToString("F1") + "%")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "&mdash;" : Model.GrossMarginPercent.ToString("F1") + "%")</td>
</tr>
}
@@ -198,20 +198,20 @@
<tr>
<td class="ps-4">@line.AccountNumber <span class="text-muted">@line.AccountName</span></td>
<td class="text-end">@line.Amount.ToString("C")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "" : (line.Amount / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "&mdash;" : (line.Amount / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
</tr>
}
<tr class="report-subtotal-row">
<td class="ps-4 fw-semibold">Total Expenses</td>
<td class="text-end fw-semibold text-danger">(@Model.TotalExpenses.ToString("C"))</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "" : (Model.TotalExpenses / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "&mdash;" : (Model.TotalExpenses / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
</tr>
</tbody>
<tfoot>
<tr class="report-net-row @(Model.NetIncome < 0 ? "report-net-negative" : "")">
<td class="ps-2">Net Income</td>
<td class="text-end @(Model.NetIncome >= 0 ? "text-success" : "text-danger")">@Model.NetIncome.ToString("C")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "" : (Model.NetIncome / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
<td class="text-end text-muted small">@(Model.TotalRevenue == 0 ? "&mdash;" : (Model.NetIncome / Model.TotalRevenue * 100).ToString("F1") + "%")</td>
</tr>
</tfoot>
</table>
@@ -28,7 +28,7 @@
<!-- 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">@Model.From.ToString("MMM d") @Model.To.ToString("MMM d, yyyy") · @Model.InvoiceCount invoices · @Model.CustomerCount customers</p>
<p class="text-muted mb-0">@Model.From.ToString("MMM d") &ndash; @Model.To.ToString("MMM d, yyyy") · @Model.InvoiceCount invoices · @Model.CustomerCount customers</p>
<div class="ms-auto d-flex gap-2">
<a href="@Url.Action("SalesAndIncomePdf", new { from = Model.From.ToString("yyyy-MM-dd"), to = Model.To.ToString("yyyy-MM-dd") })"
class="btn btn-sm btn-outline-danger no-print" target="_blank">
@@ -72,7 +72,7 @@
<div class="text-center mb-4 d-none d-print-block">
<h4 class="fw-bold">@Model.CompanyName</h4>
<h5>Sales &amp; Income Report</h5>
<p class="text-muted">@Model.From.ToString("MMMM d, yyyy") @Model.To.ToString("MMMM d, yyyy")</p>
<p class="text-muted">@Model.From.ToString("MMMM d, yyyy") &ndash; @Model.To.ToString("MMMM d, yyyy")</p>
</div>
<!-- KPI Cards -->
@@ -205,7 +205,7 @@ else
<td class="text-end fw-semibold">@c.TotalInvoiced.ToString("C")</td>
<td class="text-end text-success">@c.TotalPaid.ToString("C")</td>
<td class="text-end @(c.BalanceDue > 0 ? "text-warning" : "text-muted")">@c.BalanceDue.ToString("C")</td>
<td class="text-end text-muted small no-print">@(Model.TotalInvoiced == 0 ? "" : (c.TotalInvoiced / Model.TotalInvoiced * 100).ToString("F1") + "%")</td>
<td class="text-end text-muted small no-print">@(Model.TotalInvoiced == 0 ? "&mdash;" : (c.TotalInvoiced / Model.TotalInvoiced * 100).ToString("F1") + "%")</td>
</tr>
}
</tbody>
@@ -263,9 +263,9 @@ else
</td>
<td class="text-muted small">@inv.CustomerName</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-muted small">@(inv.DueDate?.ToString("MM/dd/yyyy") ?? "&mdash;")</td>
<td class="text-end">@inv.SubTotal.ToString("C")</td>
<td class="text-end text-muted small">@(inv.TaxAmount > 0 ? inv.TaxAmount.ToString("C") : "")</td>
<td class="text-end text-muted small">@(inv.TaxAmount > 0 ? inv.TaxAmount.ToString("C") : "&mdash;")</td>
<td class="text-end fw-semibold">@inv.Total.ToString("C")</td>
<td class="text-end text-success">@inv.AmountPaid.ToString("C")</td>
<td><span class="badge @statusBadge">@inv.Status</span></td>
@@ -76,7 +76,7 @@
@item.BalanceDue.ToString("C")
</td>
<td class="text-end">@item.AvgInvoiceValue.ToString("C")</td>
<td>@(item.LastInvoiceDate?.ToString("MMM d, yyyy") ?? "")</td>
<td>@(item.LastInvoiceDate?.ToString("MMM d, yyyy") ?? "&mdash;")</td>
</tr>
}
</tbody>
@@ -29,7 +29,7 @@
<!-- 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">@Model.From.ToString("MMM d") @Model.To.ToString("MMM d, yyyy") · @(Model.TaxableInvoiceCount + Model.NonTaxableInvoiceCount) invoices</p>
<p class="text-muted mb-0">@Model.From.ToString("MMM d") &ndash; @Model.To.ToString("MMM d, yyyy") · @(Model.TaxableInvoiceCount + Model.NonTaxableInvoiceCount) invoices</p>
<div class="ms-auto d-flex gap-2">
<a href="@Url.Action("SalesTaxCsv", new { from = Model.From.ToString("yyyy-MM-dd"), to = Model.To.ToString("yyyy-MM-dd") })"
class="btn btn-sm btn-outline-success no-print">
@@ -77,7 +77,7 @@
<div class="text-center mb-4 d-none d-print-block">
<h4 class="fw-bold">@Model.CompanyName</h4>
<h5>Sales Tax Liability Report</h5>
<p class="text-muted">@Model.From.ToString("MMMM d, yyyy") @Model.To.ToString("MMMM d, yyyy") · Invoice Basis</p>
<p class="text-muted">@Model.From.ToString("MMMM d, yyyy") &ndash; @Model.To.ToString("MMMM d, yyyy") · Invoice Basis</p>
</div>
<!-- KPI Cards -->
@@ -284,11 +284,11 @@ else
<td class="small text-muted">@inv.InvoiceDate.ToString("MM/dd/yyyy")</td>
<td><span class="badge @statusBadge">@inv.Status</span></td>
<td class="text-end">@inv.SubTotal.ToString("C")</td>
<td class="text-end text-muted small">@(isTaxable ? inv.TaxPercent.ToString("F2") + "%" : "")</td>
<td class="text-end @(isTaxable ? "fw-semibold text-primary" : "text-muted")">@(isTaxable ? inv.TaxAmount.ToString("C") : "")</td>
<td class="text-end text-muted small">@(isTaxable ? inv.TaxPercent.ToString("F2") + "%" : "&mdash;")</td>
<td class="text-end @(isTaxable ? "fw-semibold text-primary" : "text-muted")">@(isTaxable ? inv.TaxAmount.ToString("C") : "&mdash;")</td>
<td class="text-end fw-semibold">@inv.Total.ToString("C")</td>
<td class="text-end text-success no-print">@inv.AmountPaid.ToString("C")</td>
<td class="small text-muted">@(string.IsNullOrEmpty(inv.TaxAccountName) ? "" : inv.TaxAccountName)</td>
<td class="small text-muted">@(string.IsNullOrEmpty(inv.TaxAccountName) ? "&mdash;" : inv.TaxAccountName)</td>
</tr>
}
</tbody>
@@ -310,7 +310,7 @@ else
<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") · Invoice basis tax liability is recognized when invoiced, not when collected. Excludes Draft and Voided invoices.
Generated @DateTime.Now.ToString("MMM d, yyyy h:mm tt") · Invoice basis &mdash; tax liability is recognized when invoiced, not when collected. Excludes Draft and Voided invoices.
</div>
@if (Model.ByMonth.Count > 1)
@@ -1,4 +1,4 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model List<Vendor1099Row>
@{
@@ -81,7 +81,7 @@ else
{
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>1099-NEC Summary — @reportYear</h5>
<h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>1099-NEC Summary &mdash; @reportYear</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
@@ -117,14 +117,14 @@ else
<span class="text-danger small"><i class="bi bi-exclamation-triangle me-1"></i>Missing</span>
}
</td>
<td class="small">@(row.Address ?? "—")</td>
<td class="small">@(row.Address ?? "&mdash;")</td>
<td class="text-end">@row.BillsPaid.ToString("C")</td>
<td class="text-end">@row.ExpensesPaid.ToString("C")</td>
<td class="text-end fw-bold @(row.NeedsForm ? "text-danger" : "")">@row.TotalPaid.ToString("C")</td>
<td class="text-center">
@if (row.NeedsForm)
{
<span class="badge bg-danger">Yes — File Required</span>
<span class="badge bg-danger">Yes &mdash; File Required</span>
}
else
{