64a9c1531b
Razor's @() expression auto-encodes &, turning — into &mdash; 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>
308 lines
15 KiB
Plaintext
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 ?? "—")</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="">— choose invoice —</option>
|
|
@foreach (var inv in openInvoices)
|
|
{
|
|
<option value="@inv.Id"
|
|
data-balance="@inv.BalanceDue.ToString("F2")">
|
|
@inv.InvoiceNumber — 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>
|
|
}
|