Add Credit Memos standalone management module
CreditMemosController with Index, Details, Create, Apply, and Void actions. All business logic (atomic apply transaction, RemainingBalance cap, customer.CreditBalance adjustment, auto-Paid invoice when BalanceDue hits zero) mirrors the invoice-centric IssueCreditMemo/ApplyCredit/VoidCreditMemo actions in InvoicesController but redirects back to the credit memo rather than an invoice. Views: Index (stats bar, status+search filter, table), Details (two-col layout with application history table and Bootstrap Apply/Void confirm modals), Create (customer dropdown, amount, reason, notes, optional expiry). Apply modal populates amount automatically from min(remaining credit, invoice balance due) via credit-memo.js data-attribute wiring (no inline scripts). Nav: Credit Memos added to Billing & Payments section in _Layout. Build: 0 errors. Unit tests: 200/200. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
@model PowderCoating.Web.Controllers.CreditMemoCreateVm
|
||||
@{
|
||||
ViewData["Title"] = "Issue Credit Memo";
|
||||
var linkedInvoiceNumber = ViewBag.LinkedInvoiceNumber as string;
|
||||
var customers = ViewBag.Customers as List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem> ?? new();
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0"><i class="bi bi-journal-minus me-2 text-primary"></i>Issue Credit Memo</h4>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to Credit Memos
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">Credit Memo Details</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Create" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<asp-validation-summary asp-validation-summary="All" class="alert alert-danger alert-permanent"></asp-validation-summary>
|
||||
|
||||
@if (Model.OriginalInvoiceId.HasValue && !string.IsNullOrEmpty(linkedInvoiceNumber))
|
||||
{
|
||||
<input type="hidden" asp-for="OriginalInvoiceId" />
|
||||
<div class="alert alert-info alert-permanent py-2 mb-3 small">
|
||||
<i class="bi bi-link-45deg me-1"></i>
|
||||
Linked to invoice <strong>@linkedInvoiceNumber</strong>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="CustomerId" class="form-label">Customer <span class="text-danger">*</span></label>
|
||||
<select asp-for="CustomerId" asp-items="customers" class="form-select">
|
||||
<option value="0">— select customer —</option>
|
||||
</select>
|
||||
<span asp-validation-for="CustomerId" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Amount" class="form-label">Credit Amount <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input asp-for="Amount" type="number" step="0.01" min="0.01"
|
||||
class="form-control" placeholder="0.00" />
|
||||
</div>
|
||||
<span asp-validation-for="Amount" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Reason" class="form-label">Reason <span class="text-danger">*</span></label>
|
||||
<input asp-for="Reason" class="form-control"
|
||||
placeholder="e.g. Price adjustment, billing error, goodwill credit…" />
|
||||
<span asp-validation-for="Reason" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Notes" class="form-label">Internal Notes</label>
|
||||
<textarea asp-for="Notes" class="form-control" rows="3"
|
||||
placeholder="Additional context for your records (not shown to customer)"></textarea>
|
||||
<span asp-validation-for="Notes" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label asp-for="ExpiryDate" class="form-label">
|
||||
Expiry Date
|
||||
<span class="text-muted small ms-1">(optional — leave blank for no expiry)</span>
|
||||
</label>
|
||||
<input asp-for="ExpiryDate" type="date" class="form-control" />
|
||||
<span asp-validation-for="ExpiryDate" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg me-1"></i>Issue Credit Memo
|
||||
</button>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3 border-0 bg-light">
|
||||
<div class="card-body py-2 small text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Issuing a credit memo immediately adds the amount to the customer's credit balance.
|
||||
You can apply it to one or more open invoices from the Credit Memo Details page.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,307 @@
|
||||
@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 btn-sm">
|
||||
<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">@(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-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-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-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>
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
@model List<PowderCoating.Core.Entities.CreditMemo>
|
||||
@using PowderCoating.Core.Enums
|
||||
@{
|
||||
ViewData["Title"] = "Credit Memos";
|
||||
var status = ViewBag.Status as string ?? "";
|
||||
var search = ViewBag.Search as string ?? "";
|
||||
int activeCount = ViewBag.ActiveCount;
|
||||
decimal outstanding = ViewBag.OutstandingBalance;
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0"><i class="bi bi-journal-minus me-2 text-primary"></i>Credit Memos</h4>
|
||||
<a asp-action="Create" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg me-1"></i>Issue Credit Memo
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@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>
|
||||
}
|
||||
|
||||
@* ── Stats bar ─────────────────────────────────────────────────── *@
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card border-0 bg-primary-subtle h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-primary fw-semibold small">Active Memos</div>
|
||||
<div class="fs-3 fw-bold">@activeCount</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card border-0 bg-warning-subtle h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-warning fw-semibold small">Outstanding Credit</div>
|
||||
<div class="fs-3 fw-bold">@outstanding.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card border-0 bg-success-subtle h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-success fw-semibold small">Total Memos</div>
|
||||
<div class="fs-3 fw-bold">@Model.Count</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card border-0 bg-secondary-subtle h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-secondary fw-semibold small">Total Issued</div>
|
||||
<div class="fs-3 fw-bold">@Model.Sum(m => m.Amount).ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Filters ──────────────────────────────────────────────────── *@
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form method="get" class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small mb-1">Search</label>
|
||||
<input name="search" value="@search" class="form-control form-control-sm"
|
||||
placeholder="Customer, memo #, or reason…" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small mb-1">Status</label>
|
||||
<select name="status" class="form-select form-select-sm">
|
||||
<option value="" selected="@(status == "")">All Statuses</option>
|
||||
<option value="Active" selected="@(status == "Active")">Active</option>
|
||||
<option value="PartiallyApplied" selected="@(status == "PartiallyApplied")">Partially Applied</option>
|
||||
<option value="FullyApplied" selected="@(status == "FullyApplied")">Fully Applied</option>
|
||||
<option value="Voided" selected="@(status == "Voided")">Voided</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-auto">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Filter</button>
|
||||
<a asp-action="Index" class="btn btn-sm btn-outline-secondary ms-1">Clear</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Table ────────────────────────────────────────────────────── *@
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="alert alert-info alert-permanent">No credit memos found.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Memo #</th>
|
||||
<th>Customer</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="text-end">Applied</th>
|
||||
<th class="text-end">Remaining</th>
|
||||
<th>Issue Date</th>
|
||||
<th>Expires</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var m in Model)
|
||||
{
|
||||
var rowClass = m.Status == CreditMemoStatus.Voided ? "text-muted" : "";
|
||||
var expired = m.ExpiryDate.HasValue && m.ExpiryDate.Value < DateTime.UtcNow
|
||||
&& m.Status != CreditMemoStatus.FullyApplied
|
||||
&& m.Status != CreditMemoStatus.Voided;
|
||||
<tr class="@rowClass">
|
||||
<td>
|
||||
<a asp-action="Details" asp-route-id="@m.Id" class="fw-semibold text-decoration-none">
|
||||
@m.MemoNumber
|
||||
</a>
|
||||
</td>
|
||||
<td>@(string.IsNullOrWhiteSpace(m.Customer?.CompanyName) ? $"{m.Customer?.ContactFirstName} {m.Customer?.ContactLastName}".Trim() : m.Customer.CompanyName)</td>
|
||||
<td class="text-end">@m.Amount.ToString("C")</td>
|
||||
<td class="text-end">@m.AmountApplied.ToString("C")</td>
|
||||
<td class="text-end @(m.RemainingBalance > 0 && m.Status != CreditMemoStatus.Voided ? "text-success fw-semibold" : "")">
|
||||
@m.RemainingBalance.ToString("C")
|
||||
</td>
|
||||
<td>@m.IssueDate.ToLocalTime().ToString("MM/dd/yyyy")</td>
|
||||
<td class="@(expired ? "text-danger fw-semibold" : "")">
|
||||
@(m.ExpiryDate.HasValue ? m.ExpiryDate.Value.ToLocalTime().ToString("MM/dd/yyyy") : "—")
|
||||
@if (expired) { <small>(Expired)</small> }
|
||||
</td>
|
||||
<td>
|
||||
@{
|
||||
var (badgeClass, badgeLabel) = m.Status switch
|
||||
{
|
||||
CreditMemoStatus.Active => ("bg-success-subtle text-success", "Active"),
|
||||
CreditMemoStatus.PartiallyApplied => ("bg-warning-subtle text-warning", "Partial"),
|
||||
CreditMemoStatus.FullyApplied => ("bg-secondary-subtle text-secondary", "Applied"),
|
||||
CreditMemoStatus.Voided => ("bg-danger-subtle text-danger", "Voided"),
|
||||
_ => ("bg-secondary-subtle text-secondary", m.Status.ToString())
|
||||
};
|
||||
}
|
||||
<span class="badge @badgeClass">@badgeLabel</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a asp-action="Details" asp-route-id="@m.Id"
|
||||
class="btn btn-sm btn-outline-primary">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1077,6 +1077,10 @@
|
||||
<span>Online Payments</span>
|
||||
</a>
|
||||
}
|
||||
<a asp-controller="CreditMemos" asp-action="Index" class="nav-link">
|
||||
<i class="bi bi-journal-minus"></i>
|
||||
<span>Credit Memos</span>
|
||||
</a>
|
||||
<a asp-controller="GiftCertificates" asp-action="Index" class="nav-link">
|
||||
<i class="bi bi-gift"></i>
|
||||
<span>Gift Certificates</span>
|
||||
|
||||
Reference in New Issue
Block a user