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 @@