diff --git a/src/PowderCoating.Web/Controllers/InvoicesController.cs b/src/PowderCoating.Web/Controllers/InvoicesController.cs index 2c8a2a0..e808ccd 100644 --- a/src/PowderCoating.Web/Controllers/InvoicesController.cs +++ b/src/PowderCoating.Web/Controllers/InvoicesController.cs @@ -82,14 +82,15 @@ public class InvoicesController : Controller // ----------------------------------------------------------------------- /// /// Displays the paginated invoice list with multi-mode filtering. The filter cascade handles - /// nine combinations of overdue/outstanding/thisMonth flags with status and search term so the - /// database receives a single targeted predicate — no full-table load then in-memory LINQ. + /// statusGroup pills (unpaid/partial/paid/all) plus legacy flag combinations (overdue/outstanding/thisMonth) + /// so the database receives a single targeted predicate — no full-table load then in-memory LINQ. /// Balance-due sort is computed in the ORDER BY expression rather than a stored column because /// balance = Total − AmountPaid − CreditApplied − GiftCertificateRedeemed changes on every payment. /// public async Task Index( string? searchTerm, InvoiceStatus? statusFilter, + string? statusGroup, string? sortColumn, string sortDirection = "desc", bool outstandingOnly = false, @@ -100,6 +101,11 @@ public class InvoicesController : Controller { try { + // Default landing: show unpaid invoices so the list is immediately actionable. + if (string.IsNullOrEmpty(statusGroup) && !statusFilter.HasValue && + string.IsNullOrEmpty(searchTerm) && !outstandingOnly && !thisMonthOnly && !overdueOnly) + return RedirectToAction("Index", new { statusGroup = "unpaid" }); + var today = DateTime.Today; var startOfMonth = new DateTime(today.Year, today.Month, 1); var endOfMonth = startOfMonth.AddMonths(1); @@ -116,7 +122,18 @@ public class InvoicesController : Controller System.Linq.Expressions.Expression>? filter = null; - if (overdueOnly) + // Status-group pills take priority over the dropdown and legacy flags. + if (!string.IsNullOrEmpty(statusGroup)) + { + filter = statusGroup switch + { + "unpaid" => i => i.Status == InvoiceStatus.Draft || i.Status == InvoiceStatus.Sent || i.Status == InvoiceStatus.Overdue, + "partial" => i => i.Status == InvoiceStatus.PartiallyPaid, + "paid" => i => i.Status == InvoiceStatus.Paid, + _ => null // "all" — no predicate + }; + } + else if (overdueOnly) { filter = i => (i.Status == InvoiceStatus.Sent || i.Status == InvoiceStatus.PartiallyPaid || i.Status == InvoiceStatus.Overdue) && i.DueDate.HasValue && i.DueDate.Value < today; @@ -215,12 +232,20 @@ public class InvoicesController : Controller ViewBag.SearchTerm = searchTerm; ViewBag.StatusFilter = statusFilter; + ViewBag.StatusGroup = statusGroup; ViewBag.OutstandingOnly = outstandingOnly; ViewBag.ThisMonthOnly = thisMonthOnly; ViewBag.OverdueOnly = overdueOnly; ViewBag.SortColumn = gridRequest.SortColumn; ViewBag.SortDirection = gridRequest.SortDirection; + // Pill badge counts — always global (not scoped to current filter/page) + ViewBag.UnpaidCount = await _unitOfWork.Invoices.CountAsync(i => + i.Status == InvoiceStatus.Draft || i.Status == InvoiceStatus.Sent || i.Status == InvoiceStatus.Overdue); + ViewBag.PartialCount = await _unitOfWork.Invoices.CountAsync(i => i.Status == InvoiceStatus.PartiallyPaid); + ViewBag.PaidCount = await _unitOfWork.Invoices.CountAsync(i => i.Status == InvoiceStatus.Paid); + ViewBag.AllCount = await _unitOfWork.Invoices.CountAsync(); + return View(pagedResult); } catch (Exception ex) diff --git a/src/PowderCoating.Web/Views/Invoices/Index.cshtml b/src/PowderCoating.Web/Views/Invoices/Index.cshtml index dcdb897..728e796 100644 --- a/src/PowderCoating.Web/Views/Invoices/Index.cshtml +++ b/src/PowderCoating.Web/Views/Invoices/Index.cshtml @@ -11,8 +11,13 @@ ViewData["PageHelpContent"] = "Invoices are created from completed jobs and sent to the customer for payment. Lifecycle: Draft (editable) → Sent (locked, awaiting payment) → Partially Paid / Paid. Overdue = past due date with a balance still owed. Outstanding shows the total A/R balance across all unpaid invoices currently on screen. Use Void to cancel without deleting history."; var searchTerm = ViewBag.SearchTerm as string; var statusFilter = ViewBag.StatusFilter as InvoiceStatus?; + var statusGroup = ViewBag.StatusGroup as string; var outstandingOnly = (bool)(ViewBag.OutstandingOnly ?? false); var thisMonthOnly = (bool)(ViewBag.ThisMonthOnly ?? false); + var unpaidCount = (int)(ViewBag.UnpaidCount ?? 0); + var partialCount = (int)(ViewBag.PartialCount ?? 0); + var paidCount = (int)(ViewBag.PaidCount ?? 0); + var allCount = (int)(ViewBag.AllCount ?? 0); } @{ @@ -52,52 +57,77 @@
-
-
- - - - @if (outstandingOnly) { } - @if (thisMonthOnly) { } -
- - -
- + + + @if (outstandingOnly) { } + @if (thisMonthOnly) { } +
+ + +
+ + + @if (!string.IsNullOrEmpty(searchTerm) || statusFilter.HasValue || outstandingOnly || thisMonthOnly || !string.IsNullOrEmpty(statusGroup)) { - + } - - - @if (!string.IsNullOrEmpty(searchTerm) || statusFilter.HasValue || outstandingOnly || thisMonthOnly) - { - Clear - } - @if (outstandingOnly) - { +
+ + New Invoice + +
+ + + + @if (outstandingOnly) + { +
Outstanding A/R - } - @if (thisMonthOnly && statusFilter == InvoiceStatus.Paid) - { +
+ } + @if (thisMonthOnly && statusFilter == InvoiceStatus.Paid) + { +
Paid — @DateTime.Now.ToString("MMMM yyyy") - } - else if (thisMonthOnly) - { +
+ } + else if (thisMonthOnly) + { +
@DateTime.Now.ToString("MMMM yyyy") - } - - - New Invoice - +
+ }