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
@@ -6,7 +6,7 @@
<div class="container-fluid py-3" style="max-width:700px"> <div class="container-fluid py-3" style="max-width:700px">
<div class="d-flex align-items-center gap-3 mb-3"> <div class="d-flex align-items-center gap-3 mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"><i class="bi bi-arrow-left me-1"></i>Back</a> <a asp-action="Index" class="btn btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i>Back</a>
<h4 class="mb-0"><i class="bi bi-megaphone me-2 text-primary"></i>New Announcement</h4> <h4 class="mb-0"><i class="bi bi-megaphone me-2 text-primary"></i>New Announcement</h4>
</div> </div>
@@ -6,7 +6,7 @@
<div class="container-fluid py-3" style="max-width:700px"> <div class="container-fluid py-3" style="max-width:700px">
<div class="d-flex align-items-center gap-3 mb-3"> <div class="d-flex align-items-center gap-3 mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"><i class="bi bi-arrow-left me-1"></i>Back</a> <a asp-action="Index" class="btn btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i>Back</a>
<h4 class="mb-0"><i class="bi bi-megaphone me-2 text-primary"></i>Edit Announcement</h4> <h4 class="mb-0"><i class="bi bi-megaphone me-2 text-primary"></i>Edit Announcement</h4>
</div> </div>
@@ -20,7 +20,7 @@
<div class="container-fluid py-3"> <div class="container-fluid py-3">
<div class="d-flex align-items-center justify-content-between mb-3"> <div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="mb-0"><i class="bi bi-megaphone me-2 text-primary"></i>Announcements</h4> <h4 class="mb-0"><i class="bi bi-megaphone me-2 text-primary"></i>Announcements</h4>
<a asp-action="Create" class="btn btn-primary btn-sm"> <a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>New Announcement <i class="bi bi-plus-lg me-1"></i>New Announcement
</a> </a>
</div> </div>
@@ -69,7 +69,7 @@
</p> </p>
</div> </div>
<div class="modal-footer"> <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="quickCreateSubmit"> <button type="submit" class="btn btn-primary" id="quickCreateSubmit">
<i class="bi bi-check-circle"></i> Create Appointment <i class="bi bi-check-circle"></i> Create Appointment
</button> </button>
@@ -17,7 +17,7 @@
<div class="container-fluid py-3" style="max-width:900px"> <div class="container-fluid py-3" style="max-width:900px">
<div class="d-flex align-items-center gap-3 mb-3"> <div class="d-flex align-items-center gap-3 mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back <i class="bi bi-arrow-left me-1"></i>Back
</a> </a>
<h4 class="mb-0"><i class="bi bi-shield-check me-2 text-primary"></i>Audit Entry #@Model.Id</h4> <h4 class="mb-0"><i class="bi bi-shield-check me-2 text-primary"></i>Audit Entry #@Model.Id</h4>
@@ -6,7 +6,7 @@
<div class="d-flex align-items-center mb-3 gap-2"> <div class="d-flex align-items-center mb-3 gap-2">
<h4 class="mb-0 fw-semibold">Bank Reconciliation</h4> <h4 class="mb-0 fw-semibold">Bank Reconciliation</h4>
<a asp-action="Create" class="btn btn-sm btn-primary ms-auto"> <a asp-action="Create" class="btn btn-primary ms-auto">
<i class="bi bi-plus-lg me-1"></i>Start New Reconciliation <i class="bi bi-plus-lg me-1"></i>Start New Reconciliation
</a> </a>
</div> </div>
+23 -23
View File
@@ -1,10 +1,10 @@
@model PowderCoating.Application.DTOs.Accounting.CreateBillDto @model PowderCoating.Application.DTOs.Accounting.CreateBillDto
@{ @{
ViewData["Title"] = "New Bill"; ViewData["Title"] = "New Bill";
ViewData["PageIcon"] = "bi-receipt-cutoff"; ViewData["PageIcon"] = "bi-receipt-cutoff";
ViewData["PageHelpTitle"] = "New Bill"; ViewData["PageHelpTitle"] = "New Bill";
ViewData["PageHelpContent"] = "Record a vendor invoice to track what you owe. Bills start as Draft (editable) and become Open once confirmed. Partial payments are supported — each payment reduces the balance. Link line items to expense accounts and optionally to specific jobs for cost tracking."; ViewData["PageHelpContent"] = "Record a vendor invoice to track what you owe. Bills start as Draft (editable) and become Open once confirmed. Partial payments are supported each payment reduces the balance. Link line items to expense accounts and optionally to specific jobs for cost tracking.";
string? fromPoNumber = ViewBag.FromPoNumber as string; string? fromPoNumber = ViewBag.FromPoNumber as string;
int? fromPoId = ViewBag.FromPoId as int?; int? fromPoId = ViewBag.FromPoId as int?;
} }
@@ -13,7 +13,7 @@
<div> <div>
@if (!string.IsNullOrEmpty(fromPoNumber)) @if (!string.IsNullOrEmpty(fromPoNumber))
{ {
<p class="text-muted mb-0 small"><i class="bi bi-box-arrow-in-down text-success me-1"></i> Pre-filled from <strong>@fromPoNumber</strong> — review and save</p> <p class="text-muted mb-0 small"><i class="bi bi-box-arrow-in-down text-success me-1"></i> Pre-filled from <strong>@fromPoNumber</strong> review and save</p>
} }
</div> </div>
@if (fromPoId.HasValue) @if (fromPoId.HasValue)
@@ -44,7 +44,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Bill Details" data-bs-title="Bill Details"
data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due — drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation. Payment Terms auto-fill from the vendor record."> data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation. Payment Terms auto-fill from the vendor record.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -66,8 +66,8 @@
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-danger">*</span></label> <label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-danger">*</span></label>
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select" id="vendorSelect" <select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select" id="vendorSelect"
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor"> data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
<option value="">— Select Vendor —</option> <option value=""> Select Vendor </option>
<option value="__new__">+ Add New Vendor…</option> <option value="__new__">+ Add New Vendor</option>
</select> </select>
<span asp-validation-for="VendorId" class="text-danger small"></span> <span asp-validation-for="VendorId" class="text-danger small"></span>
</div> </div>
@@ -100,7 +100,7 @@
<label for="receiptFile" class="form-label fw-medium">Attach Receipt / Document</label> <label for="receiptFile" class="form-label fw-medium">Attach Receipt / Document</label>
<input type="file" name="receiptFile" id="receiptFile" class="form-control" <input type="file" name="receiptFile" id="receiptFile" class="form-control"
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" /> accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div> <div class="form-text">JPG, PNG, GIF, WebP, or PDF up to 10 MB.</div>
</div> </div>
</div> </div>
</div> </div>
@@ -114,7 +114,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Line Items" data-bs-title="Line Items"
data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories."> data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -150,7 +150,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Bill Summary" data-bs-title="Bill Summary"
data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed — each payment recorded reduces the balance due until the bill is fully paid."> data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed each payment recorded reduces the balance due until the bill is fully paid.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -198,7 +198,7 @@
<div class="mb-2"> <div class="mb-2">
<label class="form-label small fw-medium">Bank / Cash Account <span class="text-danger">*</span></label> <label class="form-label small fw-medium">Bank / Cash Account <span class="text-danger">*</span></label>
<select name="bankAccountId" class="form-select form-select-sm" id="payNowBankAccount"> <select name="bankAccountId" class="form-select form-select-sm" id="payNowBankAccount">
<option value="">— Select Account —</option> <option value=""> Select Account </option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts) @foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
{ {
<option value="@item.Value">@item.Text</option> <option value="@item.Value">@item.Text</option>
@@ -232,7 +232,7 @@
<tr class="line-item-row"> <tr class="line-item-row">
<td> <td>
<select class="form-select form-select-sm account-select" name="LineItems[INDEX].AccountId" required> <select class="form-select form-select-sm account-select" name="LineItems[INDEX].AccountId" required>
<option value="">— Account —</option> <option value=""> Account </option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts) @foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
{ {
<option value="@item.Value">@item.Text</option> <option value="@item.Value">@item.Text</option>
@@ -242,7 +242,7 @@
<td><input type="text" class="form-control form-control-sm" name="LineItems[INDEX].Description" placeholder="Description" /></td> <td><input type="text" class="form-control form-control-sm" name="LineItems[INDEX].Description" placeholder="Description" /></td>
<td> <td>
<select class="form-select form-select-sm" name="LineItems[INDEX].JobId"> <select class="form-select form-select-sm" name="LineItems[INDEX].JobId">
<option value="">—</option> <option value=""></option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.Jobs) @foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.Jobs)
{ {
<option value="@item.Value">@item.Text</option> <option value="@item.Value">@item.Text</option>
@@ -273,12 +273,12 @@
<div class="mb-3"> <div class="mb-3">
<label for="scanReceiptFile" class="form-label fw-medium">Receipt / Invoice Document</label> <label for="scanReceiptFile" class="form-label fw-medium">Receipt / Invoice Document</label>
<input type="file" id="scanReceiptFile" class="form-control" accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" /> <input type="file" id="scanReceiptFile" class="form-control" accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div> <div class="form-text">JPG, PNG, GIF, WebP, or PDF up to 10 MB.</div>
</div> </div>
<div id="scanReceiptStatus" class="text-muted small mt-2"></div> <div id="scanReceiptStatus" class="text-muted small mt-2"></div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="scanReceiptUploadBtn"> <button type="button" class="btn btn-primary" id="scanReceiptUploadBtn">
<i class="bi bi-camera me-1"></i>Scan &amp; Fill <i class="bi bi-camera me-1"></i>Scan &amp; Fill
</button> </button>
@@ -393,8 +393,8 @@
} }
if (lineCount === 0) addLineItem(); if (lineCount === 0) addLineItem();
// ── AI Auto-suggest Account on description blur ─────────────────────── // ── AI Auto-suggest Account on description blur ───────────────────────
// Keyword shortcuts — handle common cases with zero API cost // Keyword shortcuts handle common cases with zero API cost
const _keywordMap = [ const _keywordMap = [
{ words: ['electric','power','utility','gas','water','internet','phone','telecom'], hint: 'utilities' }, { words: ['electric','power','utility','gas','water','internet','phone','telecom'], hint: 'utilities' },
{ words: ['powder','paint','coat','material','supply','supplies','chemical','resin'], hint: 'materials' }, { words: ['powder','paint','coat','material','supply','supplies','chemical','resin'], hint: 'materials' },
@@ -407,7 +407,7 @@
{ words: ['advertising','marketing','promo'], hint: 'advertising' }, { words: ['advertising','marketing','promo'], hint: 'advertising' },
]; ];
// Session cache: description (lowercased) → { accountId, accountName } // Session cache: description (lowercased) { accountId, accountName }
const _suggestCache = new Map(); const _suggestCache = new Map();
function _keywordGuess(description) { function _keywordGuess(description) {
@@ -480,7 +480,7 @@
hint2.className = 'ai-account-hint text-muted small mt-1'; hint2.className = 'ai-account-hint text-muted small mt-1';
accountSel.parentNode.appendChild(hint2); accountSel.parentNode.appendChild(hint2);
} }
hint2.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:.75rem;height:.75rem"></span> Thinking…'; hint2.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:.75rem;height:.75rem"></span> Thinking';
try { try {
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value; const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
@@ -501,14 +501,14 @@
} }
} }
// Event delegation — works for dynamically added rows // Event delegation works for dynamically added rows
document.getElementById('lineItemsBody').addEventListener('blur', function (e) { document.getElementById('lineItemsBody').addEventListener('blur', function (e) {
if (e.target.matches('[name$=".Description"]')) { if (e.target.matches('[name$=".Description"]')) {
_suggestAccountForRow(e.target.closest('tr')); _suggestAccountForRow(e.target.closest('tr'));
} }
}, true); // capture phase so blur bubbles }, true); // capture phase so blur bubbles
// ── Scan Receipt ───────────────────────────────────────────────────── // ── Scan Receipt ─────────────────────────────────────────────────────
document.getElementById('scanReceiptUploadBtn').addEventListener('click', async function () { document.getElementById('scanReceiptUploadBtn').addEventListener('click', async function () {
const fileInput = document.getElementById('scanReceiptFile'); const fileInput = document.getElementById('scanReceiptFile');
if (!fileInput.files.length) { alert('Please select a file.'); return; } if (!fileInput.files.length) { alert('Please select a file.'); return; }
@@ -535,7 +535,7 @@
return; return;
} }
// Auto-fill bill header — try to match vendor name to dropdown // Auto-fill bill header try to match vendor name to dropdown
if (data.vendorName) { if (data.vendorName) {
const vendorSel = document.getElementById('vendorSelect'); const vendorSel = document.getElementById('vendorSelect');
if (vendorSel && !vendorSel.value) { if (vendorSel && !vendorSel.value) {
@@ -553,7 +553,7 @@
vendorSel.value = bestOption.value; vendorSel.value = bestOption.value;
vendorSel.dispatchEvent(new Event('change')); vendorSel.dispatchEvent(new Event('change'));
} else { } else {
// No match — put the name in Memo so user knows what the AI saw // No match put the name in Memo so user knows what the AI saw
const memo = document.querySelector('[name="Memo"]'); const memo = document.querySelector('[name="Memo"]');
if (memo && !memo.value) memo.value = data.vendorName; if (memo && !memo.value) memo.value = data.vendorName;
} }
@@ -598,7 +598,7 @@
const modal = bootstrap.Modal.getInstance(document.getElementById('scanReceiptModal')); const modal = bootstrap.Modal.getInstance(document.getElementById('scanReceiptModal'));
if (modal) modal.hide(); if (modal) modal.hide();
statusEl.textContent = 'Scan complete — review and adjust as needed.'; statusEl.textContent = 'Scan complete review and adjust as needed.';
} catch (e) { } catch (e) {
statusEl.textContent = 'Error connecting to AI service.'; statusEl.textContent = 'Error connecting to AI service.';
} finally { } finally {
@@ -8,7 +8,7 @@
} }
<div class="mb-4"> <div class="mb-4">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Budgets <i class="bi bi-arrow-left me-1"></i>Back to Budgets
</a> </a>
</div> </div>
@@ -48,7 +48,7 @@
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center"> <div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>Monthly Amounts by Account</h5> <h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>Monthly Amounts by Account</h5>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" id="fillEvenlyBtn"> <button type="button" class="btn btn-sm btn-outline-secondary" id="fillEvenlyBtn">
<i class="bi bi-distribute-horizontal me-1"></i>Spread Annual Evenly <i class="bi bi-distribute-horizontal me-1"></i>Spread Annual Evenly
</button> </button>
</div> </div>
@@ -1,14 +1,14 @@
@using PowderCoating.Web.Controllers @using PowderCoating.Web.Controllers
@model BudgetCreateVm @model BudgetCreateVm
@{ @{
ViewData["Title"] = $"Edit Budget — {Model.Name}"; ViewData["Title"] = $"Edit Budget {Model.Name}";
ViewData["PageIcon"] = "bi-pencil"; ViewData["PageIcon"] = "bi-pencil";
var months = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; var months = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
} }
<div class="mb-4 d-flex justify-content-between align-items-center"> <div class="mb-4 d-flex justify-content-between align-items-center">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Budgets <i class="bi bi-arrow-left me-1"></i>Back to Budgets
</a> </a>
<a asp-controller="Reports" asp-action="BudgetVsActual" asp-route-budgetId="@Model.Id" class="btn btn-outline-primary btn-sm"> <a asp-controller="Reports" asp-action="BudgetVsActual" asp-route-budgetId="@Model.Id" class="btn btn-outline-primary btn-sm">
@@ -24,7 +24,7 @@
<div class="card border-0 shadow-sm mb-4"> <div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3"> <div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold"> <h5 class="mb-0 fw-semibold">
<i class="bi bi-pie-chart me-2 text-primary"></i>@Model.Name — @Model.FiscalYear <i class="bi bi-pie-chart me-2 text-primary"></i>@Model.Name @Model.FiscalYear
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -51,7 +51,7 @@
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center"> <div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>Monthly Amounts by Account</h5> <h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>Monthly Amounts by Account</h5>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" id="fillEvenlyBtn"> <button type="button" class="btn btn-sm btn-outline-secondary" id="fillEvenlyBtn">
<i class="bi bi-distribute-horizontal me-1"></i>Spread Annual Evenly <i class="bi bi-distribute-horizontal me-1"></i>Spread Annual Evenly
</button> </button>
</div> </div>
@@ -91,7 +91,7 @@
</div> </div>
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Back to Catalog <i class="bi bi-arrow-left me-1"></i> Back to Catalog
</a> </a>
@if (!(bool)(ViewBag.AiPriceCheckEnabled ?? true)) @if (!(bool)(ViewBag.AiPriceCheckEnabled ?? true))
@@ -215,7 +215,7 @@
<div id="categoryModalError" class="alert alert-danger d-none"></div> <div id="categoryModalError" class="alert alert-danger d-none"></div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveCategoryBtn"> <button type="button" class="btn btn-primary" id="saveCategoryBtn">
<i class="bi bi-check-circle me-1"></i>Create Category <i class="bi bi-check-circle me-1"></i>Create Category
</button> </button>
@@ -238,7 +238,7 @@
<div id="categoryModalError" class="alert alert-danger d-none"></div> <div id="categoryModalError" class="alert alert-danger d-none"></div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveCategoryBtn"> <button type="button" class="btn btn-primary" id="saveCategoryBtn">
<i class="bi bi-check-circle me-1"></i>Create Category <i class="bi bi-check-circle me-1"></i>Create Category
</button> </button>
@@ -184,7 +184,7 @@
</div> </div>
<div class="d-flex gap-2 justify-content-end"> <div class="d-flex gap-2 justify-content-end">
<a asp-action="Index" class="btn btn-secondary">Cancel</a> <a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Create Company <i class="bi bi-save me-1"></i>Create Company
</button> </button>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Company.CompanyDto @model PowderCoating.Application.DTOs.Company.CompanyDto
@{ @{
ViewData["Title"] = Model.CompanyName; ViewData["Title"] = Model.CompanyName;
@@ -470,7 +470,7 @@
<i class="bi bi-fire me-1"></i>Reset All Company Data <i class="bi bi-fire me-1"></i>Reset All Company Data
</h6> </h6>
<p class="text-muted small mb-0"> <p class="text-muted small mb-0">
Permanently deletes <strong>all business data</strong> — customers, vendors, accounts, invoices, bills, jobs, quotes, inventory, catalog items, and more. Permanently deletes <strong>all business data</strong> customers, vendors, accounts, invoices, bills, jobs, quotes, inventory, catalog items, and more.
The company record, user accounts, and system configuration are preserved. The company record, user accounts, and system configuration are preserved.
Use this to wipe a migration and start fresh. Use this to wipe a migration and start fresh.
</p> </p>
@@ -491,7 +491,7 @@
There is no going back. There is no going back.
@if (Model.UserCount > 0) @if (Model.UserCount > 0)
{ {
<br /><strong class="text-danger">This company has @Model.UserCount user(s) — remove them first.</strong> <br /><strong class="text-danger">This company has @Model.UserCount user(s) remove them first.</strong>
} }
</p> </p>
</div> </div>
@@ -523,7 +523,7 @@
<!-- Loading spinner --> <!-- Loading spinner -->
<div id="oc-loading" class="d-flex justify-content-center align-items-center py-5"> <div id="oc-loading" class="d-flex justify-content-center align-items-center py-5">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading…</span> <span class="visually-hidden">Loading</span>
</div> </div>
</div> </div>
@@ -545,14 +545,14 @@
</div> </div>
<table class="table table-sm table-borderless mb-0 small"> <table class="table table-sm table-borderless mb-0 small">
<tr><th style="width:38%;" class="text-muted fw-normal">Role</th><td id="oc-role">—</td></tr> <tr><th style="width:38%;" class="text-muted fw-normal">Role</th><td id="oc-role"></td></tr>
<tr><th class="text-muted fw-normal">Department</th><td id="oc-dept">—</td></tr> <tr><th class="text-muted fw-normal">Department</th><td id="oc-dept"></td></tr>
<tr><th class="text-muted fw-normal">Position</th><td id="oc-position">—</td></tr> <tr><th class="text-muted fw-normal">Position</th><td id="oc-position"></td></tr>
<tr><th class="text-muted fw-normal">Phone</th><td id="oc-phone">—</td></tr> <tr><th class="text-muted fw-normal">Phone</th><td id="oc-phone"></td></tr>
<tr><th class="text-muted fw-normal">Hired</th><td id="oc-hire">—</td></tr> <tr><th class="text-muted fw-normal">Hired</th><td id="oc-hire"></td></tr>
<tr><th class="text-muted fw-normal">Account created</th><td id="oc-created">—</td></tr> <tr><th class="text-muted fw-normal">Account created</th><td id="oc-created"></td></tr>
<tr><th class="text-muted fw-normal">Last login</th><td id="oc-lastlogin">—</td></tr> <tr><th class="text-muted fw-normal">Last login</th><td id="oc-lastlogin"></td></tr>
<tr><th class="text-muted fw-normal">Email confirmed</th><td id="oc-emailconf">—</td></tr> <tr><th class="text-muted fw-normal">Email confirmed</th><td id="oc-emailconf"></td></tr>
</table> </table>
</div> </div>
@@ -625,7 +625,7 @@
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<input type="hidden" name="confirmation" id="resetDataConfirmHidden" /> <input type="hidden" name="confirmation" id="resetDataConfirmHidden" />
<div class="modal-footer"> <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" id="btnResetDataConfirm" class="btn btn-warning" disabled> <button type="submit" id="btnResetDataConfirm" class="btn btn-warning" disabled>
<i class="bi bi-fire me-1"></i>Reset All Data <i class="bi bi-fire me-1"></i>Reset All Data
</button> </button>
@@ -656,7 +656,7 @@
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<input type="hidden" name="confirmation" id="hardDeleteConfirmHidden" /> <input type="hidden" name="confirmation" id="hardDeleteConfirmHidden" />
<div class="modal-footer"> <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" id="btnHardDeleteConfirm" class="btn btn-danger" disabled> <button type="submit" id="btnHardDeleteConfirm" class="btn btn-danger" disabled>
<i class="bi bi-trash me-1"></i>Permanently Delete Company <i class="bi bi-trash me-1"></i>Permanently Delete Company
</button> </button>
@@ -694,7 +694,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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"> <button type="submit" class="btn btn-primary">
<i class="bi bi-key me-2"></i>Reset Password <i class="bi bi-key me-2"></i>Reset Password
</button> </button>
@@ -706,7 +706,7 @@
@section Scripts { @section Scripts {
<script> <script>
// Reset Data Modal — enable submit only when user types "DELETE" // Reset Data Modal enable submit only when user types "DELETE"
(function () { (function () {
var input = document.getElementById('resetDataConfirmInput'); var input = document.getElementById('resetDataConfirmInput');
var hidden = document.getElementById('resetDataConfirmHidden'); var hidden = document.getElementById('resetDataConfirmHidden');
@@ -725,7 +725,7 @@
} }
})(); })();
// Hard Delete Modal — enable submit only when user types "DELETE" // Hard Delete Modal enable submit only when user types "DELETE"
(function () { (function () {
var input = document.getElementById('hardDeleteConfirmInput'); var input = document.getElementById('hardDeleteConfirmInput');
var hidden = document.getElementById('hardDeleteConfirmHidden'); var hidden = document.getElementById('hardDeleteConfirmHidden');
@@ -760,7 +760,7 @@
}); });
} }
// ── User Details Modal ──────────────────────────────────────────────── // ── User Details Modal ────────────────────────────────────────────────
(function () { (function () {
const offcanvasEl = document.getElementById('userDetailOffcanvas'); const offcanvasEl = document.getElementById('userDetailOffcanvas');
const oc = new bootstrap.Modal(offcanvasEl); const oc = new bootstrap.Modal(offcanvasEl);
@@ -806,12 +806,12 @@
badge.textContent = u.isActive ? 'Active' : 'Inactive'; badge.textContent = u.isActive ? 'Active' : 'Inactive';
badge.className = 'badge ms-auto ' + (u.isActive ? 'bg-success' : 'bg-danger'); badge.className = 'badge ms-auto ' + (u.isActive ? 'bg-success' : 'bg-danger');
document.getElementById('oc-role').textContent = u.companyRole ? u.companyRole.replace('Company', '') : '—'; document.getElementById('oc-role').textContent = u.companyRole ? u.companyRole.replace('Company', '') : '';
document.getElementById('oc-dept').textContent = u.department || '—'; document.getElementById('oc-dept').textContent = u.department || '';
document.getElementById('oc-position').textContent = u.position || '—'; document.getElementById('oc-position').textContent = u.position || '';
document.getElementById('oc-phone').textContent = u.phone || '—'; document.getElementById('oc-phone').textContent = u.phone || '';
document.getElementById('oc-hire').textContent = u.hireDate || '—'; document.getElementById('oc-hire').textContent = u.hireDate || '';
document.getElementById('oc-created').textContent = u.createdAt || '—'; document.getElementById('oc-created').textContent = u.createdAt || '';
document.getElementById('oc-lastlogin').textContent = u.lastLoginDate || 'Never'; document.getElementById('oc-lastlogin').textContent = u.lastLoginDate || 'Never';
document.getElementById('oc-emailconf').innerHTML = u.emailConfirmed document.getElementById('oc-emailconf').innerHTML = u.emailConfirmed
? '<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>Yes</span>' ? '<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>Yes</span>'
@@ -160,7 +160,7 @@
</div> </div>
<div class="d-flex gap-2 justify-content-end"> <div class="d-flex gap-2 justify-content-end">
<a asp-action="Index" class="btn btn-secondary">Cancel</a> <a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Save Changes <i class="bi bi-save me-1"></i>Save Changes
</button> </button>
@@ -1,4 +1,4 @@
@model List<PowderCoating.Application.DTOs.Company.CompanyListDto> @model List<PowderCoating.Application.DTOs.Company.CompanyListDto>
@section Styles { @section Styles {
<style> <style>
@@ -58,7 +58,7 @@
<div class="input-group"> <div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span> <span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="searchTerm" class="form-control" <input type="text" name="searchTerm" class="form-control"
placeholder="Search by name, code, email, phone…" placeholder="Search by name, code, email, phone"
value="@searchTerm" /> value="@searchTerm" />
</div> </div>
</div> </div>
@@ -230,7 +230,7 @@
</table> </table>
</div> </div>
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) --> <!-- Mobile card view shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<div class="mobile-card-view"> <div class="mobile-card-view">
<div class="mobile-card-list"> <div class="mobile-card-list">
@foreach (var company in Model) @foreach (var company in Model)
@@ -274,7 +274,7 @@
} }
</div> </div>
<div class="mobile-card-footer"> <div class="mobile-card-footer">
<span class="btn btn-sm btn-outline-primary">View →</span> <span class="btn btn-sm btn-outline-primary">View </span>
</div> </div>
</a> </a>
} }
@@ -286,7 +286,7 @@
{ {
<div class="card-footer d-flex justify-content-between align-items-center"> <div class="card-footer d-flex justify-content-between align-items-center">
<div class="text-muted small"> <div class="text-muted small">
Showing @((pageNumber - 1) * pageSize + 1)–@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount companies Showing @((pageNumber - 1) * pageSize + 1)@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount companies
</div> </div>
<div class="d-flex align-items-center gap-3"> <div class="d-flex align-items-center gap-3">
<div> <div>
@@ -413,7 +413,7 @@
<h6 class="fw-bold text-danger"><i class="bi bi-fire me-2"></i>Hard Delete (Permanent)</h6> <h6 class="fw-bold text-danger"><i class="bi bi-fire me-2"></i>Hard Delete (Permanent)</h6>
<p class="text-muted small mb-2"> <p class="text-muted small mb-2">
<strong class="text-danger">This cannot be undone.</strong> <strong class="text-danger">This cannot be undone.</strong>
All company data — users, jobs, quotes, customers, invoices, and everything else — will be All company data users, jobs, quotes, customers, invoices, and everything else will be
<strong>permanently and irreversibly deleted</strong> from the database. <strong>permanently and irreversibly deleted</strong> from the database.
</p> </p>
<div class="alert alert-danger alert-permanent py-2 mb-3"> <div class="alert alert-danger alert-permanent py-2 mb-3">
@@ -439,7 +439,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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>
</div> </div>
</div> </div>
</div> </div>
@@ -777,7 +777,7 @@
<div id="ovenErrorMsg" class="alert alert-danger d-none"></div> <div id="ovenErrorMsg" class="alert alert-danger d-none"></div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="saveOven()">Save</button> <button type="button" class="btn btn-primary" onclick="saveOven()">Save</button>
</div> </div>
</div> </div>
@@ -71,7 +71,7 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveJobStatusBtn"> <button type="button" class="btn btn-primary" id="saveJobStatusBtn">
<i class="bi bi-check-circle me-1"></i>Save <i class="bi bi-check-circle me-1"></i>Save
</button> </button>
@@ -121,7 +121,7 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveJobPriorityBtn"> <button type="button" class="btn btn-primary" id="saveJobPriorityBtn">
<i class="bi bi-check-circle me-1"></i>Save <i class="bi bi-check-circle me-1"></i>Save
</button> </button>
@@ -195,7 +195,7 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveQuoteStatusBtn"> <button type="button" class="btn btn-primary" id="saveQuoteStatusBtn">
<i class="bi bi-check-circle me-1"></i>Save <i class="bi bi-check-circle me-1"></i>Save
</button> </button>
@@ -242,7 +242,7 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveInventoryCategoryBtn"> <button type="button" class="btn btn-primary" id="saveInventoryCategoryBtn">
<i class="bi bi-check-circle me-1"></i>Save <i class="bi bi-check-circle me-1"></i>Save
</button> </button>
@@ -335,7 +335,7 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveAppointmentTypeBtn"> <button type="button" class="btn btn-primary" id="saveAppointmentTypeBtn">
<i class="bi bi-check-circle me-1"></i>Save <i class="bi bi-check-circle me-1"></i>Save
</button> </button>
@@ -387,7 +387,7 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="savePrepServiceBtn"> <button type="button" class="btn btn-primary" id="savePrepServiceBtn">
<i class="bi bi-check-circle me-1"></i>Save <i class="bi bi-check-circle me-1"></i>Save
</button> </button>
@@ -490,7 +490,7 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveBlastSetupBtn"> <button type="button" class="btn btn-primary" id="saveBlastSetupBtn">
<i class="bi bi-check-circle me-1"></i>Save <i class="bi bi-check-circle me-1"></i>Save
</button> </button>
@@ -201,7 +201,7 @@
</div> </div>
<div class="d-flex gap-2 justify-content-end"> <div class="d-flex gap-2 justify-content-end">
<a asp-action="Index" class="btn btn-secondary">Cancel</a> <a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Create User <i class="bi bi-save me-1"></i>Create User
</button> </button>
@@ -221,11 +221,11 @@
@if (!string.IsNullOrEmpty(ViewBag.ReturnUrl)) @if (!string.IsNullOrEmpty(ViewBag.ReturnUrl))
{ {
<input type="hidden" name="returnUrl" value="@ViewBag.ReturnUrl" /> <input type="hidden" name="returnUrl" value="@ViewBag.ReturnUrl" />
<a href="@ViewBag.ReturnUrl" class="btn btn-secondary">Cancel</a> <a href="@ViewBag.ReturnUrl" class="btn btn-outline-secondary">Cancel</a>
} }
else else
{ {
<a asp-action="Index" class="btn btn-secondary">Cancel</a> <a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
} }
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Save Changes <i class="bi bi-save me-1"></i>Save Changes
@@ -1,4 +1,4 @@
@model PagedResult<PowderCoating.Application.DTOs.User.CompanyUserListDto> @model PagedResult<PowderCoating.Application.DTOs.User.CompanyUserListDto>
@{ @{
ViewData["Title"] = "Manage Users"; ViewData["Title"] = "Manage Users";
@@ -258,7 +258,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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-danger"><i class="bi bi-slash-circle"></i> Ban User</button> <button type="submit" class="btn btn-danger"><i class="bi bi-slash-circle"></i> Ban User</button>
</div> </div>
</form> </form>
@@ -7,7 +7,7 @@
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0"><i class="bi bi-journal-minus me-2 text-primary"></i>Issue Credit Memo</h4> <h4 class="mb-0"><i class="bi bi-journal-minus me-2 text-primary"></i>Issue Credit Memo</h4>
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Credit Memos <i class="bi bi-arrow-left me-1"></i>Back to Credit Memos
</a> </a>
</div> </div>
@@ -59,7 +59,7 @@
<i class="bi bi-x-circle me-1"></i>Void <i class="bi bi-x-circle me-1"></i>Void
</button> </button>
} }
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back <i class="bi bi-arrow-left me-1"></i>Back
</a> </a>
</div> </div>
@@ -247,14 +247,14 @@
@if (openInvoices.Any()) @if (openInvoices.Any())
{ {
<div class="modal-footer"> <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">Apply Credit</button> <button type="submit" class="btn btn-primary">Apply Credit</button>
</div> </div>
} }
else else
{ {
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
} }
</form> </form>
@@ -293,7 +293,7 @@
} }
</div> </div>
<div class="modal-footer"> <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-danger">Void Credit Memo</button> <button type="submit" class="btn btn-danger">Void Credit Memo</button>
</div> </div>
</form> </form>
@@ -10,7 +10,7 @@
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0"><i class="bi bi-journal-minus me-2 text-primary"></i>Credit Memos</h4> <h4 class="mb-0"><i class="bi bi-journal-minus me-2 text-primary"></i>Credit Memos</h4>
<a asp-action="Create" class="btn btn-primary btn-sm"> <a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>Issue Credit Memo <i class="bi bi-plus-lg me-1"></i>Issue Credit Memo
</a> </a>
</div> </div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel @model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
@using Microsoft.AspNetCore.Html @using Microsoft.AspNetCore.Html
@using PowderCoating.Application.DTOs.Health @using PowderCoating.Application.DTOs.Health
@using PowderCoating.Web.ViewModels.Dashboard @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);"> <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) @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 else
{ {
@@ -59,7 +59,7 @@
</div> </div>
</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 id="pwa-install-banner" class="row mb-4" style="display:none!important;">
<div class="col-12"> <div class="col-12">
<div class="alert alert-permanent mb-0 d-flex align-items-start gap-3 py-3" <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) @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) @if (configHealth != null && !configHealth.IsHealthy)
{ {
<div class="row mb-4"> <div class="row mb-4">
@@ -406,15 +406,15 @@
} }
@if (Model.AgingDays1To30 > 0) @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) @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) @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) @if (Model.AgingDaysOver90 > 0)
{ {
@@ -536,7 +536,7 @@
@if (line.EstCost.HasValue) @if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>} {<span>@line.EstCost.Value.ToString("C")</span>}
else else
{<span class="text-muted">—</span>} {<span class="text-muted"></span>}
</td> </td>
<td class="text-center"> <td class="text-center">
<button class="btn btn-sm btn-outline-danger mark-ordered-btn text-nowrap" <button class="btn btn-sm btn-outline-danger mark-ordered-btn text-nowrap"
@@ -552,7 +552,7 @@
<tr> <tr>
<td colspan="2">Vendor Total</td> <td colspan="2">Vendor Total</td>
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</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> <td></td>
</tr> </tr>
</tfoot> </tfoot>
@@ -571,7 +571,7 @@
<div class="card border-0 shadow-sm dashboard-card" style="border-left: 4px solid #0d6efd !important;"> <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"> <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"> <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> <span class="ms-2 text-muted fw-normal small" id="placed-count-label">@Model.PowderOrdersPlacedCount item@(Model.PowderOrdersPlacedCount == 1 ? "" : "s")</span>
</h5> </h5>
<small class="text-muted">Grouped by vendor &middot; Enter lbs received to update inventory</small> <small class="text-muted">Grouped by vendor &middot; Enter lbs received to update inventory</small>
@@ -630,13 +630,13 @@
@if (line.EstCost.HasValue) @if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>} {<span>@line.EstCost.Value.ToString("C")</span>}
else else
{<span class="text-muted">—</span>} {<span class="text-muted"></span>}
</td> </td>
<td class="text-muted small"> <td class="text-muted small">
@if (line.OrderedAt.HasValue) @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>} {<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 else
{<span>—</span>} {<span></span>}
</td> </td>
<td class="text-center"> <td class="text-center">
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-@line.CoatId"> <div class="d-flex align-items-center gap-1 justify-content-center receive-form-@line.CoatId">
@@ -669,7 +669,7 @@
<tr> <tr>
<td colspan="2">Vendor Total</td> <td colspan="2">Vendor Total</td>
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</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> <td colspan="2"></td>
</tr> </tr>
</tfoot> </tfoot>
@@ -739,7 +739,7 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Category</label> <label class="form-label fw-medium">Category</label>
<select class="form-select" id="apm-categoryId" name="inventoryCategoryId"> <select class="form-select" id="apm-categoryId" name="inventoryCategoryId">
<option value="">— Select category —</option> <option value=""> Select category </option>
@if (ViewBag.InventoryCategories != null) @if (ViewBag.InventoryCategories != null)
{ {
foreach (var cat in (IEnumerable<dynamic>)ViewBag.InventoryCategories) foreach (var cat in (IEnumerable<dynamic>)ViewBag.InventoryCategories)
@@ -753,7 +753,7 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Primary Vendor</label> <label class="form-label fw-medium">Primary Vendor</label>
<select class="form-select" id="apm-vendorId" name="primaryVendorId"> <select class="form-select" id="apm-vendorId" name="primaryVendorId">
<option value="">— Select vendor —</option> <option value=""> Select vendor </option>
@if (ViewBag.VendorList != null) @if (ViewBag.VendorList != null)
{ {
foreach (var v in (IEnumerable<dynamic>)ViewBag.VendorList) foreach (var v in (IEnumerable<dynamic>)ViewBag.VendorList)
@@ -814,7 +814,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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"> <button type="submit" class="btn btn-primary" id="apm-saveBtn">
<i class="bi bi-plus-circle me-1"></i>Add to Inventory <i class="bi bi-plus-circle me-1"></i>Add to Inventory
</button> </button>
@@ -883,7 +883,7 @@
const esc = s => s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : ''; 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 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'; const lbsFmt = c.lbsToOrder ? parseFloat(c.lbsToOrder).toFixed(2) : '0.00';
// Find or create vendor group // 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>` : ''} ${c.finish ? `<span class="badge bg-light text-dark border ms-1">${esc(c.finish)}</span>` : ''}
</td> </td>
<td class="text-end fw-medium">${lbsFmt} lbs</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-muted small">${orderedDate}</td>
<td class="text-center"> <td class="text-center">
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-${c.coatId}"> <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'); 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) { if (!hasInv) {
const modal = document.getElementById('addPowderModal'); const modal = document.getElementById('addPowderModal');
// Pre-fill hidden + text fields // Pre-fill hidden + text fields
@@ -1024,7 +1024,7 @@
return; return;
} }
// Inventory item exists → receive directly // Inventory item exists → receive directly
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content; ?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
@@ -1065,7 +1065,7 @@
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content; ?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
saveBtn.disabled = true; 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 { try {
const resp = await fetch('@Url.Action("AddCustomPowderToInventory", "Dashboard")', { 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 () { (function () {
const apmBtn = document.getElementById('apm-ai-btn'); const apmBtn = document.getElementById('apm-ai-btn');
const apmStatusEl = document.getElementById('apm-ai-status'); const apmStatusEl = document.getElementById('apm-ai-status');
@@ -1144,7 +1144,7 @@
const hasInput = manufacturer || colorName || colorCode || partNumber || itemName; const hasInput = manufacturer || colorName || colorCode || partNumber || itemName;
if (!hasInput) { 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; return;
} }
@@ -1153,7 +1153,7 @@
document.getElementById('apm-bad-match-btn')?.remove(); document.getElementById('apm-bad-match-btn')?.remove();
apmBtn.disabled = true; apmBtn.disabled = true;
apmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Looking up...'; 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 { try {
const formData = new FormData(); const formData = new FormData();
@@ -1220,7 +1220,7 @@
: ''; : '';
apmShowStatus('success', `<i class="bi bi-check-circle me-1"></i>Auto-filled: ${filled.join(', ')}.${reasoning}`); apmShowStatus('success', `<i class="bi bi-check-circle me-1"></i>Auto-filled: ${filled.join(', ')}.${reasoning}`);
} else { } 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) { } catch (err) {
@@ -1274,7 +1274,7 @@
(function () { (function () {
var DISMISSED_KEY = 'pcl_pwa_banner_dismissed'; 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 || var isStandalone = window.navigator.standalone === true ||
window.matchMedia('(display-mode: standalone)').matches; window.matchMedia('(display-mode: standalone)').matches;
if (isStandalone) return; if (isStandalone) return;
@@ -1298,7 +1298,7 @@
var isSafari = /webkit/i.test(ua) && !/crios|chrome|fxios|opios/i.test(ua); var isSafari = /webkit/i.test(ua) && !/crios|chrome|fxios|opios/i.test(ua);
if (isSafari) { if (isSafari) {
titleEl.textContent = 'Add to Home Screen'; 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 ' + '<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>.'; 'and tap <strong>Add to Home Screen</strong>.';
} else { } else {
@@ -1,4 +1,4 @@
@using PowderCoating.Web.Controllers @using PowderCoating.Web.Controllers
@model List<EntityPurgeStat> @model List<EntityPurgeStat>
@{ @{
ViewData["Title"] = "Data Purge & Cleanup"; ViewData["Title"] = "Data Purge & Cleanup";
@@ -59,7 +59,7 @@
<div class="alert alert-warning alert-permanent d-flex gap-3 align-items-start mb-3"> <div class="alert alert-warning alert-permanent d-flex gap-3 align-items-start mb-3">
<i class="bi bi-exclamation-triangle-fill fs-4 flex-shrink-0 mt-1"></i> <i class="bi bi-exclamation-triangle-fill fs-4 flex-shrink-0 mt-1"></i>
<div> <div>
<strong>Destructive operation — this cannot be undone.</strong> <strong>Destructive operation this cannot be undone.</strong>
Purging permanently deletes records from the database. Soft-deleted records are hidden from users but still occupy database space. Use this tool periodically to reclaim space and keep the database clean. Purging permanently deletes records from the database. Soft-deleted records are hidden from users but still occupy database space. Use this tool periodically to reclaim space and keep the database clean.
Job photo blobs in Azure Storage are also deleted when purging job photo records. Job photo blobs in Azure Storage are also deleted when purging job photo records.
</div> </div>
@@ -83,8 +83,8 @@
<th style="width:36px"></th> <th style="width:36px"></th>
<th>Entity</th> <th>Entity</th>
<th class="text-end" style="width:90px">Total</th> <th class="text-end" style="width:90px">Total</th>
<th class="text-end" style="width:100px">0–30d</th> <th class="text-end" style="width:100px">030d</th>
<th class="text-end" style="width:100px">30–90d</th> <th class="text-end" style="width:100px">3090d</th>
<th class="text-end" style="width:100px">&gt;90d</th> <th class="text-end" style="width:100px">&gt;90d</th>
<th style="width:130px">Oldest</th> <th style="width:130px">Oldest</th>
<th style="width:42px"> <th style="width:42px">
@@ -109,7 +109,7 @@
} }
else else
{ {
<span class="text-muted">—</span> <span class="text-muted"></span>
} }
</td> </td>
<td class="text-end"> <td class="text-end">
@@ -117,24 +117,24 @@
{ {
<span class="badge bg-success-subtle text-success">@s.DeletedLast30Days</span> <span class="badge bg-success-subtle text-success">@s.DeletedLast30Days</span>
} }
else { <span class="text-muted">—</span> } else { <span class="text-muted"></span> }
</td> </td>
<td class="text-end"> <td class="text-end">
@if (s.Deleted30To90Days > 0) @if (s.Deleted30To90Days > 0)
{ {
<span class="badge bg-warning-subtle text-warning">@s.Deleted30To90Days</span> <span class="badge bg-warning-subtle text-warning">@s.Deleted30To90Days</span>
} }
else { <span class="text-muted">—</span> } else { <span class="text-muted"></span> }
</td> </td>
<td class="text-end"> <td class="text-end">
@if (s.DeletedOlderThan90Days > 0) @if (s.DeletedOlderThan90Days > 0)
{ {
<span class="badge bg-danger-subtle text-danger">@s.DeletedOlderThan90Days</span> <span class="badge bg-danger-subtle text-danger">@s.DeletedOlderThan90Days</span>
} }
else { <span class="text-muted">—</span> } else { <span class="text-muted"></span> }
</td> </td>
<td class="text-muted"> <td class="text-muted">
@(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "—") @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "")
</td> </td>
<td class="text-center"> <td class="text-center">
<input type="checkbox" class="form-check-input entity-select" <input type="checkbox" class="form-check-input entity-select"
@@ -149,7 +149,7 @@
</table> </table>
</div> </div>
<!-- Mobile card view for this group — shown on screens < 992px --> <!-- Mobile card view for this group shown on screens < 992px -->
<div class="mobile-card-view"> <div class="mobile-card-view">
<div class="px-3 pt-2 pb-1"> <div class="px-3 pt-2 pb-1">
<span class="text-muted text-uppercase fw-semibold" style="font-size:0.7rem;letter-spacing:.05em">@group.Key</span> <span class="text-muted text-uppercase fw-semibold" style="font-size:0.7rem;letter-spacing:.05em">@group.Key</span>
@@ -164,7 +164,7 @@
</div> </div>
<div class="mobile-card-title"> <div class="mobile-card-title">
<h6>@s.Label</h6> <h6>@s.Label</h6>
<small>Oldest: @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "—")</small> <small>Oldest: @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "")</small>
</div> </div>
</div> </div>
<div class="mobile-card-body"> <div class="mobile-card-body">
@@ -175,11 +175,11 @@
{ {
<span class="badge bg-secondary">@s.Total</span> <span class="badge bg-secondary">@s.Total</span>
} }
else { <span class="text-muted">—</span> } else { <span class="text-muted"></span> }
</span> </span>
</div> </div>
<div class="mobile-card-row"> <div class="mobile-card-row">
<span class="mobile-card-label">0–30d / 30–90d / &gt;90d</span> <span class="mobile-card-label">030d / 3090d / &gt;90d</span>
<span class="mobile-card-value">@s.DeletedLast30Days / @s.Deleted30To90Days / @s.DeletedOlderThan90Days</span> <span class="mobile-card-value">@s.DeletedLast30Days / @s.Deleted30To90Days / @s.DeletedOlderThan90Days</span>
</div> </div>
</div> </div>
@@ -279,7 +279,7 @@
<p class="mb-0 text-muted">Are you sure you want to continue?</p> <p class="mb-0 text-muted">Are you sure you want to continue?</p>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-danger" id="confirmPurgeBtn"> <button type="button" class="btn btn-danger" id="confirmPurgeBtn">
<i class="bi bi-trash3-fill me-2"></i>Yes, Purge Permanently <i class="bi bi-trash3-fill me-2"></i>Yes, Purge Permanently
</button> </button>
@@ -300,7 +300,7 @@
const confirmModal = new bootstrap.Modal(document.getElementById('confirmModal')); const confirmModal = new bootstrap.Modal(document.getElementById('confirmModal'));
const confirmSummary= document.getElementById('confirmSummary'); const confirmSummary= document.getElementById('confirmSummary');
// ── Select all ────────────────────────────────────────────────────────── // ── Select all ──────────────────────────────────────────────────────────
selectAll.addEventListener('change', () => { selectAll.addEventListener('change', () => {
document.querySelectorAll('.entity-select:not(:disabled)').forEach(cb => { document.querySelectorAll('.entity-select:not(:disabled)').forEach(cb => {
cb.checked = selectAll.checked; cb.checked = selectAll.checked;
@@ -308,7 +308,7 @@
updatePurgeBtn(); updatePurgeBtn();
}); });
// ── Group select all ──────────────────────────────────────────────────── // ── Group select all ────────────────────────────────────────────────────
document.querySelectorAll('.group-select-all').forEach(ga => { document.querySelectorAll('.group-select-all').forEach(ga => {
ga.addEventListener('change', () => { ga.addEventListener('change', () => {
document.querySelectorAll(`.entity-select[data-group="${ga.dataset.group}"]:not(:disabled)`) document.querySelectorAll(`.entity-select[data-group="${ga.dataset.group}"]:not(:disabled)`)
@@ -335,7 +335,7 @@
previewRes.classList.add('d-none'); previewRes.classList.add('d-none');
} }
// ── Preview ───────────────────────────────────────────────────────────── // ── Preview ─────────────────────────────────────────────────────────────
previewBtn.addEventListener('click', async () => { previewBtn.addEventListener('click', async () => {
const entities = getSelectedEntities(); const entities = getSelectedEntities();
if (!entities.length) { if (!entities.length) {
@@ -344,7 +344,7 @@
} }
previewBtn.disabled = true; previewBtn.disabled = true;
previewBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading…'; previewBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading';
const days = document.getElementById('olderThanDays').value; const days = document.getElementById('olderThanDays').value;
const token = document.querySelector('input[name="__RequestVerificationToken"]').value; const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
@@ -379,7 +379,7 @@
} }
}); });
// ── Purge button → modal ──────────────────────────────────────────────── // ── Purge button modal ────────────────────────────────────────────────
purgeBtn.addEventListener('click', () => { purgeBtn.addEventListener('click', () => {
const entities = getSelectedEntities(); const entities = getSelectedEntities();
const days = document.getElementById('olderThanDays').value; const days = document.getElementById('olderThanDays').value;
@@ -390,7 +390,7 @@
confirmModal.show(); confirmModal.show();
}); });
// ── Confirm → submit form ─────────────────────────────────────────────── // ── Confirm submit form ───────────────────────────────────────────────
document.getElementById('confirmPurgeBtn').addEventListener('click', () => { document.getElementById('confirmPurgeBtn').addEventListener('click', () => {
const entities = getSelectedEntities(); const entities = getSelectedEntities();
const days = document.getElementById('olderThanDays').value; const days = document.getElementById('olderThanDays').value;
@@ -8,7 +8,7 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-end mb-3"> <div class="d-flex justify-content-end mb-3">
<a asp-action="Index" class="btn btn-secondary"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Diagnostics <i class="bi bi-arrow-left"></i> Back to Diagnostics
</a> </a>
</div> </div>
@@ -183,7 +183,7 @@
class="btn btn-outline-secondary btn-sm"> class="btn btn-outline-secondary btn-sm">
<i class="bi bi-box-arrow-up-right me-1"></i>Open in New Tab <i class="bi bi-box-arrow-up-right me-1"></i>Open in New Tab
</a> </a>
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -7,7 +7,7 @@
<div class="col-lg-8"> <div class="col-lg-8">
<div class="mb-4"> <div class="mb-4">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Asset Register <i class="bi bi-arrow-left me-1"></i>Back to Asset Register
</a> </a>
</div> </div>
@@ -14,7 +14,7 @@
} }
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Asset Register <i class="bi bi-arrow-left me-1"></i>Asset Register
</a> </a>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
@@ -9,7 +9,7 @@
<div class="col-lg-8"> <div class="col-lg-8">
<div class="mb-4"> <div class="mb-4">
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary btn-sm"> <a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Asset <i class="bi bi-arrow-left me-1"></i>Back to Asset
</a> </a>
</div> </div>
@@ -40,7 +40,7 @@
<a asp-controller="Dashboard" asp-action="Index" class="btn btn-primary"> <a asp-controller="Dashboard" asp-action="Index" class="btn btn-primary">
<i class="bi bi-house-door"></i> Go to Dashboard <i class="bi bi-house-door"></i> Go to Dashboard
</a> </a>
<button onclick="history.back()" class="btn btn-secondary"> <button onclick="history.back()" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Go Back <i class="bi bi-arrow-left"></i> Go Back
</button> </button>
</div> </div>
@@ -606,7 +606,7 @@
</div> </div>
</div> </div>
<div class="modal-footer border-0"> <div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -633,7 +633,7 @@
</div> </div>
</div> </div>
<div class="modal-footer border-0"> <div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -37,7 +37,7 @@
</div> </div>
} }
</div> </div>
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Inventory <i class="bi bi-arrow-left me-1"></i>Back to Inventory
</a> </a>
</div> </div>
@@ -80,7 +80,7 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-search"></i></button> <button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-search"></i></button>
<a asp-action="Ledger" class="btn btn-outline-secondary btn-sm">Clear</a> <a asp-action="Ledger" class="btn btn-outline-secondary">Clear</a>
</div> </div>
</div> </div>
</form> </form>
@@ -439,7 +439,7 @@
<div id="gcModalError" class="alert alert-danger d-none"></div> <div id="gcModalError" class="alert alert-danger d-none"></div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-warning" onclick="addGiftCertLineItem(this)"> <button type="button" class="btn btn-warning" onclick="addGiftCertLineItem(this)">
<i class="bi bi-plus-circle me-1"></i>Add to Invoice <i class="bi bi-plus-circle me-1"></i>Add to Invoice
</button> </button>
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.Invoice @using PowderCoating.Application.DTOs.Invoice
@using PowderCoating.Core.Enums @using PowderCoating.Core.Enums
@using PowderCoating.Web.Controllers @using PowderCoating.Web.Controllers
@model InvoiceDto @model InvoiceDto
@@ -69,7 +69,7 @@
<div class="alert alert-warning alert-permanent d-flex align-items-center gap-2 mb-4"> <div class="alert alert-warning alert-permanent d-flex align-items-center gap-2 mb-4">
<i class="bi bi-envelope-slash fs-5"></i> <i class="bi bi-envelope-slash fs-5"></i>
<span> <span>
<strong>@Model.CustomerName</strong> has no email address on file — you'll be prompted to enter one when sending. <strong>@Model.CustomerName</strong> has no email address on file you'll be prompted to enter one when sending.
<a asp-controller="Customers" asp-action="Edit" asp-route-id="@Model.CustomerId" class="alert-link">Add one in customer settings</a>. <a asp-controller="Customers" asp-action="Edit" asp-route-id="@Model.CustomerId" class="alert-link">Add one in customer settings</a>.
</span> </span>
</div> </div>
@@ -168,12 +168,12 @@
<div class="col-md-4"> <div class="col-md-4">
<label class="text-muted small mb-1">Due Date</label> <label class="text-muted small mb-1">Due Date</label>
<p class="mb-0 @(Model.Status == InvoiceStatus.Overdue ? "text-danger fw-bold" : "")"> <p class="mb-0 @(Model.Status == InvoiceStatus.Overdue ? "text-danger fw-bold" : "")">
@(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "—") @(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "")
</p> </p>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="text-muted small mb-1">Sent Date</label> <label class="text-muted small mb-1">Sent Date</label>
<p class="mb-0">@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "—")</p> <p class="mb-0">@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "")</p>
</div> </div>
@if (!string.IsNullOrWhiteSpace(Model.CustomerPO)) @if (!string.IsNullOrWhiteSpace(Model.CustomerPO))
{ {
@@ -339,7 +339,7 @@
</span> </span>
</td> </td>
<td class="text-muted"> <td class="text-muted">
@(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "—") @(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "")
</td> </td>
<td class="text-end fw-semibold">@gcItem.TotalPrice.ToString("C")</td> <td class="text-end fw-semibold">@gcItem.TotalPrice.ToString("C")</td>
<td> <td>
@@ -385,7 +385,7 @@
<tr> <tr>
<td>@p.PaymentDate.ToString("MM/dd/yyyy")</td> <td>@p.PaymentDate.ToString("MM/dd/yyyy")</td>
<td>@p.PaymentMethodDisplay</td> <td>@p.PaymentMethodDisplay</td>
<td>@(p.Reference ?? "—")</td> <td>@(p.Reference ?? "")</td>
<td> <td>
@if (!string.IsNullOrEmpty(p.DepositAccountName)) @if (!string.IsNullOrEmpty(p.DepositAccountName))
{ {
@@ -393,10 +393,10 @@
} }
else else
{ {
<span class="text-muted">—</span> <span class="text-muted"></span>
} }
</td> </td>
<td>@(p.RecordedByName ?? "—")</td> <td>@(p.RecordedByName ?? "")</td>
<td class="text-end fw-semibold text-success">@p.Amount.ToString("C")</td> <td class="text-end fw-semibold text-success">@p.Amount.ToString("C")</td>
<td class="text-end"> <td class="text-end">
@if (!isVoided) @if (!isVoided)
@@ -452,7 +452,7 @@
<td>@r.RefundDate.ToString("MM/dd/yyyy")</td> <td>@r.RefundDate.ToString("MM/dd/yyyy")</td>
<td>@r.RefundMethodDisplay</td> <td>@r.RefundMethodDisplay</td>
<td>@r.Reason</td> <td>@r.Reason</td>
<td>@(r.Reference ?? "—")</td> <td>@(r.Reference ?? "")</td>
<td><span class="badge bg-@refundStatusColor">@r.Status</span></td> <td><span class="badge bg-@refundStatusColor">@r.Status</span></td>
<td class="text-end fw-semibold text-danger">(@r.Amount.ToString("C"))</td> <td class="text-end fw-semibold text-danger">(@r.Amount.ToString("C"))</td>
<td class="text-nowrap"> <td class="text-nowrap">
@@ -564,7 +564,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Invoice Actions" data-bs-title="Invoice Actions"
data-bs-content="Workflow: Edit (Draft only) → Send Invoice (locks it, emails customer) → Record Payment. Partial payments are supported — record multiple payments until fully paid. Void cancels the invoice and reverses the customer balance without deleting history. Delete is only available for Drafts."> data-bs-content="Workflow: Edit (Draft only) Send Invoice (locks it, emails customer) Record Payment. Partial payments are supported record multiple payments until fully paid. Void cancels the invoice and reverses the customer balance without deleting history. Delete is only available for Drafts.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -761,7 +761,7 @@
{ {
<div class="mb-2"> <div class="mb-2">
<span class="badge bg-success-subtle text-success mb-2"> <span class="badge bg-success-subtle text-success mb-2">
<i class="bi bi-check-circle me-1"></i>Active — expires @Model.PaymentLinkExpiresAt!.Value.ToString("MMM d") <i class="bi bi-check-circle me-1"></i>Active expires @Model.PaymentLinkExpiresAt!.Value.ToString("MMM d")
</span> </span>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="text" id="paymentLinkInput" class="form-control font-monospace" <input type="text" id="paymentLinkInput" class="form-control font-monospace"
@@ -899,7 +899,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Payment Reference" data-bs-title="Payment Reference"
data-bs-content="Optional identifier for reconciliation — e.g., the check number, last 4 digits of the card, ACH transaction ID, or Venmo/PayPal confirmation code. Appears in payment history so you can match payments to your bank statement."> data-bs-content="Optional identifier for reconciliation e.g., the check number, last 4 digits of the card, ACH transaction ID, or Venmo/PayPal confirmation code. Appears in payment history so you can match payments to your bank statement.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -1054,7 +1054,7 @@
</div> </div>
</div> </div>
<div class="modal-footer d-none" id="resendInvoiceFooter"> <div class="modal-footer d-none" id="resendInvoiceFooter">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1095,7 +1095,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1118,7 +1118,7 @@
</div> </div>
<div id="refundAlertCredit" class="alert alert-success small mb-3 d-none"> <div id="refundAlertCredit" class="alert alert-success small mb-3 d-none">
<i class="bi bi-piggy-bank me-1"></i> <i class="bi bi-piggy-bank me-1"></i>
The refund amount will be added to the customer's store credit balance immediately — no manual action needed. The refund amount will be added to the customer's store credit balance immediately no manual action needed.
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Amount <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Amount <span class="text-danger">*</span></label>
@@ -1240,7 +1240,7 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Select Credit Memo <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Select Credit Memo <span class="text-danger">*</span></label>
<select name="CreditMemoId" class="form-select" required> <select name="CreditMemoId" class="form-select" required>
<option value="">— Select —</option> <option value=""> Select </option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AvailableCreditMemos) @foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AvailableCreditMemos)
{ {
<option value="@item.Value">@item.Text</option> <option value="@item.Value">@item.Text</option>
@@ -1254,7 +1254,7 @@
<input type="number" name="Amount" class="form-control" step="0.01" min="0.01" <input type="number" name="Amount" class="form-control" step="0.01" min="0.01"
max="@Model.BalanceDue.ToString("F2")" value="@Model.BalanceDue.ToString("F2")" required /> max="@Model.BalanceDue.ToString("F2")" value="@Model.BalanceDue.ToString("F2")" required />
</div> </div>
<div class="form-text">Balance due: @Model.BalanceDue.ToString("C") — the system will cap at the memo's remaining balance.</div> <div class="form-text">Balance due: @Model.BalanceDue.ToString("C") the system will cap at the memo's remaining balance.</div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -1336,7 +1336,7 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Bad Debt Expense Account</label> <label class="form-label">Bad Debt Expense Account</label>
<select name="expenseAccountId" class="form-select"> <select name="expenseAccountId" class="form-select">
<option value="">— Use default bad debt account —</option> <option value=""> Use default bad debt account </option>
@if (ViewBag.ExpenseAccounts != null) @if (ViewBag.ExpenseAccounts != null)
{ {
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts) @foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
@@ -1397,8 +1397,8 @@
const max = Math.min(data.remainingBalance, @Model.BalanceDue.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)); const max = Math.min(data.remainingBalance, @Model.BalanceDue.ToString("F2", System.Globalization.CultureInfo.InvariantCulture));
document.getElementById('gcAmountInput').value = max.toFixed(2); document.getElementById('gcAmountInput').value = max.toFixed(2);
document.getElementById('gcAmountInput').max = max; document.getElementById('gcAmountInput').max = max;
const expiry = data.expiryDate ? ` · Expires ${data.expiryDate}` : ''; const expiry = data.expiryDate ? ` · Expires ${data.expiryDate}` : '';
result.innerHTML = `<div class="alert alert-success py-1 mb-0 small"><i class="bi bi-check-circle me-1"></i><strong>${data.certificateCode}</strong> — $${data.remainingBalance.toFixed(2)} remaining${expiry}</div>`; result.innerHTML = `<div class="alert alert-success py-1 mb-0 small"><i class="bi bi-check-circle me-1"></i><strong>${data.certificateCode}</strong> $${data.remainingBalance.toFixed(2)} remaining${expiry}</div>`;
} }
} catch { result.innerHTML = '<div class="alert alert-danger py-1 mb-0 small">Lookup failed.</div>'; } } catch { result.innerHTML = '<div class="alert alert-danger py-1 mb-0 small">Lookup failed.</div>'; }
document.getElementById('gcLookupSpinner').style.display = 'none'; document.getElementById('gcLookupSpinner').style.display = 'none';
@@ -1511,7 +1511,7 @@
<td class="small">${escHtml(n.type.replace(/([A-Z])/g, ' $1').trim())}</td> <td class="small">${escHtml(n.type.replace(/([A-Z])/g, ' $1').trim())}</td>
<td class="small"><i class="bi ${channelIcon} me-1"></i>${escHtml(n.channel)}</td> <td class="small"><i class="bi ${channelIcon} me-1"></i>${escHtml(n.channel)}</td>
<td class="small">${escHtml(n.recipientName)}<br><span class="text-muted">${escHtml(n.recipient)}</span></td> <td class="small">${escHtml(n.recipientName)}<br><span class="text-muted">${escHtml(n.recipient)}</span></td>
<td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted">—</span>'}</td> <td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted"></span>'}</td>
<td><span class="badge bg-${statusClass}">${escHtml(n.status)}</span>${expandBtn}</td> <td><span class="badge bg-${statusClass}">${escHtml(n.status)}</span>${expandBtn}</td>
</tr>${errorRow}`; </tr>${errorRow}`;
}).join(''); }).join('');
+10 -10
View File
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Job.CreateJobDto @model PowderCoating.Application.DTOs.Job.CreateJobDto
@using PowderCoating.Core.Entities @using PowderCoating.Core.Entities
@{ @{
@@ -19,7 +19,7 @@
<i class="bi bi-layout-text-window-reverse fs-5"></i> <i class="bi bi-layout-text-window-reverse fs-5"></i>
<div> <div>
Pre-filled from template <strong>@ViewBag.TemplateName</strong>. Pre-filled from template <strong>@ViewBag.TemplateName</strong>.
Items and coatings have been loaded — review and adjust before saving. Items and coatings have been loaded review and adjust before saving.
</div> </div>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button> <button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
</div> </div>
@@ -50,7 +50,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Details" data-bs-title="Job Details"
data-bs-content="Core job information. Priority and due date are visible on the shop floor board and affect how work is sorted. Customer PO is the customer's own reference number for their purchase order — include it so it appears on invoices. Special Instructions go directly to the shop floor worker."> data-bs-content="Core job information. Priority and due date are visible on the shop floor board and affect how work is sorted. Customer PO is the customer's own reference number for their purchase order include it so it appears on invoices. Special Instructions go directly to the shop floor worker.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -70,7 +70,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Priority" data-bs-title="Job Priority"
data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint — overuse of Rush dilutes its meaning for the shop floor team."> data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint overuse of Rush dilutes its meaning for the shop floor team.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -100,7 +100,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Due Date" data-bs-title="Due Date"
data-bs-content="The customer's deadline — when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list."> data-bs-content="The customer's deadline when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -130,7 +130,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Special Instructions" data-bs-title="Special Instructions"
data-bs-content="Free-text notes visible to the shop floor worker on the work order. Use this for masking requirements, handling notes, customer preferences, or anything that doesn't fit in the item-level notes — e.g., 'Keep brackets separated, customer allergic to zinc primer'."> data-bs-content="Free-text notes visible to the shop floor worker on the work order. Use this for masking requirements, handling notes, customer preferences, or anything that doesn't fit in the item-level notes e.g., 'Keep brackets separated, customer allergic to zinc primer'.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -201,7 +201,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Items" data-bs-title="Job Items"
data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification — color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work."> data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -276,7 +276,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p> <p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p> <p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow"> <p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min): <i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong> <strong id="ovenBatchCostDisplay">$0.00</strong>
</p> </p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow"> <p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -337,7 +337,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label> <div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div> <input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div> </div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small> <small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div> </div>
<div id="cylinderInputs" style="display:none"> <div id="cylinderInputs" style="display:none">
<div class="row g-2"> <div class="row g-2">
@@ -355,7 +355,7 @@
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div> <div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="useSqFtResult()"> <button type="button" class="btn btn-primary" onclick="useSqFtResult()">
<i class="bi bi-check-circle me-1"></i>Use This Value <i class="bi bi-check-circle me-1"></i>Use This Value
</button> </button>
+89 -89
View File
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Job.JobDto @model PowderCoating.Application.DTOs.Job.JobDto
@{ @{
ViewData["Title"] = $"Job {Model.JobNumber}"; ViewData["Title"] = $"Job {Model.JobNumber}";
@@ -57,7 +57,7 @@
} }
else else
{ {
<span>Shop work has started review the quote and apply any changes manually.</span> <span>Shop work has started review the quote and apply any changes manually.</span>
} }
</div> </div>
<div class="d-flex gap-2 flex-wrap"> <div class="d-flex gap-2 flex-wrap">
@@ -217,7 +217,7 @@
</button> </button>
</div> </div>
<div id="scheduledDate-saving" class="d-none mt-1 small text-muted"> <div id="scheduledDate-saving" class="d-none mt-1 small text-muted">
<span class="spinner-border spinner-border-sm me-1"></span>Saving <span class="spinner-border spinner-border-sm me-1"></span>Saving
</div> </div>
</div> </div>
</div> </div>
@@ -263,7 +263,7 @@
<i class="bi bi-x-circle me-1"></i><small>Clear date</small> <i class="bi bi-x-circle me-1"></i><small>Clear date</small>
</button> </button>
<div id="dueDate-saving" class="d-none mt-1 small text-muted"> <div id="dueDate-saving" class="d-none mt-1 small text-muted">
<span class="spinner-border spinner-border-sm me-1"></span>Saving <span class="spinner-border spinner-border-sm me-1"></span>Saving
</div> </div>
</div> </div>
</div> </div>
@@ -273,7 +273,7 @@
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<select id="workerAssignmentSelect" class="form-select form-select-sm" <select id="workerAssignmentSelect" class="form-select form-select-sm"
onchange="updateWorkerAssignment(this)"> onchange="updateWorkerAssignment(this)">
<option value=""> Unassigned </option> <option value=""> Unassigned </option>
@foreach (var w in (IEnumerable<SelectListItem>)ViewBag.Workers) @foreach (var w in (IEnumerable<SelectListItem>)ViewBag.Workers)
{ {
if (w.Value == Model.AssignedUserId) if (w.Value == Model.AssignedUserId)
@@ -287,7 +287,7 @@
} }
</select> </select>
<span id="workerSaveIndicator" class="text-muted small d-none"> <span id="workerSaveIndicator" class="text-muted small d-none">
<span class="spinner-border spinner-border-sm me-1"></span>Saving <span class="spinner-border spinner-border-sm me-1"></span>Saving
</span> </span>
<span id="workerSavedTick" class="text-success small d-none"> <span id="workerSavedTick" class="text-success small d-none">
<i class="bi bi-check-circle-fill"></i> <i class="bi bi-check-circle-fill"></i>
@@ -321,7 +321,7 @@
<div class="card-body"> <div class="card-body">
@* ── Catalog Products ── *@ @* ── Catalog Products ── *@
@if (catalogItems.Any()) @if (catalogItems.Any())
{ {
<h6 class="text-primary mb-3"><i class="bi bi-bag-check me-2"></i>Catalog Products</h6> <h6 class="text-primary mb-3"><i class="bi bi-bag-check me-2"></i>Catalog Products</h6>
@@ -351,10 +351,10 @@
{ {
<br /> <br />
<small class="ms-3"> <small class="ms-3">
<strong>@coat.CoatName</strong> <strong>@coat.CoatName</strong>
@if (!string.IsNullOrEmpty(coat.ColorName)) @if (!string.IsNullOrEmpty(coat.ColorName))
{ {
<text> @coat.ColorName</text> <text> @coat.ColorName</text>
@if (!string.IsNullOrEmpty(coat.VendorName)) @if (!string.IsNullOrEmpty(coat.VendorName))
{ {
<text> (@coat.VendorName)</text> <text> (@coat.VendorName)</text>
@@ -373,7 +373,7 @@
<span class="badge bg-info ms-1" style="font-size:.7em;">@coat.PowderToOrder.Value.ToString("0.##") lbs</span> <span class="badge bg-info ms-1" style="font-size:.7em;">@coat.PowderToOrder.Value.ToString("0.##") lbs</span>
@if (!coat.InventoryItemId.HasValue) @if (!coat.InventoryItemId.HasValue)
{ {
<span class="badge bg-warning text-dark ms-1" style="font-size:.7em;" title="Custom powder must be purchased before coating"><i class="bi bi-cart me-1"></i>ORDER POWDER</span> <span class="badge bg-warning text-dark ms-1" style="font-size:.7em;" title="Custom powder must be purchased before coating"><i class="bi bi-cart me-1"></i>ORDER POWDER</span>
} }
} }
@if (!string.IsNullOrEmpty(coat.Notes)) @if (!string.IsNullOrEmpty(coat.Notes))
@@ -390,7 +390,7 @@
@foreach (var ps in item.PrepServices) @foreach (var ps in item.PrepServices)
{ {
<br /> <br />
<small class="ms-3"> <strong>@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}")</strong> <span class="text-muted"> @ps.EstimatedMinutes min</span></small> <small class="ms-3"> <strong>@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}")</strong> <span class="text-muted"> @ps.EstimatedMinutes min</span></small>
} }
} }
@if (!string.IsNullOrEmpty(item.Notes)) @if (!string.IsNullOrEmpty(item.Notes))
@@ -414,7 +414,7 @@
</div> </div>
} }
@* ── Custom Work ── *@ @* ── Custom Work ── *@
@if (customItems.Any()) @if (customItems.Any())
{ {
<h6 class="text-success mb-3"><i class="bi bi-calculator me-2"></i>Custom Work</h6> <h6 class="text-success mb-3"><i class="bi bi-calculator me-2"></i>Custom Work</h6>
@@ -478,10 +478,10 @@
{ {
<br /> <br />
<small class="ms-3"> <small class="ms-3">
<strong>@coat.CoatName</strong> <strong>@coat.CoatName</strong>
@if (!string.IsNullOrEmpty(coat.ColorName)) @if (!string.IsNullOrEmpty(coat.ColorName))
{ {
<text> @coat.ColorName</text> <text> @coat.ColorName</text>
@if (!string.IsNullOrEmpty(coat.VendorName)) @if (!string.IsNullOrEmpty(coat.VendorName))
{ {
<text> (@coat.VendorName)</text> <text> (@coat.VendorName)</text>
@@ -500,7 +500,7 @@
<span class="badge bg-info ms-1" style="font-size:.7em;">@coat.PowderToOrder.Value.ToString("0.##") lbs</span> <span class="badge bg-info ms-1" style="font-size:.7em;">@coat.PowderToOrder.Value.ToString("0.##") lbs</span>
@if (!coat.InventoryItemId.HasValue) @if (!coat.InventoryItemId.HasValue)
{ {
<span class="badge bg-warning text-dark ms-1" style="font-size:.7em;" title="Custom powder must be purchased before coating"><i class="bi bi-cart me-1"></i>ORDER POWDER</span> <span class="badge bg-warning text-dark ms-1" style="font-size:.7em;" title="Custom powder must be purchased before coating"><i class="bi bi-cart me-1"></i>ORDER POWDER</span>
} }
} }
@if (!string.IsNullOrEmpty(coat.Notes)) @if (!string.IsNullOrEmpty(coat.Notes))
@@ -517,7 +517,7 @@
@foreach (var ps in item.PrepServices) @foreach (var ps in item.PrepServices)
{ {
<br /> <br />
<small class="ms-3"> <strong>@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}")</strong> <span class="text-muted"> @ps.EstimatedMinutes min</span></small> <small class="ms-3"> <strong>@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}")</strong> <span class="text-muted"> @ps.EstimatedMinutes min</span></small>
} }
} }
@if (!string.IsNullOrEmpty(item.Notes)) @if (!string.IsNullOrEmpty(item.Notes))
@@ -532,7 +532,7 @@
<text>@item.SurfaceAreaSqFt.ToString("F2") @ViewBag.AreaUnit</text> <text>@item.SurfaceAreaSqFt.ToString("F2") @ViewBag.AreaUnit</text>
<br /><small class="text-muted">per item</small> <br /><small class="text-muted">per item</small>
} }
else { <span class="text-muted"></span> } else { <span class="text-muted"></span> }
</td> </td>
<td class="text-center"> <td class="text-center">
@if (item.EstimatedMinutes > 0) @if (item.EstimatedMinutes > 0)
@@ -540,7 +540,7 @@
<text>@item.EstimatedMinutes min</text> <text>@item.EstimatedMinutes min</text>
<br /><small class="text-muted">per item</small> <br /><small class="text-muted">per item</small>
} }
else { <span class="text-muted"></span> } else { <span class="text-muted"></span> }
</td> </td>
<td class="text-center"> <td class="text-center">
@if (totalPowderNeeded > 0) @if (totalPowderNeeded > 0)
@@ -548,7 +548,7 @@
<strong class="text-success">@totalPowderNeeded.ToString("F2") lbs</strong> <strong class="text-success">@totalPowderNeeded.ToString("F2") lbs</strong>
<br /><small class="text-muted">total batch</small> <br /><small class="text-muted">total batch</small>
} }
else { <span class="text-muted"></span> } else { <span class="text-muted"></span> }
</td> </td>
<td class="text-end">@item.UnitPrice.ToString("C")</td> <td class="text-end">@item.UnitPrice.ToString("C")</td>
<td class="text-end fw-semibold">@item.TotalPrice.ToString("C")</td> <td class="text-end fw-semibold">@item.TotalPrice.ToString("C")</td>
@@ -565,7 +565,7 @@
</div> </div>
} }
@* ── Labor ── *@ @* ── Labor ── *@
@if (laborItems.Any()) @if (laborItems.Any())
{ {
<h6 class="text-warning mb-3"><i class="bi bi-person-gear me-2"></i>Labor</h6> <h6 class="text-warning mb-3"><i class="bi bi-person-gear me-2"></i>Labor</h6>
@@ -599,7 +599,7 @@
{ {
<text>@item.EstimatedMinutes min</text> <text>@item.EstimatedMinutes min</text>
} }
else { <span class="text-muted"></span> } else { <span class="text-muted"></span> }
</td> </td>
<td class="text-end">@item.UnitPrice.ToString("C")</td> <td class="text-end">@item.UnitPrice.ToString("C")</td>
<td class="text-end fw-semibold">@item.TotalPrice.ToString("C")</td> <td class="text-end fw-semibold">@item.TotalPrice.ToString("C")</td>
@@ -616,7 +616,7 @@
</div> </div>
} }
@* ── Mobile cards ── *@ @* ── Mobile cards ── *@
<div class="d-lg-none mt-2"> <div class="d-lg-none mt-2">
@foreach (var item in Model.Items) @foreach (var item in Model.Items)
{ {
@@ -653,7 +653,7 @@
<span class="mobile-card-value"> <span class="mobile-card-value">
@foreach (var coat in item.Coats.OrderBy(c => c.Sequence)) @foreach (var coat in item.Coats.OrderBy(c => c.Sequence))
{ {
<small class="d-block">@coat.CoatName@(!string.IsNullOrEmpty(coat.ColorName) ? $" {coat.ColorName}" : "")</small> <small class="d-block">@coat.CoatName@(!string.IsNullOrEmpty(coat.ColorName) ? $" {coat.ColorName}" : "")</small>
} }
</span> </span>
</div> </div>
@@ -704,7 +704,7 @@
<i class="bi bi-chevron-down collapse-chevron ms-1" style="transition:transform .2s;"></i> <i class="bi bi-chevron-down collapse-chevron ms-1" style="transition:transform .2s;"></i>
</div> </div>
<div class="d-flex align-items-center gap-3"> <div class="d-flex align-items-center gap-3">
<span class="text-muted small">Total: <strong id="totalHoursDisplay"></strong></span> <span class="text-muted small">Total: <strong id="totalHoursDisplay"></strong></span>
@{ @{
var estimatedMins = Model.Items?.Sum(i => i.EstimatedMinutes * i.Quantity) ?? 0; var estimatedMins = Model.Items?.Sum(i => i.EstimatedMinutes * i.Quantity) ?? 0;
var estimatedHrs = estimatedMins / 60m; var estimatedHrs = estimatedMins / 60m;
@@ -741,7 +741,7 @@
<tfoot class="table-light fw-semibold"> <tfoot class="table-light fw-semibold">
<tr> <tr>
<td colspan="3">Total</td> <td colspan="3">Total</td>
<td class="text-end" id="timeEntriesTotalHours"></td> <td class="text-end" id="timeEntriesTotalHours"></td>
<td colspan="3"></td> <td colspan="3"></td>
</tr> </tr>
</tfoot> </tfoot>
@@ -1099,7 +1099,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="intakeModalLabel"> <h5 class="modal-title" id="intakeModalLabel">
<i class="bi bi-box-seam me-2 text-info"></i>Part Intake Check In <i class="bi bi-box-seam me-2 text-info"></i>Part Intake Check In
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
@@ -1117,7 +1117,7 @@
value="@(Model.IntakePartCount.HasValue ? Model.IntakePartCount.Value.ToString() : "")" value="@(Model.IntakePartCount.HasValue ? Model.IntakePartCount.Value.ToString() : "")"
placeholder="@intakeExpectedCount" /> placeholder="@intakeExpectedCount" />
<div id="intakeMismatchAlert" class="alert alert-warning alert-permanent mt-2 py-2 d-none"> <div id="intakeMismatchAlert" class="alert alert-warning alert-permanent mt-2 py-2 d-none">
<i class="bi bi-exclamation-triangle me-1"></i>Count doesn't match expected note the discrepancy below. <i class="bi bi-exclamation-triangle me-1"></i>Count doesn't match expected note the discrepancy below.
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -1138,7 +1138,7 @@
} }
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-info text-white" id="intakeSaveBtn"> <button type="button" class="btn btn-info text-white" id="intakeSaveBtn">
<i class="bi bi-check-circle me-1"></i>@(Model.IntakeDate.HasValue ? "Update Intake" : "Complete Check-In") <i class="bi bi-check-circle me-1"></i>@(Model.IntakeDate.HasValue ? "Update Intake" : "Complete Check-In")
</button> </button>
@@ -1195,7 +1195,7 @@
<div id="depositFormError" class="alert alert-danger d-none"></div> <div id="depositFormError" class="alert alert-danger d-none"></div>
</div> </div>
<div class="modal-footer"> <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-success" id="saveDepositBtn"> <button type="submit" class="btn btn-success" id="saveDepositBtn">
<i class="bi bi-check-circle me-1"></i>Save & Generate Receipt <i class="bi bi-check-circle me-1"></i>Save & Generate Receipt
</button> </button>
@@ -1310,7 +1310,7 @@
<a asp-action="Intake" asp-route-id="@Model.Id" <a asp-action="Intake" asp-route-id="@Model.Id"
class="btn @(Model.IntakeDate.HasValue ? "btn-outline-secondary" : "btn-outline-info")" class="btn @(Model.IntakeDate.HasValue ? "btn-outline-secondary" : "btn-outline-info")"
title="@(Model.IntakeDate.HasValue ? "Update part intake record" : "Check in parts for this job")"> title="@(Model.IntakeDate.HasValue ? "Update part intake record" : "Check in parts for this job")">
<i class=bi bi-box-seam me-2></i>@(Model.IntakeDate.HasValue ? "Intake " : "Intake") <i class=bi bi-box-seam me-2></i>@(Model.IntakeDate.HasValue ? "Intake ?" : "Intake")
</a> </a>
} }
@{ @{
@@ -1368,7 +1368,7 @@
</div> </div>
</div> </div>
<!-- Pricing Summary (internal d-print-none) --> <!-- Pricing Summary (internal d-print-none) -->
@{ @{
var jobPb = ViewBag.JobPricingBreakdown as PowderCoating.Application.DTOs.Quote.QuotePricingBreakdownDto; var jobPb = ViewBag.JobPricingBreakdown as PowderCoating.Application.DTOs.Quote.QuotePricingBreakdownDto;
} }
@@ -1400,7 +1400,7 @@
@if (jobPb.OvenBatchCost > 0) @if (jobPb.OvenBatchCost > 0)
{ {
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<span><i class="bi bi-fire me-1"></i>Oven (@jobPb.OvenBatches batch@(jobPb.OvenBatches != 1 ? "es" : "")@(jobPb.OvenCycleMinutes > 0 ? $" × {jobPb.OvenCycleMinutes} min" : "")):</span> <span><i class="bi bi-fire me-1"></i>Oven (@jobPb.OvenBatches batch@(jobPb.OvenBatches != 1 ? "es" : "")@(jobPb.OvenCycleMinutes > 0 ? $" {jobPb.OvenCycleMinutes} min" : "")):</span>
<strong>@jobPb.OvenBatchCost.ToString("C")</strong> <strong>@jobPb.OvenBatchCost.ToString("C")</strong>
</div> </div>
} }
@@ -1518,7 +1518,7 @@
} }
else if (allCatalog) else if (allCatalog)
{ {
<div class="text-muted small fst-italic">All items use fixed catalog pricing no per-category cost split available.</div> <div class="text-muted small fst-italic">All items use fixed catalog pricing no per-category cost split available.</div>
} }
else else
{ {
@@ -1547,7 +1547,7 @@
@if (jobPb.FacilityOverheadCost > 0) @if (jobPb.FacilityOverheadCost > 0)
{ {
<div class="d-flex justify-content-between small mb-1"> <div class="d-flex justify-content-between small mb-1">
<span class="text-muted">Facility overhead (@jobPb.FacilityOverheadRatePerHour.ToString("C2")/hr × estimated hours)</span> <span class="text-muted">Facility overhead (@jobPb.FacilityOverheadRatePerHour.ToString("C2")/hr estimated hours)</span>
<span>@jobPb.FacilityOverheadCost.ToString("C")</span> <span>@jobPb.FacilityOverheadCost.ToString("C")</span>
</div> </div>
} }
@@ -1712,11 +1712,11 @@
<div class="px-3 pt-3 pb-2"> <div class="px-3 pt-3 pb-2">
<div class="d-flex justify-content-between align-items-center mb-1"> <div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-muted small">Revenue <span id="costingRevenueSource" class="badge bg-light text-secondary ms-1"></span></span> <span class="text-muted small">Revenue <span id="costingRevenueSource" class="badge bg-light text-secondary ms-1"></span></span>
<span class="fw-semibold" id="costingRevenue"></span> <span class="fw-semibold" id="costingRevenue"></span>
</div> </div>
<div class="d-flex justify-content-between small text-muted mb-1 ps-2"> <div class="d-flex justify-content-between small text-muted mb-1 ps-2">
<span>Powder / Materials <a href="#" class="text-muted ms-1" onclick="costing.toggleDetail('powder');return false;"><i class="bi bi-chevron-down" id="powderChevron"></i></a></span> <span>Powder / Materials <a href="#" class="text-muted ms-1" onclick="costing.toggleDetail('powder');return false;"><i class="bi bi-chevron-down" id="powderChevron"></i></a></span>
<span id="costingPowder"></span> <span id="costingPowder"></span>
</div> </div>
<div id="powderDetail" style="display:none;" class="ps-3 pb-1"> <div id="powderDetail" style="display:none;" class="ps-3 pb-1">
<table class="table table-sm table-borderless mb-0" style="font-size:0.78rem;"> <table class="table table-sm table-borderless mb-0" style="font-size:0.78rem;">
@@ -1725,7 +1725,7 @@
</div> </div>
<div class="d-flex justify-content-between small text-muted mb-1 ps-2"> <div class="d-flex justify-content-between small text-muted mb-1 ps-2">
<span>Labor (<span id="costingLaborHours">0</span> hrs) <a href="#" class="text-muted ms-1" onclick="costing.toggleDetail('labor');return false;"><i class="bi bi-chevron-down" id="laborChevron"></i></a></span> <span>Labor (<span id="costingLaborHours">0</span> hrs) <a href="#" class="text-muted ms-1" onclick="costing.toggleDetail('labor');return false;"><i class="bi bi-chevron-down" id="laborChevron"></i></a></span>
<span id="costingLabor"></span> <span id="costingLabor"></span>
</div> </div>
<div id="laborDetail" style="display:none;" class="ps-3 pb-1"> <div id="laborDetail" style="display:none;" class="ps-3 pb-1">
<table class="table table-sm table-borderless mb-0" style="font-size:0.78rem;"> <table class="table table-sm table-borderless mb-0" style="font-size:0.78rem;">
@@ -1734,12 +1734,12 @@
</div> </div>
<div class="d-flex justify-content-between small text-muted mb-1 ps-2"> <div class="d-flex justify-content-between small text-muted mb-1 ps-2">
<span>Oven / Equipment <span id="costingOvenLabel" class="text-muted"></span></span> <span>Oven / Equipment <span id="costingOvenLabel" class="text-muted"></span></span>
<span id="costingOven"></span> <span id="costingOven"></span>
</div> </div>
<div id="costingReworkSection" style="display:none;"> <div id="costingReworkSection" style="display:none;">
<div class="d-flex justify-content-between small text-muted mb-1 ps-2"> <div class="d-flex justify-content-between small text-muted mb-1 ps-2">
<span>Rework Costs <a href="#" class="text-muted ms-1" onclick="costing.toggleDetail('rework');return false;"><i class="bi bi-chevron-down" id="reworkChevron"></i></a></span> <span>Rework Costs <a href="#" class="text-muted ms-1" onclick="costing.toggleDetail('rework');return false;"><i class="bi bi-chevron-down" id="reworkChevron"></i></a></span>
<span id="costingRework"></span> <span id="costingRework"></span>
</div> </div>
<div id="reworkDetail" style="display:none;" class="ps-3 pb-1"> <div id="reworkDetail" style="display:none;" class="ps-3 pb-1">
<table class="table table-sm table-borderless mb-0" style="font-size:0.78rem;"> <table class="table table-sm table-borderless mb-0" style="font-size:0.78rem;">
@@ -1748,25 +1748,25 @@
</div> </div>
<div class="d-flex justify-content-between small text-success mb-1 ps-2"> <div class="d-flex justify-content-between small text-success mb-1 ps-2">
<span>Billed to Customer</span> <span>Billed to Customer</span>
<span id="costingReworkBilled"></span> <span id="costingReworkBilled"></span>
</div> </div>
</div> </div>
<hr class="my-2" /> <hr class="my-2" />
<div class="d-flex justify-content-between small mb-1 ps-2"> <div class="d-flex justify-content-between small mb-1 ps-2">
<span class="text-muted">Total Costs</span> <span class="text-muted">Total Costs</span>
<span id="costingTotal" class="text-danger"></span> <span id="costingTotal" class="text-danger"></span>
</div> </div>
<div class="d-flex justify-content-between fw-bold mb-1"> <div class="d-flex justify-content-between fw-bold mb-1">
<span>Gross Profit</span> <span>Gross Profit</span>
<span id="costingProfit"></span> <span id="costingProfit"></span>
</div> </div>
<div class="d-flex justify-content-between small text-muted mb-1"> <div class="d-flex justify-content-between small text-muted mb-1">
<span>Gross Margin</span> <span>Gross Margin</span>
<span id="costingMargin"></span> <span id="costingMargin"></span>
</div> </div>
<div class="d-flex justify-content-between small text-muted"> <div class="d-flex justify-content-between small text-muted">
<span>Margin vs Quote</span> <span>Margin vs Quote</span>
<span id="costingQuotedMargin"></span> <span id="costingQuotedMargin"></span>
</div> </div>
</div> </div>
<div id="costingNotes" class="px-3 pb-3" style="font-size:0.75rem;"></div> <div id="costingNotes" class="px-3 pb-3" style="font-size:0.75rem;"></div>
@@ -1869,7 +1869,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Tags <label class="form-label">Tags
<small class="text-muted fw-normal ms-1"> colors, finish, or other keywords</small> <small class="text-muted fw-normal ms-1"> colors, finish, or other keywords</small>
</label> </label>
<input type="hidden" id="photoTagsHidden" name="tags" /> <input type="hidden" id="photoTagsHidden" name="tags" />
<div id="photoTagsContainer"></div> <div id="photoTagsContainer"></div>
@@ -1895,7 +1895,7 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="jobPhotoModule.uploadPhoto()"> <button type="button" class="btn btn-primary" onclick="jobPhotoModule.uploadPhoto()">
<i class="bi bi-upload me-1"></i>Upload <i class="bi bi-upload me-1"></i>Upload
</button> </button>
@@ -1948,7 +1948,7 @@
<textarea class="form-control" id="editPhotoCaption" rows="2" placeholder="Add a description or note..."></textarea> <textarea class="form-control" id="editPhotoCaption" rows="2" placeholder="Add a description or note..."></textarea>
</div> </div>
<div class="mb-0"> <div class="mb-0">
<label class="form-label fw-semibold">Tags <small class="text-muted fw-normal ms-1"> colors, finish, keywords</small></label> <label class="form-label fw-semibold">Tags <small class="text-muted fw-normal ms-1"> colors, finish, keywords</small></label>
<input type="hidden" id="editPhotoTagsHidden" /> <input type="hidden" id="editPhotoTagsHidden" />
<div id="editPhotoTagsContainer"></div> <div id="editPhotoTagsContainer"></div>
</div> </div>
@@ -1962,10 +1962,10 @@
<button type="button" class="btn btn-danger" onclick="jobPhotoModule.deletePhoto()"> <button type="button" class="btn btn-danger" onclick="jobPhotoModule.deletePhoto()">
<i class="bi bi-trash me-1"></i>Delete <i class="bi bi-trash me-1"></i>Delete
</button> </button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
<div id="editModeButtons" class="d-none d-flex gap-2 w-100 justify-content-end"> <div id="editModeButtons" class="d-none d-flex gap-2 w-100 justify-content-end">
<button type="button" class="btn btn-secondary" onclick="jobPhotoModule.cancelPhotoEdit()">Cancel</button> <button type="button" class="btn btn-outline-secondary" onclick="jobPhotoModule.cancelPhotoEdit()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="jobPhotoModule.savePhotoEdit()"> <button type="button" class="btn btn-primary" onclick="jobPhotoModule.savePhotoEdit()">
<i class="bi bi-check-lg me-1"></i>Save Changes <i class="bi bi-check-lg me-1"></i>Save Changes
</button> </button>
@@ -2000,7 +2000,7 @@
<div class="mb-2"> <div class="mb-2">
<label class="form-label fw-semibold" for="smsMessageText">Message</label> <label class="form-label fw-semibold" for="smsMessageText">Message</label>
<textarea class="form-control" id="smsMessageText" rows="5" <textarea class="form-control" id="smsMessageText" rows="5"
placeholder="Type your message" maxlength="160"></textarea> placeholder="Type your message" maxlength="160"></textarea>
<div class="d-flex justify-content-between mt-1"> <div class="d-flex justify-content-between mt-1">
<div id="smsStopWarning" class="text-warning small d-none"> <div id="smsStopWarning" class="text-warning small d-none">
<i class="bi bi-exclamation-triangle me-1"></i>"Reply STOP to opt out." will be appended automatically. <i class="bi bi-exclamation-triangle me-1"></i>"Reply STOP to opt out." will be appended automatically.
@@ -2012,7 +2012,7 @@
</div> </div>
<div class="modal-footer justify-content-between"> <div class="modal-footer justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" id="smsDismissBtn"> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" id="smsDismissBtn">
Skip don't send Skip don't send
</button> </button>
<button type="button" class="btn btn-info text-white" id="smsSendBtn"> <button type="button" class="btn btn-info text-white" id="smsSendBtn">
<i class="bi bi-send me-1"></i>Send SMS <i class="bi bi-send me-1"></i>Send SMS
@@ -2059,7 +2059,7 @@
<p class="mb-0 small" id="deleteConfirmItemName"></p> <p class="mb-0 small" id="deleteConfirmItemName"></p>
</div> </div>
<div class="modal-footer gap-2"> <div class="modal-footer gap-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-sm" id="deleteConfirmBtn"> <button type="button" class="btn btn-danger btn-sm" id="deleteConfirmBtn">
<i class="bi bi-trash me-1"></i>Delete <i class="bi bi-trash me-1"></i>Delete
</button> </button>
@@ -2110,7 +2110,7 @@
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div> <div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="useSqFtResult()"> <button type="button" class="btn btn-primary" onclick="useSqFtResult()">
<i class="bi bi-check-circle me-1"></i>Use This Value <i class="bi bi-check-circle me-1"></i>Use This Value
</button> </button>
@@ -2223,7 +2223,7 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Specific Item (optional)</label> <label class="form-label">Specific Item (optional)</label>
<select class="form-select" id="rwJobItem"> <select class="form-select" id="rwJobItem">
<option value=""> Whole Job </option> <option value=""> Whole Job </option>
@if (Model.Items != null) @if (Model.Items != null)
{ {
@foreach (var item in Model.Items) @foreach (var item in Model.Items)
@@ -2285,9 +2285,9 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Resolution</label> <label class="form-label">Resolution</label>
<select class="form-select" id="rwResolution"> <select class="form-select" id="rwResolution">
<option value=""> Pending </option> <option value=""> Pending </option>
<option value="0">Recoated No Charge</option> <option value="0">Recoated No Charge</option>
<option value="1">Recoated Billed to Customer</option> <option value="1">Recoated Billed to Customer</option>
<option value="2">Customer Credited</option> <option value="2">Customer Credited</option>
<option value="3">Written Off</option> <option value="3">Written Off</option>
<option value="4">No Action Required</option> <option value="4">No Action Required</option>
@@ -2324,7 +2324,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-warning" id="reworkSaveBtn" onclick="rework.save()"> <button type="button" class="btn btn-warning" id="reworkSaveBtn" onclick="rework.save()">
<i class="bi bi-floppy me-1"></i>Save <i class="bi bi-floppy me-1"></i>Save
</button> </button>
@@ -2346,7 +2346,7 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Worker <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Worker <span class="text-danger">*</span></label>
<select class="form-select" id="teWorkerId"> <select class="form-select" id="teWorkerId">
<option value=""> Select worker </option> <option value=""> Select worker </option>
@foreach (var w in (ViewBag.ShopWorkers as IEnumerable<dynamic> ?? [])) @foreach (var w in (ViewBag.ShopWorkers as IEnumerable<dynamic> ?? []))
{ {
<option value="@w.Id">@w.Name</option> <option value="@w.Id">@w.Name</option>
@@ -2365,7 +2365,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Stage / Task</label> <label class="form-label fw-semibold">Stage / Task</label>
<input type="text" class="form-control" id="teStage" placeholder="e.g. Sandblasting, Coating, Masking" list="stageOptions" /> <input type="text" class="form-control" id="teStage" placeholder="e.g. Sandblasting, Coating, Masking" list="stageOptions" />
<datalist id="stageOptions"> <datalist id="stageOptions">
<option value="Sandblasting"></option> <option value="Sandblasting"></option>
<option value="Masking & Taping"></option> <option value="Masking & Taping"></option>
@@ -2380,7 +2380,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Notes</label> <label class="form-label">Notes</label>
<textarea class="form-control" id="teNotes" rows="2" placeholder="Optional notes"></textarea> <textarea class="form-control" id="teNotes" rows="2" placeholder="Optional notes"></textarea>
</div> </div>
<div class="text-danger small d-none" id="teError"></div> <div class="text-danger small d-none" id="teError"></div>
</div> </div>
@@ -2418,7 +2418,7 @@
<script src="~/js/job-photos.js" asp-append-version="true"></script> <script src="~/js/job-photos.js" asp-append-version="true"></script>
<script src="~/js/customer-change.js" asp-append-version="true"></script> <script src="~/js/customer-change.js" asp-append-version="true"></script>
<script> <script>
// ── Inline date editing ────────────────────────────────────────────── // ── Inline date editing ──────────────────────────────────────────────
const jobId = @Model.Id; const jobId = @Model.Id;
const antiForgeryToken = () => document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? ''; const antiForgeryToken = () => document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
@@ -2544,7 +2544,7 @@
jobPhotoModule.init(@Model.Id, @Html.Raw(ViewBag.PhotoTagSuggestions ?? "[]")); jobPhotoModule.init(@Model.Id, @Html.Raw(ViewBag.PhotoTagSuggestions ?? "[]"));
// ── Auto-submit after wizard saves an item ──────────────────────── // ── Auto-submit after wizard saves an item ────────────────────────
let itemsModified = false; let itemsModified = false;
// Wrap wizardSave to set a flag before the modal hides // Wrap wizardSave to set a flag before the modal hides
@@ -2562,12 +2562,12 @@
} }
}); });
// ── Delete confirmation modal ───────────────────────────────────── // ── Delete confirmation modal ─────────────────────────────────────
let pendingDeleteItemId = -1; let pendingDeleteItemId = -1;
const deleteModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal')); const deleteModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
const deleteItemToken = document.querySelector('input[name="__RequestVerificationToken"]').value; const deleteItemToken = document.querySelector('input[name="__RequestVerificationToken"]').value;
// Delegated listener handles all delete buttons via data attributes // Delegated listener handles all delete buttons via data attributes
document.addEventListener('click', function (e) { document.addEventListener('click', function (e) {
const btn = e.target.closest('[data-delete-id]'); const btn = e.target.closest('[data-delete-id]');
if (!btn) return; if (!btn) return;
@@ -2600,7 +2600,7 @@
}); });
</script> </script>
<!-- ── Rework / Warranty ────────────────────────────────────────────── --> <!-- ── Rework / Warranty ────────────────────────────────────────────── -->
<script> <script>
const rework = (() => { const rework = (() => {
const jid = @Model.Id; const jid = @Model.Id;
@@ -2645,12 +2645,12 @@
</div> </div>
<div class="small mt-1 text-muted">${r.defectDescription}</div> <div class="small mt-1 text-muted">${r.defectDescription}</div>
<div class="small text-muted mt-1"> <div class="small text-muted mt-1">
Found: ${r.discoveredByDisplay} ${new Date(r.discoveredDate).toLocaleDateString()} Found: ${r.discoveredByDisplay} ${new Date(r.discoveredDate).toLocaleDateString()}
${r.reportedByName ? ' ' + r.reportedByName : ''} ${r.reportedByName ? ' ' + r.reportedByName : ''}
${r.jobItemDescription ? ' | Item: ' + r.jobItemDescription : ''} ${r.jobItemDescription ? ' | Item: ' + r.jobItemDescription : ''}
</div> </div>
${r.reworkJobNumber ? `<div class="small mt-1"><i class="bi bi-briefcase me-1"></i>Rework Job: <a href="/Jobs/Details/${r.reworkJobId}" class="text-decoration-none fw-semibold">${r.reworkJobNumber}</a></div>` : ''} ${r.reworkJobNumber ? `<div class="small mt-1"><i class="bi bi-briefcase me-1"></i>Rework Job: <a href="/Jobs/Details/${r.reworkJobId}" class="text-decoration-none fw-semibold">${r.reworkJobNumber}</a></div>` : ''}
${r.resolutionDisplay ? `<div class="small text-success mt-1"><i class="bi bi-check-circle me-1"></i>${r.resolutionDisplay}${r.actualReworkCost > 0 ? ' $' + r.actualReworkCost.toFixed(2) : ''}</div>` : ''} ${r.resolutionDisplay ? `<div class="small text-success mt-1"><i class="bi bi-check-circle me-1"></i>${r.resolutionDisplay}${r.actualReworkCost > 0 ? ' $' + r.actualReworkCost.toFixed(2) : ''}</div>` : ''}
</div>`).join(''); </div>`).join('');
} }
@@ -2756,7 +2756,7 @@
})(); })();
</script> </script>
<!-- ── Job Costing ──────────────────────────────────────────────────── --> <!-- ── Job Costing ──────────────────────────────────────────────────── -->
<script> <script>
const costing = (() => { const costing = (() => {
const jid = @Model.Id; const jid = @Model.Id;
@@ -2796,7 +2796,7 @@
document.getElementById('costingReworkBilled').textContent = fmt(d.reworkBilledToCustomer); document.getElementById('costingReworkBilled').textContent = fmt(d.reworkBilledToCustomer);
const rBody = document.getElementById('reworkCostLines'); const rBody = document.getElementById('reworkCostLines');
rBody.innerHTML = d.reworkLines.map(l => `<tr> rBody.innerHTML = d.reworkLines.map(l => `<tr>
<td class="text-muted">${l.jobNumber ? `<a href="/Jobs/Details" class="text-decoration-none">${l.jobNumber}</a>` : 'No job'} ${l.reason}${l.isEstimate ? ' <span class="badge bg-secondary" style="font-size:0.65rem;">est.</span>' : ''}</td> <td class="text-muted">${l.jobNumber ? `<a href="/Jobs/Details" class="text-decoration-none">${l.jobNumber}</a>` : 'No job'} ${l.reason}${l.isEstimate ? ' <span class="badge bg-secondary" style="font-size:0.65rem;">est.</span>' : ''}</td>
<td class="text-end text-nowrap">${l.billedToCustomer > 0 ? `<span class="text-success">${fmt(l.billedToCustomer)} billed</span>` : 'absorbed'}</td> <td class="text-end text-nowrap">${l.billedToCustomer > 0 ? `<span class="text-success">${fmt(l.billedToCustomer)} billed</span>` : 'absorbed'}</td>
<td class="text-end text-nowrap fw-semibold">${fmt(l.cost)}</td></tr>`).join(''); <td class="text-end text-nowrap fw-semibold">${fmt(l.cost)}</td></tr>`).join('');
} else { } else {
@@ -2812,14 +2812,14 @@
document.getElementById('costingMargin').textContent = `${d.grossMargin}%`; document.getElementById('costingMargin').textContent = `${d.grossMargin}%`;
document.getElementById('costingQuotedMargin').textContent = document.getElementById('costingQuotedMargin').textContent =
d.quotedPrice > 0 ? `${d.quotedMargin}% (quoted ${fmt(d.quotedPrice)})` : ''; d.quotedPrice > 0 ? `${d.quotedMargin}% (quoted ${fmt(d.quotedPrice)})` : '';
// Powder detail lines // Powder detail lines
const pBody = document.getElementById('powderLines'); const pBody = document.getElementById('powderLines');
pBody.innerHTML = d.hasPowderData pBody.innerHTML = d.hasPowderData
? d.powderLines.map(l => `<tr> ? d.powderLines.map(l => `<tr>
<td class="text-muted" style="max-width:160px;white-space:normal;">${l.description}${l.isActual ? ' <span class="badge bg-success" style="font-size:0.65rem;">actual</span>' : ''}</td> <td class="text-muted" style="max-width:160px;white-space:normal;">${l.description}${l.isActual ? ' <span class="badge bg-success" style="font-size:0.65rem;">actual</span>' : ''}</td>
<td class="text-end text-nowrap">${l.lbs} lbs × ${fmt(l.costPerLb)}/lb</td> <td class="text-end text-nowrap">${l.lbs} lbs ${fmt(l.costPerLb)}/lb</td>
<td class="text-end text-nowrap fw-semibold">${fmt(l.total)}</td></tr>`).join('') <td class="text-end text-nowrap fw-semibold">${fmt(l.total)}</td></tr>`).join('')
: '<tr><td colspan="3" class="text-muted">No powder cost data on coats.</td></tr>'; : '<tr><td colspan="3" class="text-muted">No powder cost data on coats.</td></tr>';
@@ -2827,16 +2827,16 @@
const lBody = document.getElementById('laborLines'); const lBody = document.getElementById('laborLines');
lBody.innerHTML = d.hasLaborData lBody.innerHTML = d.hasLaborData
? d.laborLines.map(l => `<tr> ? d.laborLines.map(l => `<tr>
<td class="text-muted">${l.worker}${l.stage ? ' ' + l.stage : ''}<br/><small>${l.workDate}</small></td> <td class="text-muted">${l.worker}${l.stage ? ' ' + l.stage : ''}<br/><small>${l.workDate}</small></td>
<td class="text-end text-nowrap">${l.hours}h × ${fmt(l.rate)}/hr${l.usingFallback ? ' <span title="Using standard labor rate" class="text-muted">*</span>' : ''}</td> <td class="text-end text-nowrap">${l.hours}h ${fmt(l.rate)}/hr${l.usingFallback ? ' <span title="Using standard labor rate" class="text-muted">*</span>' : ''}</td>
<td class="text-end text-nowrap fw-semibold">${fmt(l.total)}</td></tr>`).join('') <td class="text-end text-nowrap fw-semibold">${fmt(l.total)}</td></tr>`).join('')
: '<tr><td colspan="3" class="text-muted">No time entries logged yet.</td></tr>'; : '<tr><td colspan="3" class="text-muted">No time entries logged yet.</td></tr>';
// Notes // Notes
const notes = []; const notes = [];
if (!d.hasPowderData && d.hasPowderRateButNoQty) notes.push(' Surface area not set on one or more items edit the item and enter a surface area to calculate powder cost.'); if (!d.hasPowderData && d.hasPowderRateButNoQty) notes.push('? Surface area not set on one or more items edit the item and enter a surface area to calculate powder cost.');
else if (!d.hasPowderData) notes.push(' Add powder cost per lb on coat records to include material cost.'); else if (!d.hasPowderData) notes.push('? Add powder cost per lb on coat records to include material cost.');
if (!d.hasLaborData) notes.push(' Log time entries to include labor cost.'); if (!d.hasLaborData) notes.push('? Log time entries to include labor cost.');
if (d.laborLines?.some(l => l.usingFallback)) notes.push('* One or more workers using standard labor rate fallback.'); if (d.laborLines?.some(l => l.usingFallback)) notes.push('* One or more workers using standard labor rate fallback.');
document.getElementById('costingNotes').innerHTML = notes.map(n => `<div class="text-muted">${n}</div>`).join(''); document.getElementById('costingNotes').innerHTML = notes.map(n => `<div class="text-muted">${n}</div>`).join('');
@@ -2865,7 +2865,7 @@
})(); })();
</script> </script>
<!-- ── Time Tracking ─────────────────────────────────────────────────── --> <!-- ── Time Tracking ─────────────────────────────────────────────────── -->
<script> <script>
const timeTracking = (() => { const timeTracking = (() => {
const jid = @Model.Id; const jid = @Model.Id;
@@ -2873,7 +2873,7 @@
const modal = new bootstrap.Modal(document.getElementById('timeEntryModal')); const modal = new bootstrap.Modal(document.getElementById('timeEntryModal'));
let entries = []; let entries = [];
// ── Load ────────────────────────────────────────────────────────── // ── Load ──────────────────────────────────────────────────────────
async function load() { async function load() {
const r = await fetch(`/Jobs/GetTimeEntries?jobId=${jid}`); const r = await fetch(`/Jobs/GetTimeEntries?jobId=${jid}`);
entries = await r.json(); entries = await r.json();
@@ -2904,7 +2904,7 @@
<td class="fw-semibold">${esc(e.workerName)}</td> <td class="fw-semibold">${esc(e.workerName)}</td>
<td class="small">${d}</td> <td class="small">${d}</td>
<td class="text-end fw-semibold">${e.hoursWorked.toFixed(2)}</td> <td class="text-end fw-semibold">${e.hoursWorked.toFixed(2)}</td>
<td class="small">${e.stage ? `<span class="badge bg-secondary-subtle text-secondary">${esc(e.stage)}</span>` : '<span class="text-muted"></span>'}</td> <td class="small">${e.stage ? `<span class="badge bg-secondary-subtle text-secondary">${esc(e.stage)}</span>` : '<span class="text-muted"></span>'}</td>
<td class="small text-muted">${esc(e.notes ?? '')}</td> <td class="small text-muted">${esc(e.notes ?? '')}</td>
<td class="text-end"> <td class="text-end">
<button class="btn btn-xs btn-outline-secondary me-1 py-0 px-1" title="Edit" onclick="timeTracking.openEdit(${e.id})"><i class="bi bi-pencil"></i></button> <button class="btn btn-xs btn-outline-secondary me-1 py-0 px-1" title="Edit" onclick="timeTracking.openEdit(${e.id})"><i class="bi bi-pencil"></i></button>
@@ -2916,12 +2916,12 @@
} }
function updateTotals(total) { function updateTotals(total) {
const fmt = total > 0 ? total.toFixed(2) + ' hrs' : ''; const fmt = total > 0 ? total.toFixed(2) + ' hrs' : '';
document.getElementById('totalHoursDisplay').textContent = fmt; document.getElementById('totalHoursDisplay').textContent = fmt;
document.getElementById('timeEntriesTotalHours').textContent = total > 0 ? total.toFixed(2) : ''; document.getElementById('timeEntriesTotalHours').textContent = total > 0 ? total.toFixed(2) : '';
} }
// ── Modal helpers ───────────────────────────────────────────────── // ── Modal helpers ─────────────────────────────────────────────────
function openAdd() { function openAdd() {
document.getElementById('timeEntryModalTitle').textContent = 'Log Time'; document.getElementById('timeEntryModalTitle').textContent = 'Log Time';
document.getElementById('teEntryId').value = '0'; document.getElementById('teEntryId').value = '0';
@@ -3027,7 +3027,7 @@
} }
}); });
// ── Deposits ───────────────────────────────────────────────────────────── // ── Deposits ─────────────────────────────────────────────────────────────
// Note: antiForgeryToken() is already defined above in this script block // Note: antiForgeryToken() is already defined above in this script block
document.getElementById('addDepositForm')?.addEventListener('submit', async function(e) { document.getElementById('addDepositForm')?.addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
@@ -3041,7 +3041,7 @@
} }
if (errEl) errEl.classList.add('d-none'); if (errEl) errEl.classList.add('d-none');
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving'; } if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving'; }
const params = new URLSearchParams(new FormData(form)); const params = new URLSearchParams(new FormData(form));
@@ -3083,7 +3083,7 @@
} }
} }
// ── Collapsible sections ────────────────────────────────────────────────── // ── Collapsible sections ──────────────────────────────────────────────────
(function () { (function () {
const storageKey = 'jobDetailCollapse_@Model.Id'; const storageKey = 'jobDetailCollapse_@Model.Id';
const sections = ['collapseTimeTracking', 'collapsePartIntake', 'collapsePhotos', 'collapseDeposits', 'collapseMaterials']; const sections = ['collapseTimeTracking', 'collapsePartIntake', 'collapsePhotos', 'collapseDeposits', 'collapseMaterials'];
@@ -3122,7 +3122,7 @@
}); });
})(); })();
// ── Part Intake Modal ───────────────────────────────────────────────────── // ── Part Intake Modal ─────────────────────────────────────────────────────
(function () { (function () {
const expectedCount = @intakeExpectedCount; const expectedCount = @intakeExpectedCount;
const partCountInput = document.getElementById('intakePartCount'); const partCountInput = document.getElementById('intakePartCount');
@@ -3215,7 +3215,7 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Template Name <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Template Name <span class="text-danger">*</span></label>
<input type="text" name="templateName" class="form-control" required maxlength="100" <input type="text" name="templateName" class="form-control" required maxlength="100"
placeholder="e.g. Wheel Refinish Standard 4pc"> placeholder="e.g. Wheel Refinish Standard 4pc">
</div> </div>
<div class="mb-3"> <div class="mb-3">
+9 -9
View File
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Job.UpdateJobDto @model PowderCoating.Application.DTOs.Job.UpdateJobDto
@using PowderCoating.Core.Entities @using PowderCoating.Core.Entities
@{ @{
@@ -26,7 +26,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Details" data-bs-title="Job Details"
data-bs-content="Core job information. Priority and due date are visible on the shop floor board and job list. Customer PO is the customer's own reference number — it appears on invoices. Special Instructions go directly to the shop floor worker on the work order."> data-bs-content="Core job information. Priority and due date are visible on the shop floor board and job list. Customer PO is the customer's own reference number it appears on invoices. Special Instructions go directly to the shop floor worker on the work order.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -45,7 +45,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Status" data-bs-title="Job Status"
data-bs-content="Tracks where the job is in the workflow: Pending → Approved → Sandblasting → Cleaning → Coating → Curing → QualityCheck → Completed → ReadyForPickup → Delivered. Status changes trigger customer email notifications (if enabled). Use OnHold to pause work without losing progress."> data-bs-content="Tracks where the job is in the workflow: Pending Approved Sandblasting Cleaning Coating Curing QualityCheck Completed ReadyForPickup Delivered. Status changes trigger customer email notifications (if enabled). Use OnHold to pause work without losing progress.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</label> </label>
@@ -57,7 +57,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Priority" data-bs-title="Job Priority"
data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint — overuse of Rush dilutes its meaning for the shop floor team."> data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint overuse of Rush dilutes its meaning for the shop floor team.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</label> </label>
@@ -85,7 +85,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Due Date" data-bs-title="Due Date"
data-bs-content="The customer's deadline — when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list."> data-bs-content="The customer's deadline when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</label> </label>
@@ -170,7 +170,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Items" data-bs-title="Job Items"
data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification — color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work."> data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</div> </div>
@@ -245,7 +245,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p> <p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p> <p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow"> <p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min): <i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong> <strong id="ovenBatchCostDisplay">$0.00</strong>
</p> </p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow"> <p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -322,7 +322,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label> <div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div> <input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div> </div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small> <small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div> </div>
<div id="cylinderInputs" style="display:none"> <div id="cylinderInputs" style="display:none">
<div class="row g-2"> <div class="row g-2">
@@ -340,7 +340,7 @@
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div> <div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="useSqFtResult()"> <button type="button" class="btn btn-primary" onclick="useSqFtResult()">
<i class="bi bi-check-circle me-1"></i>Use This Value <i class="bi bi-check-circle me-1"></i>Use This Value
</button> </button>
@@ -1,8 +1,8 @@
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel @model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
@using PowderCoating.Core.Entities @using PowderCoating.Core.Entities
@{ @{
ViewData["Title"] = $"Edit Items — {Model.JobNumber}"; ViewData["Title"] = $"Edit Items {Model.JobNumber}";
ViewData["PageIcon"] = "bi-list-check"; ViewData["PageIcon"] = "bi-list-check";
} }
@@ -64,7 +64,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p> <p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p> <p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow"> <p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min): <i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong> <strong id="ovenBatchCostDisplay">$0.00</strong>
</p> </p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow"> <p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -118,7 +118,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label> <div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div> <input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div> </div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small> <small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div> </div>
<div id="cylinderInputs" style="display:none"> <div id="cylinderInputs" style="display:none">
<div class="row g-2"> <div class="row g-2">
@@ -136,7 +136,7 @@
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div> <div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="useSqFtResult()"> <button type="button" class="btn btn-primary" onclick="useSqFtResult()">
<i class="bi bi-check-circle me-1"></i>Use This Value <i class="bi bi-check-circle me-1"></i>Use This Value
</button> </button>
@@ -455,7 +455,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveWorkerAssignment"> <button type="button" class="btn btn-primary" id="saveWorkerAssignment">
<i class="bi bi-save me-2"></i>Save Assignment <i class="bi bi-save me-2"></i>Save Assignment
</button> </button>
@@ -491,7 +491,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="savePriority"> <button type="button" class="btn btn-primary" id="savePriority">
<i class="bi bi-save me-2"></i>Save Priority <i class="bi bi-save me-2"></i>Save Priority
</button> </button>
@@ -540,7 +540,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveStatus"> <button type="button" class="btn btn-primary" id="saveStatus">
<i class="bi bi-save me-2"></i>Save Status <i class="bi bi-save me-2"></i>Save Status
</button> </button>
@@ -527,7 +527,7 @@
<p class="text-muted small mb-0">This will update the job status immediately.</p> <p class="text-muted small mb-0">This will update the job status immediately.</p>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="btn-confirm-advance"> <button type="button" class="btn btn-primary" id="btn-confirm-advance">
<i class="bi bi-check-lg me-2"></i>Yes, Advance <i class="bi bi-check-lg me-2"></i>Yes, Advance
</button> </button>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Job.JobDto @model PowderCoating.Application.DTOs.Job.JobDto
@{ @{
var emailDefault = ViewBag.EmailDefaultOnComplete == true; var emailDefault = ViewBag.EmailDefaultOnComplete == true;
var preLoggedPowder = ViewBag.PreLoggedPowder as Dictionary<int, decimal> ?? new Dictionary<int, decimal>(); var preLoggedPowder = ViewBag.PreLoggedPowder as Dictionary<int, decimal> ?? new Dictionary<int, decimal>();
@@ -97,7 +97,7 @@
@if (preFilledLbs > 0) @if (preFilledLbs > 0)
{ {
<small class="text-success d-block mt-1"> <small class="text-success d-block mt-1">
<i class="bi bi-check-circle me-1"></i>Already logged — inventory adjusted <i class="bi bi-check-circle me-1"></i>Already logged inventory adjusted
</small> </small>
} }
</td> </td>
@@ -111,7 +111,7 @@
<td colspan="5"> <td colspan="5">
<small class="text-muted fst-italic"> <small class="text-muted fst-italic">
<i class="bi bi-info-circle me-1"></i> <i class="bi bi-info-circle me-1"></i>
@item.Description — No coat information available (legacy job item) @item.Description No coat information available (legacy job item)
</small> </small>
</td> </td>
</tr> </tr>
@@ -122,7 +122,7 @@
</div> </div>
<div class="alert alert-info alert-permanent mb-0"> <div class="alert alert-info alert-permanent mb-0">
<i class="bi bi-info-circle me-2"></i> <i class="bi bi-info-circle me-2"></i>
<small>Pre-filled values were already logged via scan — inventory is already adjusted for those. You can edit the amount; only the difference will be applied to inventory.</small> <small>Pre-filled values were already logged via scan inventory is already adjusted for those. You can edit the amount; only the difference will be applied to inventory.</small>
</div> </div>
</div> </div>
} }
@@ -152,7 +152,7 @@
} }
</div> </div>
<div> <div>
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-outline-secondary me-2" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
<i class="bi bi-check-circle me-1"></i>Mark as Completed <i class="bi bi-check-circle me-1"></i>Mark as Completed
</button> </button>
@@ -98,14 +98,14 @@
</div> </div>
</div> </div>
<div class="text-end"> <div class="text-end">
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-secondary">Cancel</a> <a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary">Cancel</a>
</div> </div>
} }
else else
{ {
<!-- Simple single delete --> <!-- Simple single delete -->
<div class="d-flex gap-2 justify-content-end"> <div class="d-flex gap-2 justify-content-end">
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-secondary">Cancel</a> <a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary">Cancel</a>
<form asp-action="Delete" method="post"> <form asp-action="Delete" method="post">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<input type="hidden" name="id" value="@Model.Id" /> <input type="hidden" name="id" value="@Model.Id" />
@@ -1,4 +1,4 @@
@using PowderCoating.Core.Entities @using PowderCoating.Core.Entities
@model ManufacturerLookupPattern @model ManufacturerLookupPattern
@{ @{
ViewData["Title"] = "Add Manufacturer Pattern"; ViewData["Title"] = "Add Manufacturer Pattern";
@@ -6,7 +6,7 @@
<div class="container-fluid py-3"> <div class="container-fluid py-3">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm me-3"> <a asp-action="Index" class="btn btn-outline-secondary me-3">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-left"></i>
</a> </a>
<h4 class="mb-0"><i class="bi bi-link-45deg me-2 text-primary"></i>Add Manufacturer Pattern</h4> <h4 class="mb-0"><i class="bi bi-link-45deg me-2 text-primary"></i>Add Manufacturer Pattern</h4>
@@ -39,9 +39,9 @@
<input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" /> <input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" />
<div class="form-text"> <div class="form-text">
Supported placeholders: Supported placeholders:
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens), <code>{partNumber}</code> manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> — color name transformed by Slug Transform below, <code>{slug}</code> color name transformed by Slug Transform below,
<code>{colorCode}</code> — color code as-is. <code>{colorCode}</code> color code as-is.
If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL. If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL.
</div> </div>
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span> <span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
@@ -50,10 +50,10 @@
<div class="mb-3"> <div class="mb-3">
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label> <label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
<select asp-for="SlugTransform" class="form-select"> <select asp-for="SlugTransform" class="form-select">
<option value="LowerHyphen">LowerHyphen — e.g. "jet-black"</option> <option value="LowerHyphen">LowerHyphen e.g. "jet-black"</option>
<option value="LowerUnderscore">LowerUnderscore — e.g. "jet_black"</option> <option value="LowerUnderscore">LowerUnderscore e.g. "jet_black"</option>
<option value="TitleHyphen">TitleHyphen — e.g. "Jet-Black"</option> <option value="TitleHyphen">TitleHyphen e.g. "Jet-Black"</option>
<option value="AsIs">AsIs — color name unchanged</option> <option value="AsIs">AsIs color name unchanged</option>
</select> </select>
<div class="form-text">How the color name is converted to a URL slug when building the product URL.</div> <div class="form-text">How the color name is converted to a URL slug when building the product URL.</div>
<span asp-validation-for="SlugTransform" class="text-danger small"></span> <span asp-validation-for="SlugTransform" class="text-danger small"></span>
@@ -6,7 +6,7 @@
<div class="container-fluid py-3"> <div class="container-fluid py-3">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm me-3"> <a asp-action="Index" class="btn btn-outline-secondary me-3">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-left"></i>
</a> </a>
<h4 class="mb-0"><i class="bi bi-trash3 me-2 text-danger"></i>Delete Pattern</h4> <h4 class="mb-0"><i class="bi bi-trash3 me-2 text-danger"></i>Delete Pattern</h4>
@@ -1,4 +1,4 @@
@using PowderCoating.Core.Entities @using PowderCoating.Core.Entities
@model ManufacturerLookupPattern @model ManufacturerLookupPattern
@{ @{
ViewData["Title"] = "Edit Pattern"; ViewData["Title"] = "Edit Pattern";
@@ -6,7 +6,7 @@
<div class="container-fluid py-3"> <div class="container-fluid py-3">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm me-3"> <a asp-action="Index" class="btn btn-outline-secondary me-3">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-left"></i>
</a> </a>
<h4 class="mb-0"><i class="bi bi-pencil me-2 text-primary"></i>Edit Pattern</h4> <h4 class="mb-0"><i class="bi bi-pencil me-2 text-primary"></i>Edit Pattern</h4>
@@ -40,9 +40,9 @@
<input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" /> <input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" />
<div class="form-text"> <div class="form-text">
Supported placeholders: Supported placeholders:
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens), <code>{partNumber}</code> manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> — color name transformed by Slug Transform below, <code>{slug}</code> color name transformed by Slug Transform below,
<code>{colorCode}</code> — color code as-is. <code>{colorCode}</code> color code as-is.
If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL. If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL.
</div> </div>
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span> <span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
@@ -51,10 +51,10 @@
<div class="mb-3"> <div class="mb-3">
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label> <label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
<select asp-for="SlugTransform" class="form-select"> <select asp-for="SlugTransform" class="form-select">
<option value="LowerHyphen">LowerHyphen — e.g. "jet-black"</option> <option value="LowerHyphen">LowerHyphen e.g. "jet-black"</option>
<option value="LowerUnderscore">LowerUnderscore — e.g. "jet_black"</option> <option value="LowerUnderscore">LowerUnderscore e.g. "jet_black"</option>
<option value="TitleHyphen">TitleHyphen — e.g. "Jet-Black"</option> <option value="TitleHyphen">TitleHyphen e.g. "Jet-Black"</option>
<option value="AsIs">AsIs — color name unchanged</option> <option value="AsIs">AsIs color name unchanged</option>
</select> </select>
<div class="form-text">How the color name is converted to a URL slug when building the product URL.</div> <div class="form-text">How the color name is converted to a URL slug when building the product URL.</div>
<span asp-validation-for="SlugTransform" class="text-danger small"></span> <span asp-validation-for="SlugTransform" class="text-danger small"></span>
@@ -9,7 +9,7 @@
<div class="container-fluid py-3" style="max-width:800px"> <div class="container-fluid py-3" style="max-width:800px">
<div class="d-flex align-items-center gap-3 mb-3"> <div class="d-flex align-items-center gap-3 mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back <i class="bi bi-arrow-left me-1"></i>Back
</a> </a>
<h4 class="mb-0"><i class="bi bi-bell me-2 text-primary"></i>Notification Details</h4> <h4 class="mb-0"><i class="bi bi-bell me-2 text-primary"></i>Notification Details</h4>
@@ -96,7 +96,7 @@
</div> </div>
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<a asp-action="Index" class="btn btn-secondary"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Cancel <i class="bi bi-arrow-left"></i> Cancel
</a> </a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.User.SuperAdminDetailsDto @model PowderCoating.Application.DTOs.User.SuperAdminDetailsDto
@{ @{
ViewData["Title"] = "User Details"; ViewData["Title"] = "User Details";
var isSuperAdmin = ViewBag.IsSuperAdmin as bool? ?? false; var isSuperAdmin = ViewBag.IsSuperAdmin as bool? ?? false;
@@ -198,7 +198,7 @@
<hr /> <hr />
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<a asp-action="Index" class="btn btn-secondary"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to List <i class="bi bi-arrow-left"></i> Back to List
</a> </a>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
@@ -289,7 +289,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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-warning"> <button type="submit" class="btn btn-warning">
<i class="bi bi-key"></i> Reset Password <i class="bi bi-key"></i> Reset Password
</button> </button>
@@ -323,7 +323,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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-danger"><i class="bi bi-slash-circle"></i> Ban User</button> <button type="submit" class="btn btn-danger"><i class="bi bi-slash-circle"></i> Ban User</button>
</div> </div>
</form> </form>
@@ -87,7 +87,7 @@
</div> </div>
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<a asp-action="Index" asp-route-filter="superadmins" class="btn btn-secondary"> <a asp-action="Index" asp-route-filter="superadmins" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Cancel <i class="bi bi-arrow-left"></i> Cancel
</a> </a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
@@ -1,4 +1,4 @@
@{ @{
ViewData["Title"] = "Grant SuperAdmin Access"; ViewData["Title"] = "Grant SuperAdmin Access";
ViewData["PageIcon"] = "bi-shield-plus"; ViewData["PageIcon"] = "bi-shield-plus";
} }
@@ -100,7 +100,7 @@
</p> </p>
</div> </div>
<div class="modal-footer"> <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-danger"> <button type="submit" class="btn btn-danger">
<i class="bi bi-shield-plus"></i> Grant SuperAdmin <i class="bi bi-shield-plus"></i> Grant SuperAdmin
</button> </button>
@@ -1,4 +1,4 @@
@model PagedResult<PowderCoating.Application.DTOs.User.PlatformUserListDto> @model PagedResult<PowderCoating.Application.DTOs.User.PlatformUserListDto>
@section Styles { @section Styles {
<style> <style>
@@ -188,7 +188,7 @@
} }
else else
{ {
<span class="btn btn-outline-secondary disabled" title="Root account — protected"> <span class="btn btn-outline-secondary disabled" title="Root account protected">
<i class="bi bi-shield-lock"></i> <i class="bi bi-shield-lock"></i>
</span> </span>
} }
@@ -234,7 +234,7 @@
</table> </table>
</div> </div>
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) --> <!-- Mobile card view shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<div class="mobile-card-view"> <div class="mobile-card-view">
<div class="mobile-card-list"> <div class="mobile-card-list">
@foreach (var user in Model.Items) @foreach (var user in Model.Items)
@@ -295,7 +295,7 @@
</div> </div>
</div> </div>
<div class="mobile-card-footer"> <div class="mobile-card-footer">
<span class="btn btn-sm btn-outline-primary">View →</span> <span class="btn btn-sm btn-outline-primary">View </span>
</div> </div>
</a> </a>
} }
@@ -349,7 +349,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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-warning"> <button type="submit" class="btn btn-warning">
<i class="bi bi-key"></i> Reset Password <i class="bi bi-key"></i> Reset Password
</button> </button>
@@ -382,7 +382,7 @@
</p> </p>
</div> </div>
<div class="modal-footer"> <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-danger"> <button type="submit" class="btn btn-danger">
<i class="bi bi-shield-x"></i> Revoke Access <i class="bi bi-shield-x"></i> Revoke Access
</button> </button>
@@ -7,7 +7,7 @@
<div class="container-fluid py-3"> <div class="container-fluid py-3">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm me-3"> <a asp-action="Index" class="btn btn-outline-secondary me-3">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-left"></i>
</a> </a>
<div> <div>
@@ -7,7 +7,7 @@
<div class="container-fluid py-3"> <div class="container-fluid py-3">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm me-3"> <a asp-action="Index" class="btn btn-outline-secondary me-3">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-left"></i>
</a> </a>
<div> <div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Powder.PowderInsightsDashboardDto @model PowderCoating.Application.DTOs.Powder.PowderInsightsDashboardDto
@{ @{
ViewData["Title"] = "Powder Insights"; ViewData["Title"] = "Powder Insights";
ViewData["PageIcon"] = "bi-graph-up"; ViewData["PageIcon"] = "bi-graph-up";
@@ -6,12 +6,12 @@
} }
<div class="d-flex justify-content-end mb-4"> <div class="d-flex justify-content-end mb-4">
<a asp-controller="Inventory" asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-controller="Inventory" asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-box-seam me-1"></i>Inventory <i class="bi bi-box-seam me-1"></i>Inventory
</a> </a>
</div> </div>
@* ── KPI cards ── *@ @* ── KPI cards ── *@
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-6 col-md-3"> <div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100"> <div class="card border-0 shadow-sm h-100">
@@ -49,7 +49,7 @@
</div> </div>
</div> </div>
@* ── Tabs ── *@ @* ── Tabs ── *@
<ul class="nav nav-tabs mb-3" id="insightsTabs" role="tablist"> <ul class="nav nav-tabs mb-3" id="insightsTabs" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="forecast-tab" data-bs-toggle="tab" data-bs-target="#forecast" type="button"> <button class="nav-link active" id="forecast-tab" data-bs-toggle="tab" data-bs-target="#forecast" type="button">
@@ -74,7 +74,7 @@
<div class="tab-content"> <div class="tab-content">
@* ── Tab 1: Stock Forecast (Layer 2, immediate value) ── *@ @* ── Tab 1: Stock Forecast (Layer 2, immediate value) ── *@
<div class="tab-pane fade show active" id="forecast" role="tabpanel"> <div class="tab-pane fade show active" id="forecast" role="tabpanel">
@if (!Model.LowStockAlerts.Any()) @if (!Model.LowStockAlerts.Any())
{ {
@@ -87,7 +87,7 @@
{ {
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header bg-transparent"> <div class="card-header bg-transparent">
<h6 class="mb-0"><i class="bi bi-box-seam me-2"></i>Powder Demand vs. Stock — Active Jobs</h6> <h6 class="mb-0"><i class="bi bi-box-seam me-2"></i>Powder Demand vs. Stock Active Jobs</h6>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover mb-0"> <table class="table table-hover mb-0">
@@ -115,7 +115,7 @@
<td class="text-end fw-semibold">@item.CurrentStockLbs.ToString("0.##") lbs</td> <td class="text-end fw-semibold">@item.CurrentStockLbs.ToString("0.##") lbs</td>
<td class="text-end">@item.ScheduledDemandLbs.ToString("0.##") lbs</td> <td class="text-end">@item.ScheduledDemandLbs.ToString("0.##") lbs</td>
<td class="text-end @(item.ShortfallLbs > 0 ? "text-danger fw-bold" : "text-muted")"> <td class="text-end @(item.ShortfallLbs > 0 ? "text-danger fw-bold" : "text-muted")">
@(item.ShortfallLbs > 0 ? $"{item.ShortfallLbs:0.##} lbs" : "—") @(item.ShortfallLbs > 0 ? $"{item.ShortfallLbs:0.##} lbs" : "")
</td> </td>
<td class="text-center">@item.ActiveJobCount</td> <td class="text-center">@item.ActiveJobCount</td>
<td class="text-center"> <td class="text-center">
@@ -141,7 +141,7 @@
} }
</div> </div>
@* ── Tab 2: Coverage Efficiency (Layer 2) ── *@ @* ── Tab 2: Coverage Efficiency (Layer 2) ── *@
<div class="tab-pane fade" id="efficiency" role="tabpanel"> <div class="tab-pane fade" id="efficiency" role="tabpanel">
@if (!readiness.IsLayer2Ready) @if (!readiness.IsLayer2Ready)
{ {
@@ -157,7 +157,7 @@
else if (!Model.EfficiencyBySku.Any()) else if (!Model.EfficiencyBySku.Any())
{ {
<div class="text-center py-5 text-muted"> <div class="text-center py-5 text-muted">
<p>No efficiency data yet — record actual powder usage on completed jobs to see this chart.</p> <p>No efficiency data yet record actual powder usage on completed jobs to see this chart.</p>
</div> </div>
} }
else else
@@ -188,11 +188,11 @@
<strong>@eff.Name</strong> <strong>@eff.Name</strong>
@if (!string.IsNullOrEmpty(eff.ColorName)) @if (!string.IsNullOrEmpty(eff.ColorName))
{ {
<br /><small class="text-muted">@eff.ColorName · @eff.Manufacturer</small> <br /><small class="text-muted">@eff.ColorName · @eff.Manufacturer</small>
} }
@if (!eff.HasEnoughData) @if (!eff.HasEnoughData)
{ {
<br /><small class="text-muted fst-italic">Low confidence — need 5+ samples</small> <br /><small class="text-muted fst-italic">Low confidence need 5+ samples</small>
} }
</td> </td>
<td class="text-end">@eff.CatalogCoverageSqFtPerLb.ToString("0.#") sq ft/lb</td> <td class="text-end">@eff.CatalogCoverageSqFtPerLb.ToString("0.#") sq ft/lb</td>
@@ -214,7 +214,7 @@
} }
</div> </div>
@* ── Tab 3: Predictive (Layer 3, gated) ── *@ @* ── Tab 3: Predictive (Layer 3, gated) ── *@
<div class="tab-pane fade" id="predictive" role="tabpanel"> <div class="tab-pane fade" id="predictive" role="tabpanel">
@if (!readiness.IsLayer3Ready) @if (!readiness.IsLayer3Ready)
{ {
@@ -238,9 +238,9 @@
<div class="alert alert-info alert-permanent text-start"> <div class="alert alert-info alert-permanent text-start">
<h6 class="alert-heading"><i class="bi bi-lightbulb me-2"></i>What unlocks here</h6> <h6 class="alert-heading"><i class="bi bi-lightbulb me-2"></i>What unlocks here</h6>
<ul class="mb-0 small"> <ul class="mb-0 small">
<li><strong>Smart reorder suggestions</strong> — quantity recommendations based on your actual usage history + scheduled job pipeline</li> <li><strong>Smart reorder suggestions</strong> quantity recommendations based on your actual usage history + scheduled job pipeline</li>
<li><strong>Waste pattern detection</strong> — identifies jobs and powder types that consistently over-consume</li> <li><strong>Waste pattern detection</strong> identifies jobs and powder types that consistently over-consume</li>
<li><strong>Per-powder efficiency corrections</strong> — suggests updating coverage defaults based on real data</li> <li><strong>Per-powder efficiency corrections</strong> suggests updating coverage defaults based on real data</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -258,7 +258,7 @@
</div> </div>
@if (!Model.ReorderSuggestions.Any()) @if (!Model.ReorderSuggestions.Any())
{ {
<div class="card-body text-muted text-center py-4">No reorder suggestions — stock levels look good for upcoming pipeline.</div> <div class="card-body text-muted text-center py-4">No reorder suggestions stock levels look good for upcoming pipeline.</div>
} }
else else
{ {
@@ -284,7 +284,7 @@
<strong>@s.Name</strong> <strong>@s.Name</strong>
@if (!string.IsNullOrEmpty(s.ColorName)) @if (!string.IsNullOrEmpty(s.ColorName))
{ {
<br /><small class="text-muted">@s.ColorName · @s.Manufacturer</small> <br /><small class="text-muted">@s.ColorName · @s.Manufacturer</small>
} }
</td> </td>
<td class="text-end">@s.CurrentStockLbs.ToString("0.#") lbs</td> <td class="text-end">@s.CurrentStockLbs.ToString("0.#") lbs</td>
@@ -311,7 +311,7 @@
@* Waste Patterns *@ @* Waste Patterns *@
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header bg-transparent"> <div class="card-header bg-transparent">
<h6 class="mb-0"><i class="bi bi-exclamation-triangle me-2 text-warning"></i>Waste Patterns <small class="text-muted fw-normal">— coats that used &gt;20% more than estimated</small></h6> <h6 class="mb-0"><i class="bi bi-exclamation-triangle me-2 text-warning"></i>Waste Patterns <small class="text-muted fw-normal"> coats that used &gt;20% more than estimated</small></h6>
</div> </div>
@if (!Model.WastePatterns.Any()) @if (!Model.WastePatterns.Any())
{ {
@@ -344,7 +344,7 @@
<br /><small class="text-muted">@w.CoatName</small> <br /><small class="text-muted">@w.CoatName</small>
</td> </td>
<td class="text-muted small">@(w.InventoryItemName ?? "Custom")</td> <td class="text-muted small">@(w.InventoryItemName ?? "Custom")</td>
<td>@(w.Complexity ?? "—")</td> <td>@(w.Complexity ?? "")</td>
<td class="text-end">@w.EstimatedLbs.ToString("0.##") lbs</td> <td class="text-end">@w.EstimatedLbs.ToString("0.##") lbs</td>
<td class="text-end">@w.ActualLbs.ToString("0.##") lbs</td> <td class="text-end">@w.ActualLbs.ToString("0.##") lbs</td>
<td class="text-end text-danger fw-bold">+@w.OveragePct.ToString("0.#")%</td> <td class="text-end text-danger fw-bold">+@w.OveragePct.ToString("0.#")%</td>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Quote.CreateQuoteDto @model PowderCoating.Application.DTOs.Quote.CreateQuoteDto
@using PowderCoating.Core.Entities @using PowderCoating.Core.Entities
@{ @{
@@ -51,7 +51,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Customer vs Prospect/Walk-In" data-bs-title="Customer vs Prospect/Walk-In"
data-bs-content="Choose &lt;strong&gt;Existing Customer&lt;/strong&gt; if this person is already in your system. Choose &lt;strong&gt;New Prospect/Walk-In&lt;/strong&gt; if they haven't committed yet — their details stay on the quote. When they approve, you can convert them to a full customer record with one click.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#prospect-conversion' target='_blank'&gt;Learn more →&lt;/a&gt;"> data-bs-content="Choose &lt;strong&gt;Existing Customer&lt;/strong&gt; if this person is already in your system. Choose &lt;strong&gt;New Prospect/Walk-In&lt;/strong&gt; if they haven't committed yet their details stay on the quote. When they approve, you can convert them to a full customer record with one click.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#prospect-conversion' target='_blank'&gt;Learn more &lt;/a&gt;">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</h5> </h5>
@@ -146,7 +146,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Quote Information" data-bs-title="Quote Information"
data-bs-content="Set the quote date, expiration, and any internal notes. The &lt;strong&gt;Expiration Date&lt;/strong&gt; is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The &lt;strong&gt;Customer PO&lt;/strong&gt; field is optional — use it if the customer provides their own purchase order number.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more →&lt;/a&gt;"> data-bs-content="Set the quote date, expiration, and any internal notes. The &lt;strong&gt;Expiration Date&lt;/strong&gt; is shown to the customer once it passes the quote is flagged Expired and can no longer be approved without editing. The &lt;strong&gt;Customer PO&lt;/strong&gt; field is optional use it if the customer provides their own purchase order number.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more &lt;/a&gt;">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</h5> </h5>
@@ -210,7 +210,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Oven &amp; Batch Pricing" data-bs-title="Oven &amp; Batch Pricing"
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill — for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings."> data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</h5> </h5>
@@ -253,7 +253,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Quote Item Types" data-bs-title="Quote Item Types"
data-bs-content="&lt;strong&gt;Calculated&lt;/strong&gt; — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.&lt;br&gt;&lt;strong&gt;Custom Work&lt;/strong&gt; — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.&lt;br&gt;&lt;strong&gt;AI Photo&lt;/strong&gt; — upload photos and let the AI estimate surface area and complexity for you.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-items' target='_blank'&gt;Learn more →&lt;/a&gt;"> data-bs-content="&lt;strong&gt;Calculated&lt;/strong&gt; you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.&lt;br&gt;&lt;strong&gt;Custom Work&lt;/strong&gt; you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.&lt;br&gt;&lt;strong&gt;AI Photo&lt;/strong&gt; upload photos and let the AI estimate surface area and complexity for you.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-items' target='_blank'&gt;Learn more &lt;/a&gt;">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</h5> </h5>
@@ -314,7 +314,7 @@
<div class="form-check"> <div class="form-check">
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" /> <input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" />
<label class="form-check-label small" for="hideDiscountFromCustomer"> <label class="form-check-label small" for="hideDiscountFromCustomer">
Hide discount from customer — PDFs and approval portal show final price only Hide discount from customer PDFs and approval portal show final price only
</label> </label>
</div> </div>
</div> </div>
@@ -329,7 +329,7 @@
<a tabindex="0" class="help-icon text-white" role="button" <a tabindex="0" class="help-icon text-white" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-toggle="popover" data-bs-placement="left"
data-bs-title="Pricing Summary" data-bs-title="Pricing Summary"
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin — all based on the rates in Settings. A &lt;strong&gt;Tier Discount&lt;/strong&gt; appears automatically if the customer has a pricing tier assigned. A &lt;strong&gt;Rush Fee&lt;/strong&gt; is added when Rush Job is checked.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#pricing-breakdown' target='_blank'&gt;Learn more →&lt;/a&gt;"> data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin all based on the rates in Settings. A &lt;strong&gt;Tier Discount&lt;/strong&gt; appears automatically if the customer has a pricing tier assigned. A &lt;strong&gt;Rush Fee&lt;/strong&gt; is added when Rush Job is checked.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#pricing-breakdown' target='_blank'&gt;Learn more &lt;/a&gt;">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</h5> </h5>
@@ -340,7 +340,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p> <p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p> <p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow"> <p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min): <i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong> <strong id="ovenBatchCostDisplay">$0.00</strong>
</p> </p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow"> <p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -379,7 +379,7 @@
<div class="row g-3" id="stagedPhotoGrid"></div> <div class="row g-3" id="stagedPhotoGrid"></div>
<div id="stagedPhotoUploadProgress" class="d-none mt-2"> <div id="stagedPhotoUploadProgress" class="d-none mt-2">
<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div> <div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div>
<small class="text-muted">Uploading…</small> <small class="text-muted">Uploading</small>
</div> </div>
</div> </div>
</div> </div>
@@ -446,7 +446,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label> <div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div> <input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div> </div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small> <small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div> </div>
<div id="cylinderInputs" style="display:none"> <div id="cylinderInputs" style="display:none">
<div class="row g-2"> <div class="row g-2">
@@ -464,7 +464,7 @@
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div> <div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="useSqFtResult()"> <button type="button" class="btn btn-primary" onclick="useSqFtResult()">
<i class="bi bi-check-circle me-1"></i>Use This Value <i class="bi bi-check-circle me-1"></i>Use This Value
</button> </button>
@@ -681,7 +681,7 @@
<script src="~/lib/tom-select/js/tom-select.complete.min.js"></script> <script src="~/lib/tom-select/js/tom-select.complete.min.js"></script>
<script src="~/js/item-wizard.js?v=@DateTime.Now.Ticks"></script> <script src="~/js/item-wizard.js?v=@DateTime.Now.Ticks"></script>
<script> <script>
// ── Quick / Full quote mode toggle ────────────────────────────────── // ── Quick / Full quote mode toggle ──────────────────────────────────
(function () { (function () {
const STORAGE_KEY = 'pcl_quote_mode'; const STORAGE_KEY = 'pcl_quote_mode';
const form = document.getElementById('quoteForm'); const form = document.getElementById('quoteForm');
@@ -690,7 +690,7 @@
function applyMode(mode) { function applyMode(mode) {
if (mode === 'simple') { if (mode === 'simple') {
form.classList.add('quote-simple-mode'); form.classList.add('quote-simple-mode');
hint.textContent = 'Advanced fields are hidden — switch to Full Quote to see them.'; hint.textContent = 'Advanced fields are hidden switch to Full Quote to see them.';
} else { } else {
form.classList.remove('quote-simple-mode'); form.classList.remove('quote-simple-mode');
hint.textContent = ''; hint.textContent = '';
@@ -758,8 +758,8 @@
smsNote.style.display = 'inline'; smsNote.style.display = 'inline';
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark'; smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
smsNote.innerHTML = hasSms smsNote.innerHTML = hasSms
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details' ? '<i class="bi bi-phone me-1"></i>No email send via SMS from quote details'
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required'; : '<i class="bi bi-phone-slash me-1"></i>No email SMS consent required';
} else { } else {
smsNote.style.display = 'none'; smsNote.style.display = 'none';
} }
@@ -32,7 +32,7 @@
<button onclick="printQuotePdf(@Model.Id)" class="btn btn-info"> <button onclick="printQuotePdf(@Model.Id)" class="btn btn-info">
<i class="bi bi-printer me-1"></i>Print <i class="bi bi-printer me-1"></i>Print
</button> </button>
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-secondary"> <a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">
<i class="bi bi-pencil me-1"></i>Edit <i class="bi bi-pencil me-1"></i>Edit
</a> </a>
@if (Model.IsProspect && Model.StatusCode == "APPROVED") @if (Model.IsProspect && Model.StatusCode == "APPROVED")
@@ -796,10 +796,10 @@
<button type="button" class="btn btn-danger" id="qpDeleteBtn" onclick="qpGallery.deletePhoto()"> <button type="button" class="btn btn-danger" id="qpDeleteBtn" onclick="qpGallery.deletePhoto()">
<i class="bi bi-trash me-1"></i>Delete <i class="bi bi-trash me-1"></i>Delete
</button> </button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
<div id="qpEditButtons" class="d-none d-flex gap-2 w-100 justify-content-end"> <div id="qpEditButtons" class="d-none d-flex gap-2 w-100 justify-content-end">
<button type="button" class="btn btn-secondary" onclick="qpGallery.cancelEdit()">Cancel</button> <button type="button" class="btn btn-outline-secondary" onclick="qpGallery.cancelEdit()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="qpGallery.saveEdit()"> <button type="button" class="btn btn-primary" onclick="qpGallery.saveEdit()">
<i class="bi bi-check-lg me-1"></i>Save Caption <i class="bi bi-check-lg me-1"></i>Save Caption
</button> </button>
@@ -1897,7 +1897,7 @@
<div id="depositFormError" class="alert alert-danger d-none"></div> <div id="depositFormError" class="alert alert-danger d-none"></div>
</div> </div>
<div class="modal-footer"> <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-success" id="saveDepositBtn"> <button type="submit" class="btn btn-success" id="saveDepositBtn">
<i class="bi bi-check-circle me-1"></i>Save & Generate Receipt <i class="bi bi-check-circle me-1"></i>Save & Generate Receipt
</button> </button>
@@ -2156,7 +2156,7 @@
<div id="quoteAdHocEmailError" class="text-danger small mt-1 d-none"></div> <div id="quoteAdHocEmailError" class="text-danger small mt-1 d-none"></div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="sendQuoteToAdHocEmail(@Model.Id)"> <button type="button" class="btn btn-primary" onclick="sendQuoteToAdHocEmail(@Model.Id)">
<i class="bi bi-send me-1"></i>Send <i class="bi bi-send me-1"></i>Send
</button> </button>
@@ -2186,7 +2186,7 @@
</div> </div>
</div> </div>
<div class="modal-footer d-none" id="sendQuoteSmsFooter"> <div class="modal-footer d-none" id="sendQuoteSmsFooter">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -2213,7 +2213,7 @@
</div> </div>
</div> </div>
<div class="modal-footer d-none" id="sendQuoteFooter"> <div class="modal-footer d-none" id="sendQuoteFooter">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -2254,7 +2254,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
+18 -18
View File
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Quote.UpdateQuoteDto @model PowderCoating.Application.DTOs.Quote.UpdateQuoteDto
@using PowderCoating.Core.Entities @using PowderCoating.Core.Entities
@{ @{
@@ -109,7 +109,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Quote Information" data-bs-title="Quote Information"
data-bs-content="Set the quote date, expiration, and any internal notes. The &lt;strong&gt;Expiration Date&lt;/strong&gt; is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The &lt;strong&gt;Customer PO&lt;/strong&gt; field is optional — use it if the customer provides their own purchase order number.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more →&lt;/a&gt;"> data-bs-content="Set the quote date, expiration, and any internal notes. The &lt;strong&gt;Expiration Date&lt;/strong&gt; is shown to the customer once it passes the quote is flagged Expired and can no longer be approved without editing. The &lt;strong&gt;Customer PO&lt;/strong&gt; field is optional use it if the customer provides their own purchase order number.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more &lt;/a&gt;">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</h5> </h5>
@@ -173,7 +173,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Oven &amp; Batch Pricing" data-bs-title="Oven &amp; Batch Pricing"
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill — for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings."> data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</h5> </h5>
@@ -216,7 +216,7 @@
<a tabindex="0" class="help-icon" role="button" <a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Quote Item Types" data-bs-title="Quote Item Types"
data-bs-content="&lt;strong&gt;Calculated&lt;/strong&gt; — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.&lt;br&gt;&lt;strong&gt;Custom Work&lt;/strong&gt; — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.&lt;br&gt;&lt;strong&gt;AI Photo&lt;/strong&gt; — upload photos and let the AI estimate surface area and complexity for you.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-items' target='_blank'&gt;Learn more →&lt;/a&gt;"> data-bs-content="&lt;strong&gt;Calculated&lt;/strong&gt; you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.&lt;br&gt;&lt;strong&gt;Custom Work&lt;/strong&gt; you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.&lt;br&gt;&lt;strong&gt;AI Photo&lt;/strong&gt; upload photos and let the AI estimate surface area and complexity for you.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-items' target='_blank'&gt;Learn more &lt;/a&gt;">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</h5> </h5>
@@ -277,7 +277,7 @@
<div class="form-check"> <div class="form-check">
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" /> <input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" />
<label class="form-check-label small" for="hideDiscountFromCustomer"> <label class="form-check-label small" for="hideDiscountFromCustomer">
Hide discount from customer — PDFs and approval portal show final price only Hide discount from customer PDFs and approval portal show final price only
</label> </label>
</div> </div>
</div> </div>
@@ -292,7 +292,7 @@
<a tabindex="0" class="help-icon text-white" role="button" <a tabindex="0" class="help-icon text-white" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-toggle="popover" data-bs-placement="left"
data-bs-title="Pricing Summary" data-bs-title="Pricing Summary"
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin — all based on the rates in Settings. A &lt;strong&gt;Tier Discount&lt;/strong&gt; appears automatically if the customer has a pricing tier assigned. A &lt;strong&gt;Rush Fee&lt;/strong&gt; is added when Rush Job is checked.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#pricing-breakdown' target='_blank'&gt;Learn more →&lt;/a&gt;"> data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin all based on the rates in Settings. A &lt;strong&gt;Tier Discount&lt;/strong&gt; appears automatically if the customer has a pricing tier assigned. A &lt;strong&gt;Rush Fee&lt;/strong&gt; is added when Rush Job is checked.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#pricing-breakdown' target='_blank'&gt;Learn more &lt;/a&gt;">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
</a> </a>
</h5> </h5>
@@ -303,7 +303,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p> <p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p> <p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow"> <p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min): <i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong> <strong id="ovenBatchCostDisplay">$0.00</strong>
</p> </p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow"> <p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -396,7 +396,7 @@
<textarea class="form-control form-control-sm caption-textarea" rows="2" placeholder="Add a caption...">@photo.Caption</textarea> <textarea class="form-control form-control-sm caption-textarea" rows="2" placeholder="Add a caption...">@photo.Caption</textarea>
<div class="d-flex gap-1 mt-1"> <div class="d-flex gap-1 mt-1">
<button type="button" class="btn btn-primary btn-sm save-caption-btn flex-grow-1">Save</button> <button type="button" class="btn btn-primary btn-sm save-caption-btn flex-grow-1">Save</button>
<button type="button" class="btn btn-secondary btn-sm cancel-caption-btn">Cancel</button> <button type="button" class="btn btn-outline-secondary btn-sm cancel-caption-btn">Cancel</button>
</div> </div>
</div> </div>
} }
@@ -407,7 +407,7 @@
</div> </div>
<div id="editPhotoUploadProgress" class="d-none mt-2"> <div id="editPhotoUploadProgress" class="d-none mt-2">
<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div> <div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div>
<small class="text-muted">Uploading…</small> <small class="text-muted">Uploading</small>
</div> </div>
</div> </div>
</div> </div>
@@ -435,11 +435,11 @@
<span id="smsNotifyNote" class="badge @(editHasSms ? "bg-info text-white" : "bg-warning text-dark")"> <span id="smsNotifyNote" class="badge @(editHasSms ? "bg-info text-white" : "bg-warning text-dark")">
@if (editHasSms) @if (editHasSms)
{ {
<i class="bi bi-phone me-1"></i><text>No email — send via SMS from quote details</text> <i class="bi bi-phone me-1"></i><text>No email send via SMS from quote details</text>
} }
else else
{ {
<i class="bi bi-phone-slash me-1"></i><text>No email — SMS consent required</text> <i class="bi bi-phone-slash me-1"></i><text>No email SMS consent required</text>
} }
</span> </span>
} }
@@ -483,7 +483,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label> <div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div> <input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div> </div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small> <small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div> </div>
<div id="cylinderInputs" style="display:none"> <div id="cylinderInputs" style="display:none">
<div class="row g-2"> <div class="row g-2">
@@ -501,7 +501,7 @@
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div> <div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="useSqFtResult()"> <button type="button" class="btn btn-primary" onclick="useSqFtResult()">
<i class="bi bi-check-circle me-1"></i>Use This Value <i class="bi bi-check-circle me-1"></i>Use This Value
</button> </button>
@@ -579,7 +579,7 @@
@Html.Raw(Json.Serialize(ViewBag.BlastSetups ?? new List<object>())) @Html.Raw(Json.Serialize(ViewBag.BlastSetups ?? new List<object>()))
</script> </script>
<!-- Existing items — always populated on Edit --> <!-- Existing items always populated on Edit -->
<script id="existingItemsData" type="application/json"> <script id="existingItemsData" type="application/json">
@Html.Raw(System.Text.Json.JsonSerializer.Serialize((Model.QuoteItems ?? new List<PowderCoating.Application.DTOs.Quote.CreateQuoteItemDto>()).Select((item, i) => new { @Html.Raw(System.Text.Json.JsonSerializer.Serialize((Model.QuoteItems ?? new List<PowderCoating.Application.DTOs.Quote.CreateQuoteItemDto>()).Select((item, i) => new {
description = item.Description, description = item.Description,
@@ -740,8 +740,8 @@
smsNote.style.display = 'inline'; smsNote.style.display = 'inline';
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark'; smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
smsNote.innerHTML = hasSms smsNote.innerHTML = hasSms
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details' ? '<i class="bi bi-phone me-1"></i>No email send via SMS from quote details'
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required'; : '<i class="bi bi-phone-slash me-1"></i>No email SMS consent required';
} else { } else {
smsNote.style.display = 'none'; smsNote.style.display = 'none';
} }
@@ -811,7 +811,7 @@
} }
}); });
// Quote photo direct upload (Edit page — quoteId is known) // Quote photo direct upload (Edit page quoteId is known)
(function () { (function () {
const quoteId = @Model.Id; const quoteId = @Model.Id;
const uploadUrl = '@Url.Action("UploadQuotePhoto", "Quotes")'; const uploadUrl = '@Url.Action("UploadQuotePhoto", "Quotes")';
@@ -868,7 +868,7 @@
<textarea class="form-control form-control-sm caption-textarea" rows="2" placeholder="Add a caption..."></textarea> <textarea class="form-control form-control-sm caption-textarea" rows="2" placeholder="Add a caption..."></textarea>
<div class="d-flex gap-1 mt-1"> <div class="d-flex gap-1 mt-1">
<button type="button" class="btn btn-primary btn-sm save-caption-btn flex-grow-1">Save</button> <button type="button" class="btn btn-primary btn-sm save-caption-btn flex-grow-1">Save</button>
<button type="button" class="btn btn-secondary btn-sm cancel-caption-btn">Cancel</button> <button type="button" class="btn btn-outline-secondary btn-sm cancel-caption-btn">Cancel</button>
</div> </div>
</div> </div>
</div> </div>
@@ -343,7 +343,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" id="saveStatus"> <button type="button" class="btn btn-primary" id="saveStatus">
<i class="bi bi-save me-2"></i>Save Status <i class="bi bi-save me-2"></i>Save Status
</button> </button>
@@ -5,7 +5,7 @@
<div class="container py-4" style="max-width:800px"> <div class="container py-4" style="max-width:800px">
<div class="d-flex align-items-center gap-3 mb-4"> <div class="d-flex align-items-center gap-3 mb-4">
<a asp-action="Manage" class="btn btn-outline-secondary btn-sm"> <a asp-action="Manage" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back <i class="bi bi-arrow-left me-1"></i>Back
</a> </a>
<h4 class="mb-0"><i class="bi bi-plus-circle me-2 text-primary"></i>New Release Note</h4> <h4 class="mb-0"><i class="bi bi-plus-circle me-2 text-primary"></i>New Release Note</h4>
@@ -5,7 +5,7 @@
<div class="container py-4" style="max-width:800px"> <div class="container py-4" style="max-width:800px">
<div class="d-flex align-items-center gap-3 mb-4"> <div class="d-flex align-items-center gap-3 mb-4">
<a asp-action="Manage" class="btn btn-outline-secondary btn-sm"> <a asp-action="Manage" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back <i class="bi bi-arrow-left me-1"></i>Back
</a> </a>
<h4 class="mb-0"><i class="bi bi-pencil me-2 text-primary"></i>Edit v@Model.Version</h4> <h4 class="mb-0"><i class="bi bi-pencil me-2 text-primary"></i>Edit v@Model.Version</h4>
@@ -3403,7 +3403,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
@model List<PowderCoating.Core.Entities.Company> @model List<PowderCoating.Core.Entities.Company>
@{ @{
ViewData["Title"] = "Seed Data Management"; ViewData["Title"] = "Seed Data Management";
ViewData["PageIcon"] = "bi-database-fill-gear"; ViewData["PageIcon"] = "bi-database-fill-gear";
@@ -196,7 +196,7 @@
</table> </table>
</div> </div>
<!-- Mobile card view — shown on screens < 992px --> <!-- Mobile card view shown on screens < 992px -->
<div class="mobile-card-view d-lg-none"> <div class="mobile-card-view d-lg-none">
<div class="mobile-card-list"> <div class="mobile-card-list">
@foreach (var company in Model) @foreach (var company in Model)
@@ -363,7 +363,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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-warning" id="unseedSubmitBtn" disabled> <button type="submit" class="btn btn-warning" id="unseedSubmitBtn" disabled>
<i class="bi bi-database-fill-dash me-1"></i>Remove Selected Data <i class="bi bi-database-fill-dash me-1"></i>Remove Selected Data
</button> </button>
@@ -228,7 +228,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="applyLaborCalc()"> <button type="button" class="btn btn-primary" onclick="applyLaborCalc()">
<i class="bi bi-check2-circle me-1"></i>Apply &amp; Close <i class="bi bi-check2-circle me-1"></i>Apply &amp; Close
</button> </button>
@@ -298,7 +298,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="applyEquipCalc()"> <button type="button" class="btn btn-primary" onclick="applyEquipCalc()">
<i class="bi bi-check2-circle me-1"></i>Apply &amp; Close <i class="bi bi-check2-circle me-1"></i>Apply &amp; Close
</button> </button>
@@ -371,7 +371,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-primary" onclick="applyPowderCalc()"> <button type="button" class="btn btn-primary" onclick="applyPowderCalc()">
<i class="bi bi-check2-circle me-1"></i>Apply &amp; Close <i class="bi bi-check2-circle me-1"></i>Apply &amp; Close
</button> </button>
@@ -1503,10 +1503,10 @@
aria-label="Toggle sidebar"> aria-label="Toggle sidebar">
<i class="bi bi-list" style="font-size: 1.5rem;"></i> <i class="bi bi-list" style="font-size: 1.5rem;"></i>
</button> </button>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2" style="min-width:0;overflow:hidden;">
@if (ViewData["PageIcon"] != null) @if (ViewData["PageIcon"] != null)
{ {
<i class="bi @ViewData["PageIcon"]" style="font-size:1.25rem;color:var(--bs-secondary-color);"></i> <i class="bi @ViewData["PageIcon"]" style="font-size:1.25rem;color:var(--bs-secondary-color);flex-shrink:0;"></i>
} }
<h1 class="page-title mb-0">@ViewData["Title"]</h1> <h1 class="page-title mb-0">@ViewData["Title"]</h1>
@if (ViewData["PageHelpContent"] != null) @if (ViewData["PageHelpContent"] != null)
@@ -2287,7 +2287,7 @@
<p class="text-muted mb-0" id="globalConfirmMessage"></p> <p class="text-muted mb-0" id="globalConfirmMessage"></p>
</div> </div>
<div class="modal-footer border-0 justify-content-center gap-2 pb-4"> <div class="modal-footer border-0 justify-content-center gap-2 pb-4">
<button type="button" class="btn btn-secondary px-4" id="globalConfirmCancel">Cancel</button> <button type="button" class="btn btn-outline-secondary px-4" id="globalConfirmCancel">Cancel</button>
<button type="button" class="btn btn-danger px-4" id="globalConfirmOk">Confirm</button> <button type="button" class="btn btn-danger px-4" id="globalConfirmOk">Confirm</button>
</div> </div>
</div> </div>
@@ -274,7 +274,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
@@ -13,7 +13,7 @@
<div class="container-fluid py-3" style="max-width:960px"> <div class="container-fluid py-3" style="max-width:960px">
<div class="d-flex align-items-center gap-3 mb-3"> <div class="d-flex align-items-center gap-3 mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back <i class="bi bi-arrow-left me-1"></i>Back
</a> </a>
<h4 class="mb-0"> <h4 class="mb-0">
@@ -67,7 +67,7 @@
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center py-2"> <div class="card-header d-flex justify-content-between align-items-center py-2">
<span class="fw-semibold">Raw Payload</span> <span class="fw-semibold">Raw Payload</span>
<button class="btn btn-outline-secondary btn-sm" onclick="copyJson()"> <button class="btn btn-outline-secondary" onclick="copyJson()">
<i class="bi bi-clipboard me-1"></i>Copy <i class="bi bi-clipboard me-1"></i>Copy
</button> </button>
</div> </div>
@@ -1,8 +1,8 @@
@using PowderCoating.Core.Entities @using PowderCoating.Core.Entities
@using PowderCoating.Core.Enums @using PowderCoating.Core.Enums
@model Company @model Company
@{ @{
ViewData["Title"] = $"Manage – {Model.CompanyName}"; ViewData["Title"] = $"Manage {Model.CompanyName}";
var planConfigs = (dynamic)ViewBag.PlanConfigs; var planConfigs = (dynamic)ViewBag.PlanConfigs;
string PlanName(int plan) string PlanName(int plan)
@@ -22,11 +22,11 @@
<div class="container-fluid py-3" style="max-width:900px"> <div class="container-fluid py-3" style="max-width:900px">
<div class="d-flex align-items-center gap-3 mb-3"> <div class="d-flex align-items-center gap-3 mb-3">
<a asp-action="Index" class="btn btn-outline-secondary btn-sm"> <a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back <i class="bi bi-arrow-left me-1"></i>Back
</a> </a>
<a asp-controller="Companies" asp-action="Edit" asp-route-id="@Model.Id" <a asp-controller="Companies" asp-action="Edit" asp-route-id="@Model.Id"
class="btn btn-outline-secondary btn-sm"> class="btn btn-outline-secondary">
<i class="bi bi-building me-1"></i>Edit Company <i class="bi bi-building me-1"></i>Edit Company
</a> </a>
<h4 class="mb-0"> <h4 class="mb-0">
@@ -66,9 +66,9 @@
<dt class="col-7 text-muted">Users</dt> <dt class="col-7 text-muted">Users</dt>
<dd class="col-5 fw-semibold">@ViewBag.UserCount</dd> <dd class="col-5 fw-semibold">@ViewBag.UserCount</dd>
<dt class="col-7 text-muted">Stripe Customer</dt> <dt class="col-7 text-muted">Stripe Customer</dt>
<dd class="col-5"><code class="small">@(Model.StripeCustomerId ?? "—")</code></dd> <dd class="col-5"><code class="small">@(Model.StripeCustomerId ?? "")</code></dd>
<dt class="col-7 text-muted">Stripe Sub</dt> <dt class="col-7 text-muted">Stripe Sub</dt>
<dd class="col-5"><code class="small">@(Model.StripeSubscriptionId ?? "—")</code></dd> <dd class="col-5"><code class="small">@(Model.StripeSubscriptionId ?? "")</code></dd>
</dl> </dl>
</div> </div>
</div> </div>
@@ -99,7 +99,7 @@
<form method="post" asp-action="UpdateSubscription" asp-route-id="@Model.Id"> <form method="post" asp-action="UpdateSubscription" asp-route-id="@Model.Id">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
@* Comped / Internal card — prominent *@ @* Comped / Internal card prominent *@
<div class="card border-0 shadow-sm mb-3 @(Model.IsComped ? "border-success border-2" : "")"> <div class="card border-0 shadow-sm mb-3 @(Model.IsComped ? "border-success border-2" : "")">
<div class="card-header border-0 py-3 @(Model.IsComped ? "bg-success bg-opacity-10" : "bg-white")"> <div class="card-header border-0 py-3 @(Model.IsComped ? "bg-success bg-opacity-10" : "bg-white")">
<h6 class="mb-0 fw-semibold"> <h6 class="mb-0 fw-semibold">
@@ -351,11 +351,11 @@
</div> </div>
<dl class="row small mb-3"> <dl class="row small mb-3">
<dt class="col-5 text-muted">Invoice</dt> <dt class="col-5 text-muted">Invoice</dt>
<dd class="col-7 font-monospace" id="refund-invoice-number">—</dd> <dd class="col-7 font-monospace" id="refund-invoice-number"></dd>
<dt class="col-5 text-muted">Amount Paid</dt> <dt class="col-5 text-muted">Amount Paid</dt>
<dd class="col-7 fw-semibold" id="refund-amount-paid">—</dd> <dd class="col-7 fw-semibold" id="refund-amount-paid"></dd>
<dt class="col-5 text-muted">Max Refundable</dt> <dt class="col-5 text-muted">Max Refundable</dt>
<dd class="col-7 fw-semibold text-success" id="refund-max-amount">—</dd> <dd class="col-7 fw-semibold text-success" id="refund-max-amount"></dd>
</dl> </dl>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-medium">Refund Amount</label> <label class="form-label fw-medium">Refund Amount</label>
@@ -374,7 +374,7 @@
<div id="refund-result" class="d-none"></div> <div id="refund-result" class="d-none"></div>
</div> </div>
<div class="modal-footer"> <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="button" class="btn btn-danger" id="refund-submit-btn" onclick="submitRefund()"> <button type="button" class="btn btn-danger" id="refund-submit-btn" onclick="submitRefund()">
<i class="bi bi-arrow-counterclockwise me-1"></i>Issue Refund <i class="bi bi-arrow-counterclockwise me-1"></i>Issue Refund
</button> </button>
@@ -402,7 +402,7 @@
@section Scripts { @section Scripts {
<script> <script>
// ── State for the refund modal ──────────────────────────────────────────────── // ── State for the refund modal ────────────────────────────────────────────────
let _refundPaymentIntentId = null; let _refundPaymentIntentId = null;
let _refundMaxCents = 0; let _refundMaxCents = 0;
let _refundAmountPaid = ''; let _refundAmountPaid = '';
@@ -442,7 +442,7 @@ async function loadPaymentHistory() {
: ''; : '';
const refundedCell = ch.amountRefunded const refundedCell = ch.amountRefunded
? `<span class="text-danger small">${ch.amountRefunded}</span>` ? `<span class="text-danger small">${ch.amountRefunded}</span>`
: '<span class="text-muted small">—</span>'; : '<span class="text-muted small"></span>';
const desc = ch.description ? `<span class="text-muted">${ch.description}</span>` : `<code class="small">${ch.id}</code>`; const desc = ch.description ? `<span class="text-muted">${ch.description}</span>` : `<code class="small">${ch.id}</code>`;
// Show Refund button only for succeeded charges that still have something refundable // Show Refund button only for succeeded charges that still have something refundable
@@ -469,7 +469,7 @@ async function loadPaymentHistory() {
} }
function openRefundModal(chargeId, refundableCents, amountPaid, displayLabel) { function openRefundModal(chargeId, refundableCents, amountPaid, displayLabel) {
_refundPaymentIntentId = chargeId; // reusing variable — now holds charge ID _refundPaymentIntentId = chargeId; // reusing variable now holds charge ID
_refundMaxCents = refundableCents; _refundMaxCents = refundableCents;
_refundAmountPaid = amountPaid; _refundAmountPaid = amountPaid;
_refundInvoiceNumber = displayLabel; _refundInvoiceNumber = displayLabel;
@@ -509,7 +509,7 @@ async function submitRefund() {
} }
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Processing…'; submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Processing';
try { try {
const formData = new FormData(); const formData = new FormData();
@@ -204,7 +204,7 @@
<small class="text-muted me-auto"> <small class="text-muted me-auto">
<i class="bi bi-lightbulb me-1"></i>Easter egg unlocked! 🎉 <i class="bi bi-lightbulb me-1"></i>Easter egg unlocked! 🎉
</small> </small>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
+131
View File
@@ -1020,4 +1020,135 @@ a.tag-index-badge:hover {
.mw-xs { max-width: 280px; } .mw-xs { max-width: 280px; }
.mw-sm { max-width: 360px; } .mw-sm { max-width: 360px; }
.mw-md { max-width: 480px; } .mw-md { max-width: 480px; }
/* =============================================================
MOBILE / TABLET RESPONSIVENESS — Phase 2 patches
============================================================= */
/* ── 1. Top-navbar: prevent left-side from overflowing on phone ──────────
The navbar left side is: hamburger + icon + h1.page-title + company badge.
On narrow phones (<576px) this row is ~340px of content in ~330px space.
Fix: let the page-title shrink and truncate; hide company badge. */
@media (max-width: 575.98px) {
.top-navbar {
padding: 0.625rem 0.75rem;
gap: 0.35rem;
}
/* Allow left cluster to shrink — prevents pushing user-menu off-screen */
.top-navbar > .d-flex:first-child {
min-width: 0;
flex: 1 1 0;
gap: 0.35rem;
}
.page-title {
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
max-width: 40vw;
}
/* Company badge in top-navbar adds width without adding value on phone — hide it */
.top-navbar .badge.bg-primary {
display: none !important;
}
}
/* ── 2. Content area: flex children need min-width:0 to allow shrinking ──
Without this, h4/h5 inside a flex container can't shrink below their
natural content width, blowing the container past the viewport edge. */
.d-flex > h4,
.d-flex > h5 {
min-width: 0;
}
/* ── 3. Page-header rows: let title stack above action buttons on phone ──
The near-universal pattern is:
d-flex justify-content-between align-items-center mb-3 / mb-4
Adding flex-wrap lets the h4 take line 1, buttons wrap to line 2.
The mb-3/mb-4 specificity ensures we only hit block-level separators,
not inline rows or filter bars. */
@media (max-width: 575.98px) {
.d-flex.justify-content-between.align-items-center.mb-3,
.d-flex.justify-content-between.align-items-center.mb-4,
.d-flex.align-items-center.justify-content-between.mb-3,
.d-flex.align-items-center.justify-content-between.mb-4 {
flex-wrap: wrap;
gap: 0.5rem;
}
}
/* ── 4. Card headers: let title + card-level actions wrap on phone ────────
Card headers use the same justify-content-between pattern. */
@media (max-width: 575.98px) {
.card-header .d-flex.justify-content-between,
.card-header .d-flex.align-items-center.gap-2 {
flex-wrap: wrap;
gap: 0.5rem;
}
.card-header h5 {
min-width: 0;
flex-shrink: 1;
}
}
/* ── 5. Details page top button bars: wrap instead of overflow ────────────
Pattern: d-flex justify-content-end gap-2 mb-4 (Download PDF, Back, Edit…) */
@media (max-width: 575.98px) {
.d-flex.justify-content-end.gap-2.mb-4,
.d-flex.gap-2.justify-content-end.mb-4 {
flex-wrap: wrap;
}
}
/* ── 6. Alert filter banners: wrap action button below text on phone ──────
Pattern: alert d-flex justify-content-between align-items-center */
@media (max-width: 575.98px) {
.alert.d-flex.justify-content-between.align-items-center,
.alert.d-flex.align-items-center.justify-content-between {
flex-wrap: wrap;
gap: 0.4rem;
}
}
/* ── 7. Table row buttons: exempt from the global 44px touch-target floor ─
The global rule (.btn, .btn-sm { min-height: 44px }) is right for form
buttons, but forces table rows to ~50px tall on mobile — too bloated.
Buttons inside .table and inside .btn-group-sm stay compact. */
@media (max-width: 768px) {
.table .btn,
.table .btn-sm,
.btn-group-sm > .btn {
min-height: unset !important;
padding: 0.25rem 0.5rem !important;
font-size: 0.8rem !important;
}
}
/* ── 8. Notification dropdown: prevent overflow on narrow phones ──────────
Fixed width: 360px clips content on 360px phones. */
@media (max-width: 575.98px) {
.notif-dropdown {
width: calc(100vw - 2rem) !important;
min-width: unset !important;
}
}
/* ── 9. Search inputs in card/page headers: remove enforced min-width ─────
Several views set style="min-width:200px" inline on the input-group.
This is fine on tablets but breaks layout on <400px phones. */
@media (max-width: 400px) {
.card-header .input-group,
.input-group[style*="min-width"] {
min-width: unset !important;
}
}
/* ── 10. Tablet (768px): tighten content area padding ─────────────────────
2rem padding on both sides = 64px wasted on a 768px screen. Use 1.25rem. */
@media (min-width: 577px) and (max-width: 991px) {
.content-area {
padding: 1.25rem;
}
}
.mw-lg { max-width: 640px; } .mw-lg { max-width: 640px; }