Button consistency sweep + mobile responsiveness patches

- Standardize modal dismiss/cancel buttons to btn-outline-secondary across 70+ views
- Remove btn-sm from page-level Create and Back buttons (Index + Detail pages)
- Fix Edit buttons on Details pages: btn-secondary -> btn-warning
- Fix form Cancel/Back links: btn-secondary -> btn-outline-secondary
- Add 10 CSS patches to site.css for mobile/tablet responsiveness:
  top-navbar overflow prevention, page-header flex-wrap at 575px,
  table action button min-height override, notification dropdown width cap,
  tablet content padding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 19:04:10 -04:00
parent 328b195127
commit e2f9e9ae4f
71 changed files with 553 additions and 422 deletions
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
@using Microsoft.AspNetCore.Html
@using PowderCoating.Application.DTOs.Health
@using PowderCoating.Web.ViewModels.Dashboard
@@ -24,7 +24,7 @@
<p class="mb-3" style="font-family:var(--font-display);font-size:1.35rem;font-weight:500;line-height:1.4;color:var(--pcl-ink);">
@if (_attnCount > 0)
{
<span>Shop is </span><span style="color:var(--pcl-bad);">running hot</span><span> — @_attnCount item@(_attnCount == 1 ? "" : "s") need attention.</span>
<span>Shop is </span><span style="color:var(--pcl-bad);">running hot</span><span> @_attnCount item@(_attnCount == 1 ? "" : "s") need attention.</span>
}
else
{
@@ -59,7 +59,7 @@
</div>
</div>
@* PWA install banner — rendered by JS only on mobile, hidden once dismissed or already installed *@
@* PWA install banner rendered by JS only on mobile, hidden once dismissed or already installed *@
<div id="pwa-install-banner" class="row mb-4" style="display:none!important;">
<div class="col-12">
<div class="alert alert-permanent mb-0 d-flex align-items-start gap-3 py-3"
@@ -104,7 +104,7 @@
@await Html.PartialAsync("_ShopProgressWidget", shopProgressWidget)
}
@* Config health alert — only shown when there are setup gaps *@
@* Config health alert only shown when there are setup gaps *@
@if (configHealth != null && !configHealth.IsHealthy)
{
<div class="row mb-4">
@@ -406,15 +406,15 @@
}
@if (Model.AgingDays1To30 > 0)
{
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-warn);"></span>1–30d @Model.AgingDays1To30.ToString("C0")</span>
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-warn);"></span>130d @Model.AgingDays1To30.ToString("C0")</span>
}
@if (Model.AgingDays31To60 > 0)
{
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>31–60d @Model.AgingDays31To60.ToString("C0")</span>
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>3160d @Model.AgingDays31To60.ToString("C0")</span>
}
@if (Model.AgingDays61To90 > 0)
{
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>61–90d @Model.AgingDays61To90.ToString("C0")</span>
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>6190d @Model.AgingDays61To90.ToString("C0")</span>
}
@if (Model.AgingDaysOver90 > 0)
{
@@ -536,7 +536,7 @@
@if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>}
else
{<span class="text-muted">—</span>}
{<span class="text-muted"></span>}
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-danger mark-ordered-btn text-nowrap"
@@ -552,7 +552,7 @@
<tr>
<td colspan="2">Vendor Total</td>
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "")</td>
<td></td>
</tr>
</tfoot>
@@ -571,7 +571,7 @@
<div class="card border-0 shadow-sm dashboard-card" style="border-left: 4px solid #0d6efd !important;">
<div class="card-header bg-body border-0 d-flex justify-content-between align-items-center pt-4 pb-3">
<h5 class="mb-0 fw-bold">
<i class="bi bi-box-arrow-in-down me-2 text-muted"></i>Powder Ordered — Awaiting Receipt
<i class="bi bi-box-arrow-in-down me-2 text-muted"></i>Powder Ordered Awaiting Receipt
<span class="ms-2 text-muted fw-normal small" id="placed-count-label">@Model.PowderOrdersPlacedCount item@(Model.PowderOrdersPlacedCount == 1 ? "" : "s")</span>
</h5>
<small class="text-muted">Grouped by vendor &middot; Enter lbs received to update inventory</small>
@@ -630,13 +630,13 @@
@if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>}
else
{<span class="text-muted">—</span>}
{<span class="text-muted"></span>}
</td>
<td class="text-muted small">
@if (line.OrderedAt.HasValue)
{<span title="@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("g")">@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d")</span>}
else
{<span>—</span>}
{<span></span>}
</td>
<td class="text-center">
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-@line.CoatId">
@@ -669,7 +669,7 @@
<tr>
<td colspan="2">Vendor Total</td>
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "")</td>
<td colspan="2"></td>
</tr>
</tfoot>
@@ -739,7 +739,7 @@
<div class="col-md-6">
<label class="form-label fw-medium">Category</label>
<select class="form-select" id="apm-categoryId" name="inventoryCategoryId">
<option value="">— Select category —</option>
<option value=""> Select category </option>
@if (ViewBag.InventoryCategories != null)
{
foreach (var cat in (IEnumerable<dynamic>)ViewBag.InventoryCategories)
@@ -753,7 +753,7 @@
<div class="col-md-6">
<label class="form-label fw-medium">Primary Vendor</label>
<select class="form-select" id="apm-vendorId" name="primaryVendorId">
<option value="">— Select vendor —</option>
<option value=""> Select vendor </option>
@if (ViewBag.VendorList != null)
{
foreach (var v in (IEnumerable<dynamic>)ViewBag.VendorList)
@@ -814,7 +814,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" id="apm-saveBtn">
<i class="bi bi-plus-circle me-1"></i>Add to Inventory
</button>
@@ -883,7 +883,7 @@
const esc = s => s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : '';
const estCost = (c.lbsToOrder && c.costPerLb) ? (c.lbsToOrder * c.costPerLb) : null;
const orderedDate = c.orderedAt ? new Date(c.orderedAt).toLocaleDateString('en-US',{month:'short',day:'numeric'}) : '—';
const orderedDate = c.orderedAt ? new Date(c.orderedAt).toLocaleDateString('en-US',{month:'short',day:'numeric'}) : '';
const lbsFmt = c.lbsToOrder ? parseFloat(c.lbsToOrder).toFixed(2) : '0.00';
// Find or create vendor group
@@ -928,7 +928,7 @@
${c.finish ? `<span class="badge bg-light text-dark border ms-1">${esc(c.finish)}</span>` : ''}
</td>
<td class="text-end fw-medium">${lbsFmt} lbs</td>
<td class="text-end">${estCost ? '$' + estCost.toFixed(2) : '<span class="text-muted">—</span>'}</td>
<td class="text-end">${estCost ? '$' + estCost.toFixed(2) : '<span class="text-muted"></span>'}</td>
<td class="text-muted small">${orderedDate}</td>
<td class="text-center">
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-${c.coatId}">
@@ -979,7 +979,7 @@
}
qtyInput.classList.remove('is-invalid');
// Custom powder (no inventory item) → open modal to add to inventory
// Custom powder (no inventory item) → open modal to add to inventory
if (!hasInv) {
const modal = document.getElementById('addPowderModal');
// Pre-fill hidden + text fields
@@ -1024,7 +1024,7 @@
return;
}
// Inventory item exists → receive directly
// Inventory item exists → receive directly
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
@@ -1065,7 +1065,7 @@
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
saveBtn.disabled = true;
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving…';
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving';
try {
const resp = await fetch('@Url.Action("AddCustomPowderToInventory", "Dashboard")', {
@@ -1094,7 +1094,7 @@
}
});
// ── AI Lookup for Add Powder modal ───────────────────────────────────────
// ── AI Lookup for Add Powder modal ───────────────────────────────────────
(function () {
const apmBtn = document.getElementById('apm-ai-btn');
const apmStatusEl = document.getElementById('apm-ai-status');
@@ -1144,7 +1144,7 @@
const hasInput = manufacturer || colorName || colorCode || partNumber || itemName;
if (!hasInput) {
apmShowStatus('warning', '<i class="bi bi-exclamation-triangle me-1"></i>Fill in at least one field — Manufacturer, Color Name, Color Code, or Item Name — then try again.');
apmShowStatus('warning', '<i class="bi bi-exclamation-triangle me-1"></i>Fill in at least one field Manufacturer, Color Name, Color Code, or Item Name then try again.');
return;
}
@@ -1153,7 +1153,7 @@
document.getElementById('apm-bad-match-btn')?.remove();
apmBtn.disabled = true;
apmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Looking up...';
apmShowStatus('info', '<i class="bi bi-hourglass-split me-1"></i>Searching for product specifications…');
apmShowStatus('info', '<i class="bi bi-hourglass-split me-1"></i>Searching for product specifications');
try {
const formData = new FormData();
@@ -1220,7 +1220,7 @@
: '';
apmShowStatus('success', `<i class="bi bi-check-circle me-1"></i>Auto-filled: ${filled.join(', ')}.${reasoning}`);
} else {
apmShowStatus('warning', '<i class="bi bi-info-circle me-1"></i>No new fields to fill — they may already be populated, or the product wasn\'t found.');
apmShowStatus('warning', '<i class="bi bi-info-circle me-1"></i>No new fields to fill they may already be populated, or the product wasn\'t found.');
}
} catch (err) {
@@ -1274,7 +1274,7 @@
(function () {
var DISMISSED_KEY = 'pcl_pwa_banner_dismissed';
// Already installed as standalone — never show
// Already installed as standalone never show
var isStandalone = window.navigator.standalone === true ||
window.matchMedia('(display-mode: standalone)').matches;
if (isStandalone) return;
@@ -1298,7 +1298,7 @@
var isSafari = /webkit/i.test(ua) && !/crios|chrome|fxios|opios/i.test(ua);
if (isSafari) {
titleEl.textContent = 'Add to Home Screen';
msgEl.innerHTML = 'For the best experience — and so the camera only asks once — open the ' +
msgEl.innerHTML = 'For the best experience and so the camera only asks once open the ' +
'<strong>Share menu</strong> <span style="font-size:1.1em">&#9650;</span> at the bottom of Safari ' +
'and tap <strong>Add to Home Screen</strong>.';
} else {