Add inventory bin filter, print bin, mobile login fixes, and QR scan fix

- Inventory: location filter dropdown + Print Bin page (line #, name, color, SKU)
- Fix: Prismatic Powders QR scan now extracts manufacturer/SKU/color from URL path
  and uses full LookupAsync pipeline instead of relying on page fetch alone
- Fix: iOS Safari 'Login / data Zero KB' download -- add OnRejected HTML response to rate limiter
- Fix: mobile session logout -- ConfigureApplicationCookie with 30-day MaxAge persistent cookie
- Help: new 'Location Filtering & Bin Print' section in Inventory help article
- Help: HelpKnowledgeBase updated with bin filter and print bin details

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 15:19:11 -04:00
parent 8c86eba4f2
commit dfb1d34af3
8 changed files with 443 additions and 48 deletions
@@ -123,10 +123,11 @@
@{
var lowStockOnly = (bool)(ViewBag.LowStockOnly ?? false);
var activeLocation = ViewBag.Location as string;
}
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm) || !string.IsNullOrEmpty(ViewBag.Category) || lowStockOnly)
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm) || !string.IsNullOrEmpty(ViewBag.Category) || !string.IsNullOrEmpty(activeLocation) || lowStockOnly)
{
<div class="alert @(lowStockOnly ? "alert-warning" : "alert-info") alert-permanent d-flex justify-content-between align-items-center">
<div class="alert @(lowStockOnly ? "alert-warning" : "alert-info") alert-permanent d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<i class="bi bi-funnel-fill me-2"></i>
@if (lowStockOnly)
@@ -144,11 +145,24 @@
{
<span> in category "<strong>@ViewBag.Category</strong>"</span>
}
@if (!string.IsNullOrEmpty(activeLocation))
{
<span> in bin "<strong>@activeLocation</strong>"</span>
}
}
</div>
<a href="@Url.Action("Index")" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-x me-1"></i>Clear Filters
</a>
<div class="d-flex gap-2 flex-wrap">
@if (!string.IsNullOrEmpty(activeLocation))
{
<a href="@Url.Action("PrintBin", new { location = activeLocation })" target="_blank"
class="btn btn-sm btn-outline-primary" title="Print bin list">
<i class="bi bi-printer me-1"></i>Print Bin
</a>
}
<a href="@Url.Action("Index")" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-x me-1"></i>Clear Filters
</a>
</div>
</div>
}
@@ -161,14 +175,24 @@
<input type="hidden" name="sortColumn" value="@ViewBag.SortColumn" />
<input type="hidden" name="sortDirection" value="@ViewBag.SortDirection" />
<input type="hidden" name="pageSize" value="@Model.PageSize" />
<select name="category" class="form-select" style="max-width: 250px; min-width: 150px;" onchange="this.form.submit()">
<select name="category" class="form-select" style="max-width: 180px; min-width: 130px;" onchange="this.form.submit()">
<option value="">All Categories</option>
@foreach (var cat in ViewBag.Categories)
{
<option value="@cat" selected="@(cat == ViewBag.Category)">@cat</option>
}
</select>
<div class="input-group" style="max-width: 480px; min-width: 300px;">
@if (((IEnumerable<string?>)ViewBag.Locations).Any())
{
<select name="location" class="form-select" style="max-width: 180px; min-width: 130px;" onchange="this.form.submit()">
<option value="">All Locations</option>
@foreach (var loc in ViewBag.Locations)
{
<option value="@loc" selected="@(loc == activeLocation)">@loc</option>
}
</select>
}
<div class="input-group" style="max-width: 380px; min-width: 260px;">
<span class="input-group-text bg-white border-end-0">
<i class="bi bi-search text-muted"></i>
</span>
@@ -210,6 +234,7 @@
<th sortable="Name" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="ps-4">Item Name</th>
<th sortable="Category" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Category</th>
<th sortable="ColorName" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Color</th>
<th>Location</th>
<th>Vendor</th>
<th sortable="QuantityOnHand" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Quantity</th>
<th>Reorder Point</th>
@@ -250,6 +275,21 @@
<span class="text-muted">&mdash;</span>
}
</td>
<td>
@if (!string.IsNullOrEmpty(item.Location))
{
<a href="@Url.Action("Index", new { location = item.Location })"
class="badge bg-info bg-opacity-10 text-info text-decoration-none"
onclick="event.stopPropagation();"
title="Filter by this location">
<i class="bi bi-geo-alt me-1"></i>@item.Location
</a>
}
else
{
<span class="text-muted">&mdash;</span>
}
</td>
<td>
@if (!string.IsNullOrEmpty(item.PrimaryVendorName))
{
@@ -352,6 +392,15 @@
<span class="mobile-card-value">@item.ColorName</span>
</div>
}
@if (!string.IsNullOrEmpty(item.Location))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Location</span>
<span class="mobile-card-value">
<i class="bi bi-geo-alt me-1 text-info"></i>@item.Location
</span>
</div>
}
@if (!string.IsNullOrEmpty(item.PrimaryVendorName))
{
<div class="mobile-card-row">
@@ -0,0 +1,98 @@
@model IEnumerable<PowderCoating.Application.DTOs.Inventory.InventoryListDto>
@{
Layout = null;
var location = ViewBag.Location as string ?? "";
var printedAt = (DateTime)(ViewBag.PrintedAt ?? DateTime.Now);
var items = Model.ToList();
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bin @location &mdash; Inventory</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, Helvetica, sans-serif; font-size: 11pt; color: #111; background: #fff; padding: 20px; }
.header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 14px; }
.header h1 { font-size: 18pt; }
.header .meta { font-size: 9pt; color: #555; text-align: right; line-height: 1.6; }
.summary { font-size: 9.5pt; color: #444; margin-bottom: 14px; }
table { width: 100%; border-collapse: collapse; }
thead th { background: #f0f0f0; border-top: 1px solid #bbb; border-bottom: 1px solid #bbb; padding: 6px 8px; text-align: left; font-size: 9pt; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; }
tbody td { padding: 5px 8px; border-bottom: 1px solid #e0e0e0; vertical-align: middle; font-size: 10pt; }
tbody tr:last-child td { border-bottom: 2px solid #bbb; }
.qty { text-align: right; font-weight: 600; }
.qty.low { color: #c00; }
.qty.out { color: #888; }
.reorder { text-align: right; color: #555; }
.cost { text-align: right; }
.status-low { font-size: 8pt; color: #c00; font-weight: 600; }
.status-out { font-size: 8pt; color: #888; font-weight: 600; }
.footer { margin-top: 18px; font-size: 8.5pt; color: #888; border-top: 1px solid #ddd; padding-top: 8px; display: flex; justify-content: space-between; }
@@media print {
body { padding: 10px; }
.no-print { display: none !important; }
a { text-decoration: none; color: inherit; }
}
</style>
</head>
<body>
<div class="no-print" style="margin-bottom:16px;">
<button onclick="window.print()" style="padding:6px 16px;font-size:11pt;cursor:pointer;margin-right:8px;">
&#128438; Print
</button>
<a href="javascript:history.back()" style="font-size:10pt;color:#0d6efd;">&larr; Back</a>
</div>
<div class="header">
<div>
<h1>Bin: @location</h1>
<div style="font-size:10pt;color:#555;margin-top:4px;">Inventory count sheet</div>
</div>
<div class="meta">
Powder Coating Logix<br />
Printed: @printedAt.ToString("MMM d, yyyy h:mm tt")<br />
@items.Count item@(items.Count == 1 ? "" : "s")
</div>
</div>
@if (!items.Any())
{
<p style="color:#888;margin-top:20px;">No active inventory items found in this location.</p>
}
else
{
<table>
<thead>
<tr>
<th style="width:5%">#</th>
<th style="width:45%">Item Name</th>
<th style="width:25%">Color</th>
<th style="width:25%">SKU</th>
</tr>
</thead>
<tbody>
@{ var row = 0; }
@foreach (var item in items)
{
row++;
<tr>
<td style="color:#aaa;font-size:9pt;">@row</td>
<td><strong>@item.Name</strong></td>
<td>@(item.ColorName ?? "&mdash;")</td>
<td style="font-family:monospace;font-size:9.5pt;">@item.SKU</td>
</tr>
}
</tbody>
</table>
}
<div class="footer">
<span>Bin: @location</span>
<span>Powder Coating Logix &mdash; @printedAt.ToString("yyyy")</span>
</div>
</body>
</html>