Files
PowderCoatingLogix/src/PowderCoating.Web/Views/CreditMemos/Details.cshtml
T
spouliot 64a9c1531b Fix — HTML entity rendering across 60 views
Razor's @() expression auto-encodes &, turning — into — which
rendered as literal text in the browser. Wrapped all such expressions in
@Html.Raw() so the em-dash entity is passed through unescaped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 09:27:45 -04:00

308 lines
15 KiB
Plaintext

@model PowderCoating.Core.Entities.CreditMemo
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = $"Credit Memo {Model.MemoNumber}";
var applications = ViewBag.Applications as List<CreditMemoApplication> ?? new();
var openInvoices = ViewBag.OpenInvoices as List<Invoice> ?? new();
bool canApply = ViewBag.CanApply;
var (badgeClass, badgeLabel) = Model.Status switch
{
CreditMemoStatus.Active => ("bg-success-subtle text-success border border-success-subtle", "Active"),
CreditMemoStatus.PartiallyApplied => ("bg-warning-subtle text-warning border border-warning-subtle", "Partially Applied"),
CreditMemoStatus.FullyApplied => ("bg-secondary-subtle text-secondary border border-secondary-subtle", "Fully Applied"),
CreditMemoStatus.Voided => ("bg-danger-subtle text-danger border border-danger-subtle", "Voided"),
_ => ("bg-secondary-subtle text-secondary", Model.Status.ToString())
};
}
@if (TempData["Success"] != null)
{
<div class="alert alert-success alert-permanent alert-dismissible fade show">
@TempData["Success"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-permanent alert-dismissible fade show">
@TempData["Error"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@* ── Header ────────────────────────────────────────────────────── *@
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h4 class="mb-1">
<i class="bi bi-journal-minus me-2 text-primary"></i>@Model.MemoNumber
<span class="badge @badgeClass ms-2 fs-6">@badgeLabel</span>
</h4>
<div class="text-muted">
Customer: <a asp-controller="Customers" asp-action="Details" asp-route-id="@Model.CustomerId"
class="fw-semibold text-decoration-none">
@(string.IsNullOrWhiteSpace(Model.Customer?.CompanyName) ? $"{Model.Customer?.ContactFirstName} {Model.Customer?.ContactLastName}".Trim() : Model.Customer.CompanyName)
</a>
</div>
</div>
<div class="d-flex gap-2">
@if (canApply && openInvoices.Any())
{
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#applyModal">
<i class="bi bi-check2-circle me-1"></i>Apply to Invoice
</button>
}
@if (Model.Status != CreditMemoStatus.Voided)
{
<button class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#voidModal">
<i class="bi bi-x-circle me-1"></i>Void
</button>
}
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back
</a>
</div>
</div>
<div class="row g-3">
@* ── Left: memo details ──────────────────────────────────────── *@
<div class="col-lg-5">
<div class="card h-100">
<div class="card-header fw-semibold">Credit Memo Details</div>
<div class="card-body">
@* Balance summary *@
<div class="row text-center mb-4">
<div class="col-4 border-end">
<div class="small text-muted">Total Credit</div>
<div class="fs-5 fw-bold">@Model.Amount.ToString("C")</div>
</div>
<div class="col-4 border-end">
<div class="small text-muted">Applied</div>
<div class="fs-5 fw-bold text-secondary">@Model.AmountApplied.ToString("C")</div>
</div>
<div class="col-4">
<div class="small text-muted">Remaining</div>
<div class="fs-5 fw-bold @(Model.RemainingBalance > 0 ? "text-success" : "text-secondary")">
@Model.RemainingBalance.ToString("C")
</div>
</div>
</div>
<dl class="row mb-0 small">
<dt class="col-5 text-muted">Issue Date</dt>
<dd class="col-7">@Model.IssueDate.ToLocalTime().ToString("MMMM d, yyyy")</dd>
<dt class="col-5 text-muted">Expiry Date</dt>
<dd class="col-7">
@if (Model.ExpiryDate.HasValue)
{
var expired = Model.ExpiryDate.Value < DateTime.UtcNow;
<span class="@(expired ? "text-danger fw-semibold" : "")">
@Model.ExpiryDate.Value.ToLocalTime().ToString("MMMM d, yyyy")
@if (expired) { <small>(Expired)</small> }
</span>
}
else
{
<span class="text-muted">No expiry</span>
}
</dd>
@if (Model.OriginalInvoice != null)
{
<dt class="col-5 text-muted">Original Invoice</dt>
<dd class="col-7">
<a asp-controller="Invoices" asp-action="Details" asp-route-id="@Model.OriginalInvoiceId"
class="text-decoration-none">@Model.OriginalInvoice.InvoiceNumber</a>
</dd>
}
<dt class="col-5 text-muted">Issued By</dt>
<dd class="col-7">@(Model.IssuedBy?.FullName ?? "System")</dd>
<dt class="col-5 text-muted">Reason</dt>
<dd class="col-7">@Model.Reason</dd>
@if (!string.IsNullOrWhiteSpace(Model.Notes))
{
<dt class="col-5 text-muted">Notes</dt>
<dd class="col-7">@Model.Notes</dd>
}
</dl>
</div>
</div>
</div>
@* ── Right: application history ──────────────────────────────── *@
<div class="col-lg-7">
<div class="card h-100">
<div class="card-header fw-semibold d-flex justify-content-between align-items-center">
<span>Application History</span>
<span class="badge bg-secondary-subtle text-secondary">@applications.Count applied</span>
</div>
<div class="card-body p-0">
@if (!applications.Any())
{
<div class="p-4 text-muted text-center">
<i class="bi bi-info-circle me-1"></i>
This credit memo has not been applied to any invoice yet.
@if (canApply && openInvoices.Any())
{
<span>Use <strong>Apply to Invoice</strong> above to apply it.</span>
}
else if (canApply && !openInvoices.Any())
{
<span>No open invoices with a balance due for this customer.</span>
}
</div>
}
else
{
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th>Invoice</th>
<th>Date Applied</th>
<th class="text-end">Amount</th>
<th>Applied By</th>
</tr>
</thead>
<tbody>
@foreach (var a in applications)
{
<tr>
<td>
<a asp-controller="Invoices" asp-action="Details"
asp-route-id="@a.InvoiceId" class="text-decoration-none">
@(a.Invoice?.InvoiceNumber ?? $"#{a.InvoiceId}")
</a>
</td>
<td>@a.AppliedDate.ToLocalTime().ToString("MM/dd/yyyy")</td>
<td class="text-end fw-semibold text-success">@a.AmountApplied.ToString("C")</td>
<td class="small text-muted">@Html.Raw(a.AppliedBy?.FullName ?? "&mdash;")</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
</div>
</div>
@* ── Apply Modal ─────────────────────────────────────────────── *@
@if (canApply)
{
<div class="modal fade" id="applyModal" tabindex="-1"
data-remaining-balance="@Model.RemainingBalance.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)">
<div class="modal-dialog">
<div class="modal-content">
<form asp-action="Apply" asp-route-id="@Model.Id" method="post">
@Html.AntiForgeryToken()
<div class="modal-header">
<h5 class="modal-title">Apply Credit to Invoice</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info alert-permanent py-2 small">
<strong>Available credit: @Model.RemainingBalance.ToString("C")</strong>
</div>
@if (!openInvoices.Any())
{
<p class="text-muted">No open invoices with a balance due for this customer.</p>
}
else
{
<div class="mb-3">
<label class="form-label">Select Invoice</label>
<select name="invoiceId" id="applyInvoiceId" class="form-select" required>
<option value="">&mdash; choose invoice &mdash;</option>
@foreach (var inv in openInvoices)
{
<option value="@inv.Id"
data-balance="@inv.BalanceDue.ToString("F2")">
@inv.InvoiceNumber &mdash; Due @inv.BalanceDue.ToString("C")
@if (inv.DueDate.HasValue && inv.DueDate.Value < DateTime.UtcNow)
{ <text>(Overdue)</text> }
</option>
}
</select>
</div>
<div class="mb-3">
<label class="form-label">Amount to Apply</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" name="amount" id="applyAmount"
class="form-control" step="0.01" min="0.01"
max="@Model.RemainingBalance.ToString("F2")" required />
</div>
<div id="applyMaxHint" class="form-text text-muted"></div>
</div>
}
</div>
@if (openInvoices.Any())
{
<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">Apply Credit</button>
</div>
}
else
{
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
}
</form>
</div>
</div>
</div>
}
@* ── Void Confirm Modal ──────────────────────────────────────── *@
@if (Model.Status != CreditMemoStatus.Voided)
{
<div class="modal fade" id="voidModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form asp-action="Void" asp-route-id="@Model.Id" method="post">
@Html.AntiForgeryToken()
<div class="modal-header">
<h5 class="modal-title">Void Credit Memo</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to void <strong>@Model.MemoNumber</strong>?</p>
@if (Model.RemainingBalance > 0)
{
<div class="alert alert-warning alert-permanent py-2">
The unapplied balance of <strong>@Model.RemainingBalance.ToString("C")</strong>
will be reversed from <strong>@(string.IsNullOrWhiteSpace(Model.Customer?.CompanyName) ? $"{Model.Customer?.ContactFirstName} {Model.Customer?.ContactLastName}".Trim() : Model.Customer?.CompanyName)</strong>'s credit balance.
</div>
}
@if (Model.AmountApplied > 0)
{
<p class="small text-muted mb-0">
The <strong>@Model.AmountApplied.ToString("C")</strong> already applied to invoices
will <em>not</em> be reversed.
</p>
}
</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-danger">Void Credit Memo</button>
</div>
</form>
</div>
</div>
</div>
}
@section Scripts {
<script src="~/js/credit-memo.js"></script>
}