Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Accounts/Ledger.cshtml
T
spouliot a0bdd2b5b4 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>
2026-05-20 21:37:10 -04:00

285 lines
14 KiB
Plaintext

@model PowderCoating.Application.DTOs.Accounting.AccountLedgerDto
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = $"Ledger &mdash; {Model.AccountNumber} {Model.Name}";
ViewData["PageIcon"] = "bi-journal-text";
ViewData["PageHelpTitle"] = "Account Ledger";
ViewData["PageHelpContent"] = "A chronological list of every transaction posted to this account. Click any Reference to open the source record. Debit increases asset and expense accounts; credit increases liability, equity, and revenue accounts. Use the date range or quick buttons (This Month, YTD, etc.) to narrow the view.";
string typeColor = Model.AccountType switch
{
AccountType.Asset => "success",
AccountType.Liability => "danger",
AccountType.Equity => "primary",
AccountType.Revenue => "info",
AccountType.CostOfGoods => "warning",
AccountType.Expense => "secondary",
_ => "secondary"
};
string typeIcon = Model.AccountType switch
{
AccountType.Asset => "bi-safe",
AccountType.Liability => "bi-credit-card",
AccountType.Equity => "bi-bar-chart-line",
AccountType.Revenue => "bi-graph-up-arrow",
AccountType.CostOfGoods => "bi-box-seam",
AccountType.Expense => "bi-receipt-cutoff",
_ => "bi-journal"
};
string typeLabel = Model.AccountType.ToDisplayName();
// Derive from AccountSubType (more reliable than AccountType which users can misconfigure)
bool normalDebitBalance =
Model.AccountSubType == AccountSubType.Cash ||
Model.AccountSubType == AccountSubType.Checking ||
Model.AccountSubType == AccountSubType.Savings ||
Model.AccountSubType == AccountSubType.AccountsReceivable ||
Model.AccountSubType == AccountSubType.Inventory ||
Model.AccountSubType == AccountSubType.FixedAsset ||
Model.AccountSubType == AccountSubType.OtherCurrentAsset ||
Model.AccountSubType == AccountSubType.OtherAsset ||
Model.AccountSubType == AccountSubType.CostOfGoodsSold ||
(int)Model.AccountSubType >= 50; // all Expense subtypes
string balanceLabel = normalDebitBalance ? "Debit Balance" : "Credit Balance";
// Quick-range helpers for the filter bar
var today = DateTime.Today;
var thisMonthFrom = new DateTime(today.Year, today.Month, 1).ToString("yyyy-MM-dd");
var thisMonthTo = today.ToString("yyyy-MM-dd");
var lastMonthFrom = new DateTime(today.Year, today.Month, 1).AddMonths(-1).ToString("yyyy-MM-dd");
var lastMonthTo = new DateTime(today.Year, today.Month, 1).AddDays(-1).ToString("yyyy-MM-dd");
var qtdFrom = new DateTime(today.Year, ((today.Month - 1) / 3) * 3 + 1, 1).ToString("yyyy-MM-dd");
var ytdFrom = new DateTime(today.Year, 1, 1).ToString("yyyy-MM-dd");
}
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-action="Index">Chart of Accounts</a></li>
<li class="breadcrumb-item active">@Model.AccountNumber &ndash; @Model.Name</li>
</ol>
</nav>
<!-- Account header -->
<div class="d-flex align-items-center gap-3 mb-4">
<div class="rounded-3 p-3 bg-@typeColor bg-opacity-10 text-@typeColor">
<i class="bi @typeIcon fs-3"></i>
</div>
<div>
<p class="text-muted mb-0">
<span class="badge bg-@typeColor bg-opacity-75 me-1">@typeLabel</span>
<span class="text-muted small">@Model.AccountSubType.ToDisplayName() · @balanceLabel</span>
</p>
</div>
<div class="ms-auto">
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil me-1"></i>Edit Account
</a>
</div>
</div>
<!-- Date range filter -->
<div class="card shadow-sm mb-4">
<div class="card-body py-3">
<form method="get" class="row g-2 align-items-end">
<input type="hidden" name="id" value="@Model.Id" />
<div class="col-auto">
<label class="form-label form-label-sm mb-1">From</label>
<input type="date" name="from" class="form-control form-control-sm" value="@Model.From.ToString("yyyy-MM-dd")" />
</div>
<div class="col-auto">
<label class="form-label form-label-sm mb-1">To</label>
<input type="date" name="to" class="form-control form-control-sm" value="@Model.To.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>Filter
</button>
</div>
<div class="col-auto ms-2">
<div class="btn-group btn-group-sm" role="group">
<a href="@Url.Action("Ledger", new { id = Model.Id, from = thisMonthFrom, to = thisMonthTo })"
class="btn btn-outline-secondary">This Month</a>
<a href="@Url.Action("Ledger", new { id = Model.Id, from = lastMonthFrom, to = lastMonthTo })"
class="btn btn-outline-secondary">Last Month</a>
<a href="@Url.Action("Ledger", new { id = Model.Id, from = qtdFrom, to = thisMonthTo })"
class="btn btn-outline-secondary">QTD</a>
<a href="@Url.Action("Ledger", new { id = Model.Id, from = ytdFrom, to = thisMonthTo })"
class="btn btn-outline-secondary">YTD</a>
</div>
</div>
</form>
</div>
</div>
<!-- Summary cards -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card shadow-sm h-100 text-center">
<div class="card-body py-3">
<div class="h5 text-muted mb-1">@Model.OpeningBalance.ToString("C")</div>
<div class="text-muted small">Opening Balance</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm h-100 text-center">
<div class="card-body py-3">
@{
var netChange = normalDebitBalance
? Model.PeriodDebits - Model.PeriodCredits
: Model.PeriodCredits - Model.PeriodDebits;
var netColor = netChange >= 0 ? "success" : "danger";
}
<div class="h5 text-@netColor mb-1">@netChange.ToString("C")</div>
<div class="text-muted small">Period Net Change</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm h-100 text-center">
<div class="card-body py-3">
<div class="h5 text-success mb-1">@Model.PeriodDebits.ToString("C")</div>
<div class="text-muted small">Total Debits</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm h-100 text-center border-@typeColor border-opacity-50">
<div class="card-body py-3">
<div class="h5 fw-bold text-@typeColor mb-1">@Model.ClosingBalance.ToString("C")</div>
<div class="text-muted small">Closing @balanceLabel</div>
</div>
</div>
</div>
</div>
<!-- Ledger table -->
<div class="card shadow-sm">
<div class="card-header d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<span class="fw-semibold">
<i class="bi bi-journal-text me-1"></i>
Transactions
<span class="text-muted fw-normal small ms-1">
@Model.From.ToString("MMM d") &ndash; @Model.To.ToString("MMM d, yyyy")
</span>
</span>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Transaction Ledger"
data-bs-content="Each row is one side of a double-entry posting. Reference links back to the source record (invoice, bill, expense, or payment). Debit and Credit show which side of the entry hit this account. Running Balance updates after each line. The Opening Balance row shows the balance brought forward from before the selected date range.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<span class="badge bg-secondary">@Model.Entries.Count entries</span>
</div>
@if (!Model.Entries.Any())
{
<div class="card-body text-center py-5 text-muted">
<i class="bi bi-journal-x fs-1 d-block mb-2"></i>
<p class="mb-0">No transactions found for this account in the selected period.</p>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width:105px">Date</th>
<th style="width:160px">Reference</th>
<th style="width:140px">Source</th>
<th>Description</th>
<th class="text-end" style="width:110px">Debit</th>
<th class="text-end" style="width:110px">Credit</th>
<th class="text-end" style="width:120px">Balance</th>
</tr>
</thead>
<tbody>
<!-- Opening balance row -->
<tr class="table-light">
<td class="text-muted small">@Model.From.ToString("MM/dd/yyyy")</td>
<td><span class="fw-medium text-muted">&mdash;</span></td>
<td><span class="badge bg-dark-subtle text-dark">Opening Balance</span></td>
<td class="text-muted small">Balance brought forward as of @Model.From.ToString("MMM d, yyyy")</td>
<td></td>
<td></td>
<td class="text-end">
<span class="fw-semibold @(Model.OpeningBalance < 0 ? "text-danger" : "")">
@Model.OpeningBalance.ToString("C")
</span>
</td>
</tr>
@foreach (var entry in Model.Entries)
{
<tr>
<td class="text-muted small">@entry.Date.ToString("MM/dd/yyyy")</td>
<td>
@if (entry.LinkController != null && entry.LinkId.HasValue)
{
<a asp-controller="@entry.LinkController" asp-action="Details"
asp-route-id="@entry.LinkId" class="text-decoration-none fw-medium">
@entry.Reference
</a>
}
else
{
<span class="fw-medium">@entry.Reference</span>
}
</td>
<td>
@{
string sourceBadge = entry.Source switch
{
"Invoice" => "bg-info-subtle text-info",
"Invoice Payment" => "bg-success-subtle text-success",
"Customer Payment" => "bg-success-subtle text-success",
"Bill" => "bg-warning-subtle text-warning",
"Bill Payment" => "bg-danger-subtle text-danger",
"Expense" => "bg-secondary-subtle text-secondary",
"Sales Tax" => "bg-primary-subtle text-primary",
_ => "bg-secondary-subtle text-secondary"
};
}
<span class="badge @sourceBadge">@entry.Source</span>
</td>
<td class="text-muted small">@entry.Description</td>
<td class="text-end">
@if (entry.Debit > 0)
{
<span class="text-success fw-medium">@entry.Debit.ToString("C")</span>
}
</td>
<td class="text-end">
@if (entry.Credit > 0)
{
<span class="text-danger fw-medium">@entry.Credit.ToString("C")</span>
}
</td>
<td class="text-end">
<span class="fw-semibold @(entry.RunningBalance < 0 ? "text-danger" : "")">
@entry.RunningBalance.ToString("C")
</span>
</td>
</tr>
}
</tbody>
<tfoot class="table-light fw-semibold">
<tr>
<td colspan="4" class="text-muted small">Period Totals</td>
<td class="text-end text-success">@Model.PeriodDebits.ToString("C")</td>
<td class="text-end text-danger">@Model.PeriodCredits.ToString("C")</td>
<td class="text-end">@Model.ClosingBalance.ToString("C")</td>
</tr>
</tfoot>
</table>
</div>
}
</div>