Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Bills/Details.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

467 lines
25 KiB
Plaintext

@model PowderCoating.Application.DTOs.Accounting.BillDto
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = $"Bill {Model.BillNumber}";
ViewData["PageIcon"] = "bi-receipt-cutoff";
ViewData["PageHelpTitle"] = "Bill Status";
ViewData["PageHelpContent"] = "Draft: editable, not yet confirmed. Open: awaiting payment. Partially Paid: some payments recorded. Paid: fully settled. Voided: cancelled &mdash; preserves history. Edit is only available in Draft status. Use Void instead of deleting to keep a complete audit trail.";
string StatusBadge(BillStatus s) => s switch
{
BillStatus.Draft => "secondary",
BillStatus.Open => "primary",
BillStatus.PartiallyPaid => "warning",
BillStatus.Paid => "success",
BillStatus.Voided => "danger",
_ => "secondary"
};
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center gap-2">
<a asp-action="Index" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
<span class="badge bg-@StatusBadge(Model.Status) fs-6">@Model.Status</span>
<span class="text-muted small">@Model.VendorName</span>
</div>
<div class="d-flex gap-2">
@if (Model.Status == BillStatus.Draft || Model.Status == BillStatus.Open || Model.Status == BillStatus.PartiallyPaid)
{
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
<i class="bi bi-pencil me-1"></i>Edit
</a>
}
@if (Model.Status == BillStatus.Open || Model.Status == BillStatus.PartiallyPaid)
{
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#paymentModal">
<i class="bi bi-cash me-1"></i>Record Payment
</button>
}
@if (Model.Status != BillStatus.Voided && Model.Status != BillStatus.Paid)
{
<form asp-action="Void" asp-route-id="@Model.Id" method="post"
onsubmit="return confirm('Void bill @Model.BillNumber? This cannot be undone.')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-outline-danger">
<i class="bi bi-x-circle me-1"></i>Void
</button>
</form>
}
</div>
</div>
@if (TempData["Success"] != null)
{
<div class="alert alert-success alert-dismissible fade show">
<i class="bi bi-check-circle me-2"></i>@TempData["Success"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-dismissible fade show">
<i class="bi bi-exclamation-triangle me-2"></i>@TempData["Error"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<div class="row g-4">
<!-- Left column -->
<div class="col-lg-8">
<!-- Bill info -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-sm-6">
<p class="text-muted small mb-1">Vendor</p>
<p class="fw-medium mb-0">@Model.VendorName</p>
@if (!string.IsNullOrEmpty(Model.VendorEmail))
{ <p class="text-muted small mb-0">@Model.VendorEmail</p> }
@if (!string.IsNullOrEmpty(Model.VendorPhone))
{ <p class="text-muted small mb-0">@Model.VendorPhone</p> }
</div>
<div class="col-sm-6">
<div class="row g-2">
<div class="col-6">
<p class="text-muted small mb-1">Bill Date</p>
<p class="mb-0">@Model.BillDate.ToString("MMM d, yyyy")</p>
</div>
@if (Model.DueDate.HasValue)
{
<div class="col-6">
<p class="text-muted small mb-1">Due Date</p>
<p class="mb-0 @(Model.Status != BillStatus.Paid && Model.DueDate < DateTime.Today ? "text-danger fw-medium" : "")">
@Model.DueDate.Value.ToString("MMM d, yyyy")
</p>
</div>
}
@if (!string.IsNullOrEmpty(Model.VendorInvoiceNumber))
{
<div class="col-6">
<p class="text-muted small mb-1">Vendor Ref #</p>
<p class="mb-0">@Model.VendorInvoiceNumber</p>
</div>
}
@if (!string.IsNullOrEmpty(Model.Terms))
{
<div class="col-6">
<p class="text-muted small mb-1">Terms</p>
<p class="mb-0">@Model.Terms</p>
</div>
}
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Memo))
{
<div class="col-12">
<p class="text-muted small mb-1">Memo</p>
<p class="mb-0">@Model.Memo</p>
</div>
}
</div>
</div>
</div>
<!-- Line items -->
<div class="card shadow-sm mb-4">
<div class="card-header fw-semibold">Line Items</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th>Account</th>
<th>Description</th>
<th>Job</th>
<th class="text-center">Qty</th>
<th class="text-end">Unit Price</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
@foreach (var li in Model.LineItems.OrderBy(l => l.DisplayOrder))
{
<tr>
<td><span class="text-muted small">@li.AccountNumber</span> @li.AccountName</td>
<td>@li.Description</td>
<td>@li.JobNumber</td>
<td class="text-center">@li.Quantity.ToString("G")</td>
<td class="text-end">@li.UnitPrice.ToString("C")</td>
<td class="text-end fw-medium">@li.Amount.ToString("C")</td>
</tr>
}
</tbody>
<tfoot class="table-light">
<tr>
<td colspan="5" class="text-end text-muted">Subtotal</td>
<td class="text-end">@Model.SubTotal.ToString("C")</td>
</tr>
@if (Model.TaxPercent > 0)
{
<tr>
<td colspan="5" class="text-end text-muted">Tax (@Model.TaxPercent.ToString("G")%)</td>
<td class="text-end">@Model.TaxAmount.ToString("C")</td>
</tr>
}
<tr class="fw-bold">
<td colspan="5" class="text-end">Total</td>
<td class="text-end fs-5">@Model.Total.ToString("C")</td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Receipt attachment -->
@if (!string.IsNullOrEmpty(Model.ReceiptFilePath))
{
<div class="card shadow-sm mb-4">
<div class="card-header fw-semibold"><i class="bi bi-paperclip me-2"></i>Receipt / Document</div>
<div class="card-body d-flex align-items-center gap-3">
<a asp-action="DownloadReceipt" asp-route-id="@Model.Id"
class="btn btn-outline-secondary" target="_blank">
<i class="bi bi-download me-1"></i>Download Attachment
</a>
@if (Model.Status == PowderCoating.Core.Enums.BillStatus.Draft)
{
<form asp-action="RemoveReceipt" asp-route-id="@Model.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-outline-danger btn-sm"
onclick="return confirm('Remove this receipt attachment?')">
<i class="bi bi-trash me-1"></i>Remove
</button>
</form>
}
</div>
</div>
}
<!-- Payment history -->
@if (Model.Payments.Any())
{
<div class="card shadow-sm">
<div class="card-header fw-semibold d-flex align-items-center gap-2">
Payment History
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Payment History"
data-bs-content="All payments applied to this bill. Each records the date, method, bank account, and optional check number for reconciliation. Delete a payment to reverse it and restore the balance due. Check # is useful for matching against your bank statement.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th>Payment #</th>
<th>Date</th>
<th>Method</th>
<th>Check #</th>
<th>Bank Account</th>
<th>Memo</th>
<th class="text-end">Amount</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var pmt in Model.Payments.OrderByDescending(p => p.PaymentDate))
{
<tr>
<td><span class="text-muted small">@pmt.PaymentNumber</span></td>
<td>@pmt.PaymentDate.ToString("MMM d, yyyy")</td>
<td>@pmt.PaymentMethod</td>
<td>@pmt.CheckNumber</td>
<td>@pmt.BankAccountName</td>
<td><span class="text-muted small">@pmt.Memo</span></td>
<td class="text-end text-success fw-medium">@pmt.Amount.ToString("C")</td>
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-secondary me-1" title="Edit payment"
onclick="openEditBillPaymentModal(@pmt.Id, @Model.Id, '@pmt.PaymentDate.ToString("yyyy-MM-dd")', @((int)pmt.PaymentMethod), @pmt.BankAccountId, '@(pmt.CheckNumber ?? "")', '@(pmt.Memo ?? "")')">
<i class="bi bi-pencil"></i>
</button>
<form asp-action="DeletePayment" method="post" class="d-inline"
onsubmit="return confirm('Delete this payment? The bill balance will be restored.')">
@Html.AntiForgeryToken()
<input type="hidden" name="paymentId" value="@pmt.Id" />
<input type="hidden" name="billId" value="@Model.Id" />
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete payment">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
<!-- Right column: Balance summary -->
<div class="col-lg-4">
<div class="card shadow-sm border-@(Model.Status == BillStatus.Paid ? "success" : Model.BalanceDue > 0 ? "danger" : "secondary")">
<div class="card-header fw-semibold d-flex align-items-center gap-2">
Balance Summary
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Balance Summary"
data-bs-content="Balance Due = Bill Total minus all payments recorded. Multiple partial payments are supported &mdash; each reduces the balance until fully paid. Deleting a payment reverses it and restores the balance due.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Bill Total</span>
<span class="fw-medium">@Model.Total.ToString("C")</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Amount Paid</span>
<span class="text-success fw-medium">@Model.AmountPaid.ToString("C")</span>
</div>
<hr />
<div class="d-flex justify-content-between fs-5 fw-bold">
<span>Balance Due</span>
<span class="@(Model.BalanceDue > 0 ? "text-danger" : "text-success")">
@Model.BalanceDue.ToString("C")
</span>
</div>
@if (Model.Status == BillStatus.Open || Model.Status == BillStatus.PartiallyPaid)
{
<div class="d-grid mt-3">
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#paymentModal">
<i class="bi bi-cash me-1"></i>Record Payment
</button>
</div>
}
</div>
</div>
<div class="card shadow-sm mt-3">
<div class="card-header fw-semibold d-flex align-items-center gap-2">
AP Account
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="AP Account"
data-bs-content="The Accounts Payable liability account this bill is posted to. In double-entry bookkeeping, creating a bill debits the expense accounts on the line items and credits this AP account, recording the liability owed to the vendor.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<p class="mb-0 text-muted small">@Model.APAccountName</p>
</div>
</div>
</div>
</div>
<!-- Record Payment Modal -->
@if (Model.Status == BillStatus.Open || Model.Status == BillStatus.PartiallyPaid)
{
<div class="modal fade" id="paymentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Record Payment &mdash; @Model.BillNumber</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form asp-action="RecordPayment" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="BillId" value="@Model.Id" />
<div class="modal-body">
<div class="row g-3">
<div class="col-sm-6">
<label class="form-label fw-medium">Payment Date <span class="text-danger">*</span></label>
<input type="date" name="PaymentDate" value="@DateTime.Today.ToString("yyyy-MM-dd")" class="form-control" required />
</div>
<div class="col-sm-6">
<label class="form-label fw-medium">Amount <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" step="0.01" min="0.01" max="@Model.BalanceDue"
name="Amount" value="@Model.BalanceDue" class="form-control" required />
</div>
<div class="form-text">Balance due: @Model.BalanceDue.ToString("C")</div>
</div>
<div class="col-sm-6">
<label class="form-label fw-medium">Payment Method <span class="text-danger">*</span></label>
<select name="PaymentMethod" class="form-select" required>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.PaymentMethods)
{
<option value="@item.Value">@item.Text</option>
}
</select>
</div>
<div class="col-sm-6">
<div class="d-flex align-items-center gap-1 mb-1">
<label class="form-label fw-medium mb-0">Bank Account <span class="text-danger">*</span></label>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Bank Account"
data-bs-content="The bank or cash account this payment is drawn from. Used for bank reconciliation &mdash; helps match this payment to the corresponding debit on your bank statement.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<select name="BankAccountId" class="form-select" required>
<option value="">&mdash; Select Account &mdash;</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
{
<option value="@item.Value">@item.Text</option>
}
</select>
</div>
<div class="col-sm-6">
<label class="form-label fw-medium">Check #</label>
<input type="text" name="CheckNumber" class="form-control" placeholder="Optional" />
</div>
<div class="col-12">
<label class="form-label fw-medium">Memo</label>
<input type="text" name="Memo" class="form-control" placeholder="Optional notes" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-cash me-1"></i>Record Payment
</button>
</div>
</form>
</div>
</div>
</div>
}
<!-- Edit Payment Modal -->
<div class="modal fade" id="editBillPaymentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-pencil me-2"></i>Edit Payment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form asp-action="EditPayment" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="PaymentId" id="editBillPaymentId" />
<input type="hidden" name="BillId" value="@Model.Id" />
<div class="modal-body">
<div class="row g-3">
<div class="col-sm-6">
<label class="form-label fw-medium">Payment Date <span class="text-danger">*</span></label>
<input type="date" name="PaymentDate" id="editBillPaymentDate" class="form-control" required />
</div>
<div class="col-sm-6">
<label class="form-label fw-medium">Method <span class="text-danger">*</span></label>
<select name="PaymentMethod" id="editBillPaymentMethod" class="form-select" required>
<option value="0">Cash</option>
<option value="1">Check</option>
<option value="2">Credit/Debit Card</option>
<option value="3">Bank Transfer / ACH</option>
<option value="4">Digital Payment</option>
</select>
</div>
<div class="col-12">
<label class="form-label fw-medium">Bank Account <span class="text-danger">*</span></label>
<select name="BankAccountId" id="editBillBankAccountId" class="form-select" required>
<option value="">&mdash; Select Account &mdash;</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
{
<option value="@item.Value">@item.Text</option>
}
</select>
</div>
<div class="col-sm-6">
<label class="form-label fw-medium">Check #</label>
<input type="text" name="CheckNumber" id="editBillCheckNumber" class="form-control" placeholder="Optional" />
</div>
<div class="col-12">
<label class="form-label fw-medium">Memo</label>
<input type="text" name="Memo" id="editBillMemo" class="form-control" placeholder="Optional" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
@section Scripts {
<script>
function openEditBillPaymentModal(paymentId, billId, paymentDate, paymentMethod, bankAccountId, checkNumber, memo) {
document.getElementById('editBillPaymentId').value = paymentId;
document.getElementById('editBillPaymentDate').value = paymentDate;
document.getElementById('editBillPaymentMethod').value = paymentMethod;
document.getElementById('editBillBankAccountId').value = String(bankAccountId);
document.getElementById('editBillCheckNumber').value = checkNumber;
document.getElementById('editBillMemo').value = memo;
new bootstrap.Modal(document.getElementById('editBillPaymentModal')).show();
}
</script>
}