Jobs list defaults to On Floor; add Completed filter pill; fix encoding bugs

- /Jobs now redirects to ?statusGroup=active so completed jobs don't clutter the default view
- Add Completed pill (filters Completed + ReadyForPickup + Delivered)
- Pill badge counts are now global DB counts, not page-local item counts
- Ready pill badge now shows ReadyForPickup-only count
- All pill links to ?statusGroup=all to bypass the redirect
- Fix double-encoded & in Completed filter alert label
- Fix corrupted em dash (â€") in Customers/Details billing email fallback text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 20:11:33 -04:00
parent 551116d7e5
commit 24f3df1bbc
3 changed files with 55 additions and 16 deletions
@@ -110,6 +110,11 @@ public class JobsController : Controller
{ {
try try
{ {
// Default landing view: On Floor — redirect bare /Jobs to ?statusGroup=active
// so completed/cancelled jobs don't clutter the first screen.
if (string.IsNullOrEmpty(statusGroup) && string.IsNullOrEmpty(searchTerm) && string.IsNullOrEmpty(tagFilter))
return RedirectToAction("Index", new { statusGroup = "active" });
// Create and validate grid request // Create and validate grid request
var gridRequest = new GridRequest var gridRequest = new GridRequest
{ {
@@ -141,6 +146,13 @@ public class JobsController : Controller
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Delivered && j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Delivered
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Cancelled; && j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Cancelled;
} }
else if (statusGroup == "completed")
{
filter = j => j.JobStatus.StatusCode == AppConstants.StatusCodes.Job.Completed
|| j.JobStatus.StatusCode == AppConstants.StatusCodes.Job.ReadyForPickup
|| j.JobStatus.StatusCode == AppConstants.StatusCodes.Job.Delivered;
}
// "all" or unknown group: no filter applied (show every status)
} }
else if (!string.IsNullOrWhiteSpace(searchTerm)) else if (!string.IsNullOrWhiteSpace(searchTerm))
{ {
@@ -195,6 +207,27 @@ public class JobsController : Controller
gridRequest, jobDtos, gridRequest, jobDtos,
string.IsNullOrWhiteSpace(tagFilter) ? totalCount : jobDtos.Count); string.IsNullOrWhiteSpace(tagFilter) ? totalCount : jobDtos.Count);
// Pill badge counts — always global (not scoped to current filter/page)
var today = DateTime.Today;
ViewBag.AllJobCount = await _unitOfWork.Jobs.CountAsync();
ViewBag.ActiveCount = await _unitOfWork.Jobs.CountAsync(j =>
j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Completed
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.ReadyForPickup
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Delivered
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Cancelled);
ViewBag.OverdueCount = await _unitOfWork.Jobs.CountAsync(j =>
j.DueDate < today
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Completed
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.ReadyForPickup
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Delivered
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Cancelled);
ViewBag.CompletedCount = await _unitOfWork.Jobs.CountAsync(j =>
j.JobStatus.StatusCode == AppConstants.StatusCodes.Job.Completed
|| j.JobStatus.StatusCode == AppConstants.StatusCodes.Job.ReadyForPickup
|| j.JobStatus.StatusCode == AppConstants.StatusCodes.Job.Delivered);
ViewBag.ReadyCount = await _unitOfWork.Jobs.CountAsync(j =>
j.JobStatus.StatusCode == AppConstants.StatusCodes.Job.ReadyForPickup);
// Set ViewBag for sorting // Set ViewBag for sorting
ViewBag.SearchTerm = searchTerm; ViewBag.SearchTerm = searchTerm;
ViewBag.StatusGroup = statusGroup; ViewBag.StatusGroup = statusGroup;
@@ -123,7 +123,7 @@
} }
else else
{ {
<span class="text-muted">Not set — invoices go to contact email</span> <span class=text-muted>Not set &mdash; invoices go to contact email</span>
} }
</p> </p>
</div> </div>
+21 -15
View File
@@ -8,10 +8,12 @@
} }
@{ @{
var _wip = Model.Items.Count(j => j.StatusIsWIP); var _allCount = (int)(ViewBag.AllJobCount ?? 0);
var _done = Model.Items.Count(j => j.StatusCode == "COMPLETED" || j.StatusCode == "READYFORPICKUP" || j.StatusCode == "DELIVERED"); var _wip = (int)(ViewBag.ActiveCount ?? 0);
var _overdue = Model.Items.Count(j => j.DueDate.HasValue && j.DueDate.Value < DateTime.Now && j.StatusCode != "COMPLETED" && j.StatusCode != "READYFORPICKUP" && j.StatusCode != "DELIVERED" && j.StatusCode != "CANCELLED"); var _done = (int)(ViewBag.CompletedCount ?? 0);
var _value = Model.Items.Sum(j => j.FinalPrice); var _ready = (int)(ViewBag.ReadyCount ?? 0);
var _overdue = (int)(ViewBag.OverdueCount ?? 0);
var _value = Model.Items.Sum(j => j.FinalPrice);
} }
<div class="pcl-metric-strip"> <div class="pcl-metric-strip">
<div class="pcl-metric-strip-cell"> <div class="pcl-metric-strip-cell">
@@ -39,23 +41,23 @@
Showing <strong>@Model.TotalCount</strong> job(s) matching "<strong>@ViewBag.SearchTerm</strong>" Showing <strong>@Model.TotalCount</strong> job(s) matching "<strong>@ViewBag.SearchTerm</strong>"
<small class="text-muted ms-2">(searches job number, description, customer, PO, instructions, status, priority)</small> <small class="text-muted ms-2">(searches job number, description, customer, PO, instructions, status, priority)</small>
</div> </div>
<a href="@Url.Action("Index")" class="btn btn-sm btn-outline-secondary"> <a href="@Url.Action("Index", new { statusGroup = "active" })" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-x me-1"></i>Clear Filter <i class="bi bi-x me-1"></i>Clear Filter
</a> </a>
</div> </div>
} }
@if (!string.IsNullOrEmpty(ViewBag.StatusGroup as string)) @if (!string.IsNullOrEmpty(ViewBag.StatusGroup as string) && ViewBag.StatusGroup != "active" && ViewBag.StatusGroup != "all")
{ {
var groupLabel = ViewBag.StatusGroup == "active" ? "Active Jobs (excluding completed & cancelled)" var groupLabel = ViewBag.StatusGroup == "overdue" ? "Overdue Jobs (past due date)"
: ViewBag.StatusGroup == "overdue" ? "Overdue Jobs (past due date)" : ViewBag.StatusGroup == "completed" ? "Completed Jobs (completed, ready for pickup & delivered)"
: ViewBag.StatusGroup; : (string)ViewBag.StatusGroup;
<div class="alert alert-warning alert-permanent d-flex justify-content-between align-items-center"> <div class="alert alert-warning alert-permanent d-flex justify-content-between align-items-center">
<div> <div>
<i class="bi bi-funnel-fill me-2"></i> <i class="bi bi-funnel-fill me-2"></i>
Showing: <strong>@groupLabel</strong> &mdash; @Model.TotalCount result@(Model.TotalCount == 1 ? "" : "s") Showing: <strong>@groupLabel</strong> &mdash; @Model.TotalCount result@(Model.TotalCount == 1 ? "" : "s")
</div> </div>
<a href="@Url.Action("Index")" class="btn btn-sm btn-outline-secondary"> <a href="@Url.Action("Index", new { statusGroup = "active" })" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-x me-1"></i>Show All <i class="bi bi-x me-1"></i>Back to On Floor
</a> </a>
</div> </div>
} }
@@ -63,7 +65,8 @@
@{ @{
var _activeGroup = ViewBag.StatusGroup as string; var _activeGroup = ViewBag.StatusGroup as string;
var _activeSearch = ViewBag.SearchTerm as string; var _activeSearch = ViewBag.SearchTerm as string;
var _noFilter = string.IsNullOrEmpty(_activeGroup) && string.IsNullOrEmpty(_activeSearch) && string.IsNullOrEmpty(ViewBag.TagFilter as string); // "all" is the explicit show-everything group (bare URL now redirects to "active")
var _noFilter = _activeGroup == "all" && string.IsNullOrEmpty(_activeSearch) && string.IsNullOrEmpty(ViewBag.TagFilter as string);
} }
<!-- Jobs Table Card --> <!-- Jobs Table Card -->
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
@@ -135,8 +138,8 @@
</div> </div>
<!-- Row 2: quick-view pills --> <!-- Row 2: quick-view pills -->
<div class="pcl-pill-group"> <div class="pcl-pill-group">
<a href="@Url.Action("Index")" class="pcl-pill @(_noFilter ? "active" : "")"> <a href="@Url.Action("Index", new { statusGroup = "all" })" class="pcl-pill @(_noFilter ? "active" : "")">
All <span class="pcl-pill-count">@Model.TotalCount</span> All <span class="pcl-pill-count">@_allCount</span>
</a> </a>
<a href="@Url.Action("Index", new { statusGroup = "active" })" class="pcl-pill @(_activeGroup == "active" ? "active" : "")"> <a href="@Url.Action("Index", new { statusGroup = "active" })" class="pcl-pill @(_activeGroup == "active" ? "active" : "")">
On floor <span class="pcl-pill-count">@_wip</span> On floor <span class="pcl-pill-count">@_wip</span>
@@ -145,7 +148,10 @@
Overdue <span class="pcl-pill-count">@_overdue</span> Overdue <span class="pcl-pill-count">@_overdue</span>
</a> </a>
<a href="@Url.Action("Index", new { searchTerm = "ReadyForPickup" })" class="pcl-pill @(_activeSearch == "ReadyForPickup" ? "active" : "")"> <a href="@Url.Action("Index", new { searchTerm = "ReadyForPickup" })" class="pcl-pill @(_activeSearch == "ReadyForPickup" ? "active" : "")">
Ready <span class="pcl-pill-count">@_done</span> Ready <span class="pcl-pill-count">@_ready</span>
</a>
<a href="@Url.Action("Index", new { statusGroup = "completed" })" class="pcl-pill @(_activeGroup == "completed" ? "active" : "")">
Completed <span class="pcl-pill-count">@_done</span>
</a> </a>
</div> </div>
</div> </div>