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="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>
</div>
@@ -6,7 +6,7 @@
<div class="container-fluid py-3" style="max-width:700px">
<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>
</div>
@@ -20,7 +20,7 @@
<div class="container-fluid py-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>
<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
</a>
</div>
@@ -69,7 +69,7 @@
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" id="quickCreateSubmit">
<i class="bi bi-check-circle"></i> Create Appointment
</button>
@@ -17,7 +17,7 @@
<div class="container-fluid py-3" style="max-width:900px">
<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
</a>
<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">
<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
</a>
</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["PageIcon"] = "bi-receipt-cutoff";
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;
int? fromPoId = ViewBag.FromPoId as int?;
}
@@ -13,7 +13,7 @@
<div>
@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>
@if (fromPoId.HasValue)
@@ -44,7 +44,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</div>
@@ -66,8 +66,8 @@
<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"
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
<option value="">— Select Vendor —</option>
<option value="__new__">+ Add New Vendor…</option>
<option value=""> Select Vendor </option>
<option value="__new__">+ Add New Vendor</option>
</select>
<span asp-validation-for="VendorId" class="text-danger small"></span>
</div>
@@ -100,7 +100,7 @@
<label for="receiptFile" class="form-label fw-medium">Attach Receipt / Document</label>
<input type="file" name="receiptFile" id="receiptFile" 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>
@@ -114,7 +114,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</div>
@@ -150,7 +150,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
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>
</a>
</div>
@@ -198,7 +198,7 @@
<div class="mb-2">
<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">
<option value="">— Select Account —</option>
<option value=""> Select Account </option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
{
<option value="@item.Value">@item.Text</option>
@@ -232,7 +232,7 @@
<tr class="line-item-row">
<td>
<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)
{
<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>
<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)
{
<option value="@item.Value">@item.Text</option>
@@ -273,12 +273,12 @@
<div class="mb-3">
<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" />
<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 id="scanReceiptStatus" class="text-muted small mt-2"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="scanReceiptUploadBtn">
<i class="bi bi-camera me-1"></i>Scan &amp; Fill
</button>
@@ -393,8 +393,8 @@
}
if (lineCount === 0) addLineItem();
// ── AI Auto-suggest Account on description blur ───────────────────────
// Keyword shortcuts — handle common cases with zero API cost
// ── AI Auto-suggest Account on description blur ───────────────────────
// Keyword shortcuts handle common cases with zero API cost
const _keywordMap = [
{ words: ['electric','power','utility','gas','water','internet','phone','telecom'], hint: 'utilities' },
{ words: ['powder','paint','coat','material','supply','supplies','chemical','resin'], hint: 'materials' },
@@ -407,7 +407,7 @@
{ words: ['advertising','marketing','promo'], hint: 'advertising' },
];
// Session cache: description (lowercased) → { accountId, accountName }
// Session cache: description (lowercased) { accountId, accountName }
const _suggestCache = new Map();
function _keywordGuess(description) {
@@ -480,7 +480,7 @@
hint2.className = 'ai-account-hint text-muted small mt-1';
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 {
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) {
if (e.target.matches('[name$=".Description"]')) {
_suggestAccountForRow(e.target.closest('tr'));
}
}, true); // capture phase so blur bubbles
// ── Scan Receipt ─────────────────────────────────────────────────────
// ── Scan Receipt ─────────────────────────────────────────────────────
document.getElementById('scanReceiptUploadBtn').addEventListener('click', async function () {
const fileInput = document.getElementById('scanReceiptFile');
if (!fileInput.files.length) { alert('Please select a file.'); return; }
@@ -535,7 +535,7 @@
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) {
const vendorSel = document.getElementById('vendorSelect');
if (vendorSel && !vendorSel.value) {
@@ -553,7 +553,7 @@
vendorSel.value = bestOption.value;
vendorSel.dispatchEvent(new Event('change'));
} 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"]');
if (memo && !memo.value) memo.value = data.vendorName;
}
@@ -598,7 +598,7 @@
const modal = bootstrap.Modal.getInstance(document.getElementById('scanReceiptModal'));
if (modal) modal.hide();
statusEl.textContent = 'Scan complete — review and adjust as needed.';
statusEl.textContent = 'Scan complete review and adjust as needed.';
} catch (e) {
statusEl.textContent = 'Error connecting to AI service.';
} finally {
@@ -8,7 +8,7 @@
}
<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
</a>
</div>
@@ -48,7 +48,7 @@
<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>
<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
</button>
</div>
@@ -1,14 +1,14 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model BudgetCreateVm
@{
ViewData["Title"] = $"Edit Budget — {Model.Name}";
ViewData["Title"] = $"Edit Budget {Model.Name}";
ViewData["PageIcon"] = "bi-pencil";
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">
<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
</a>
<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-header bg-white border-0 py-3">
<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>
</div>
<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">
<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">
<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
</button>
</div>
@@ -91,7 +91,7 @@
</div>
<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
</a>
@if (!(bool)(ViewBag.AiPriceCheckEnabled ?? true))
@@ -215,7 +215,7 @@
<div id="categoryModalError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveCategoryBtn">
<i class="bi bi-check-circle me-1"></i>Create Category
</button>
@@ -238,7 +238,7 @@
<div id="categoryModalError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveCategoryBtn">
<i class="bi bi-check-circle me-1"></i>Create Category
</button>
@@ -184,7 +184,7 @@
</div>
<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">
<i class="bi bi-save me-1"></i>Create Company
</button>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Company.CompanyDto
@model PowderCoating.Application.DTOs.Company.CompanyDto
@{
ViewData["Title"] = Model.CompanyName;
@@ -470,7 +470,7 @@
<i class="bi bi-fire me-1"></i>Reset All Company Data
</h6>
<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.
Use this to wipe a migration and start fresh.
</p>
@@ -491,7 +491,7 @@
There is no going back.
@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>
</div>
@@ -523,7 +523,7 @@
<!-- Loading spinner -->
<div id="oc-loading" class="d-flex justify-content-center align-items-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading…</span>
<span class="visually-hidden">Loading</span>
</div>
</div>
@@ -545,14 +545,14 @@
</div>
<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 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">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">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">Email confirmed</th><td id="oc-emailconf">—</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">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">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">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>
</table>
</div>
@@ -625,7 +625,7 @@
@Html.AntiForgeryToken()
<input type="hidden" name="confirmation" id="resetDataConfirmHidden" />
<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>
<i class="bi bi-fire me-1"></i>Reset All Data
</button>
@@ -656,7 +656,7 @@
@Html.AntiForgeryToken()
<input type="hidden" name="confirmation" id="hardDeleteConfirmHidden" />
<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>
<i class="bi bi-trash me-1"></i>Permanently Delete Company
</button>
@@ -694,7 +694,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-key me-2"></i>Reset Password
</button>
@@ -706,7 +706,7 @@
@section Scripts {
<script>
// Reset Data Modal — enable submit only when user types "DELETE"
// Reset Data Modal enable submit only when user types "DELETE"
(function () {
var input = document.getElementById('resetDataConfirmInput');
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 () {
var input = document.getElementById('hardDeleteConfirmInput');
var hidden = document.getElementById('hardDeleteConfirmHidden');
@@ -760,7 +760,7 @@
});
}
// ── User Details Modal ────────────────────────────────────────────────
// ── User Details Modal ────────────────────────────────────────────────
(function () {
const offcanvasEl = document.getElementById('userDetailOffcanvas');
const oc = new bootstrap.Modal(offcanvasEl);
@@ -806,12 +806,12 @@
badge.textContent = u.isActive ? 'Active' : 'Inactive';
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-dept').textContent = u.department || '—';
document.getElementById('oc-position').textContent = u.position || '—';
document.getElementById('oc-phone').textContent = u.phone || '—';
document.getElementById('oc-hire').textContent = u.hireDate || '—';
document.getElementById('oc-created').textContent = u.createdAt || '—';
document.getElementById('oc-role').textContent = u.companyRole ? u.companyRole.replace('Company', '') : '';
document.getElementById('oc-dept').textContent = u.department || '';
document.getElementById('oc-position').textContent = u.position || '';
document.getElementById('oc-phone').textContent = u.phone || '';
document.getElementById('oc-hire').textContent = u.hireDate || '';
document.getElementById('oc-created').textContent = u.createdAt || '';
document.getElementById('oc-lastlogin').textContent = u.lastLoginDate || 'Never';
document.getElementById('oc-emailconf').innerHTML = u.emailConfirmed
? '<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>Yes</span>'
@@ -160,7 +160,7 @@
</div>
<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">
<i class="bi bi-save me-1"></i>Save Changes
</button>
@@ -1,4 +1,4 @@
@model List<PowderCoating.Application.DTOs.Company.CompanyListDto>
@model List<PowderCoating.Application.DTOs.Company.CompanyListDto>
@section Styles {
<style>
@@ -58,7 +58,7 @@
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="searchTerm" class="form-control"
placeholder="Search by name, code, email, phone…"
placeholder="Search by name, code, email, phone"
value="@searchTerm" />
</div>
</div>
@@ -230,7 +230,7 @@
</table>
</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-list">
@foreach (var company in Model)
@@ -274,7 +274,7 @@
}
</div>
<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>
</a>
}
@@ -286,7 +286,7 @@
{
<div class="card-footer d-flex justify-content-between align-items-center">
<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 class="d-flex align-items-center gap-3">
<div>
@@ -413,7 +413,7 @@
<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">
<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.
</p>
<div class="alert alert-danger alert-permanent py-2 mb-3">
@@ -439,7 +439,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
@@ -777,7 +777,7 @@
<div id="ovenErrorMsg" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveOven()">Save</button>
</div>
</div>
@@ -71,7 +71,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveJobStatusBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
@@ -121,7 +121,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveJobPriorityBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
@@ -195,7 +195,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveQuoteStatusBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
@@ -242,7 +242,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveInventoryCategoryBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
@@ -335,7 +335,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveAppointmentTypeBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
@@ -387,7 +387,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="savePrepServiceBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
@@ -490,7 +490,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveBlastSetupBtn">
<i class="bi bi-check-circle me-1"></i>Save
</button>
@@ -201,7 +201,7 @@
</div>
<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">
<i class="bi bi-save me-1"></i>Create User
</button>
@@ -221,11 +221,11 @@
@if (!string.IsNullOrEmpty(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
{
<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">
<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";
@@ -258,7 +258,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger"><i class="bi bi-slash-circle"></i> Ban User</button>
</div>
</form>
@@ -7,7 +7,7 @@
<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>
<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
</a>
</div>
@@ -59,7 +59,7 @@
<i class="bi bi-x-circle me-1"></i>Void
</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
</a>
</div>
@@ -247,14 +247,14 @@
@if (openInvoices.Any())
{
<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>
</div>
}
else
{
<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>
}
</form>
@@ -293,7 +293,7 @@
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Void Credit Memo</button>
</div>
</form>
@@ -10,7 +10,7 @@
<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>
<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
</a>
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
@using Microsoft.AspNetCore.Html
@using PowderCoating.Application.DTOs.Health
@using PowderCoating.Web.ViewModels.Dashboard
@@ -24,7 +24,7 @@
<p class="mb-3" style="font-family:var(--font-display);font-size:1.35rem;font-weight:500;line-height:1.4;color:var(--pcl-ink);">
@if (_attnCount > 0)
{
<span>Shop is </span><span style="color:var(--pcl-bad);">running hot</span><span> — @_attnCount item@(_attnCount == 1 ? "" : "s") need attention.</span>
<span>Shop is </span><span style="color:var(--pcl-bad);">running hot</span><span> @_attnCount item@(_attnCount == 1 ? "" : "s") need attention.</span>
}
else
{
@@ -59,7 +59,7 @@
</div>
</div>
@* PWA install banner — rendered by JS only on mobile, hidden once dismissed or already installed *@
@* PWA install banner rendered by JS only on mobile, hidden once dismissed or already installed *@
<div id="pwa-install-banner" class="row mb-4" style="display:none!important;">
<div class="col-12">
<div class="alert alert-permanent mb-0 d-flex align-items-start gap-3 py-3"
@@ -104,7 +104,7 @@
@await Html.PartialAsync("_ShopProgressWidget", shopProgressWidget)
}
@* Config health alert — only shown when there are setup gaps *@
@* Config health alert only shown when there are setup gaps *@
@if (configHealth != null && !configHealth.IsHealthy)
{
<div class="row mb-4">
@@ -406,15 +406,15 @@
}
@if (Model.AgingDays1To30 > 0)
{
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-warn);"></span>1–30d @Model.AgingDays1To30.ToString("C0")</span>
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-warn);"></span>130d @Model.AgingDays1To30.ToString("C0")</span>
}
@if (Model.AgingDays31To60 > 0)
{
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>31–60d @Model.AgingDays31To60.ToString("C0")</span>
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>3160d @Model.AgingDays31To60.ToString("C0")</span>
}
@if (Model.AgingDays61To90 > 0)
{
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>61–90d @Model.AgingDays61To90.ToString("C0")</span>
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>6190d @Model.AgingDays61To90.ToString("C0")</span>
}
@if (Model.AgingDaysOver90 > 0)
{
@@ -536,7 +536,7 @@
@if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>}
else
{<span class="text-muted">—</span>}
{<span class="text-muted"></span>}
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-danger mark-ordered-btn text-nowrap"
@@ -552,7 +552,7 @@
<tr>
<td colspan="2">Vendor Total</td>
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "")</td>
<td></td>
</tr>
</tfoot>
@@ -571,7 +571,7 @@
<div class="card border-0 shadow-sm dashboard-card" style="border-left: 4px solid #0d6efd !important;">
<div class="card-header bg-body border-0 d-flex justify-content-between align-items-center pt-4 pb-3">
<h5 class="mb-0 fw-bold">
<i class="bi bi-box-arrow-in-down me-2 text-muted"></i>Powder Ordered — Awaiting Receipt
<i class="bi bi-box-arrow-in-down me-2 text-muted"></i>Powder Ordered Awaiting Receipt
<span class="ms-2 text-muted fw-normal small" id="placed-count-label">@Model.PowderOrdersPlacedCount item@(Model.PowderOrdersPlacedCount == 1 ? "" : "s")</span>
</h5>
<small class="text-muted">Grouped by vendor &middot; Enter lbs received to update inventory</small>
@@ -630,13 +630,13 @@
@if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>}
else
{<span class="text-muted">—</span>}
{<span class="text-muted"></span>}
</td>
<td class="text-muted small">
@if (line.OrderedAt.HasValue)
{<span title="@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("g")">@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d")</span>}
else
{<span>—</span>}
{<span></span>}
</td>
<td class="text-center">
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-@line.CoatId">
@@ -669,7 +669,7 @@
<tr>
<td colspan="2">Vendor Total</td>
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "")</td>
<td colspan="2"></td>
</tr>
</tfoot>
@@ -739,7 +739,7 @@
<div class="col-md-6">
<label class="form-label fw-medium">Category</label>
<select class="form-select" id="apm-categoryId" name="inventoryCategoryId">
<option value="">— Select category —</option>
<option value=""> Select category </option>
@if (ViewBag.InventoryCategories != null)
{
foreach (var cat in (IEnumerable<dynamic>)ViewBag.InventoryCategories)
@@ -753,7 +753,7 @@
<div class="col-md-6">
<label class="form-label fw-medium">Primary Vendor</label>
<select class="form-select" id="apm-vendorId" name="primaryVendorId">
<option value="">— Select vendor —</option>
<option value=""> Select vendor </option>
@if (ViewBag.VendorList != null)
{
foreach (var v in (IEnumerable<dynamic>)ViewBag.VendorList)
@@ -814,7 +814,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" id="apm-saveBtn">
<i class="bi bi-plus-circle me-1"></i>Add to Inventory
</button>
@@ -883,7 +883,7 @@
const esc = s => s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : '';
const estCost = (c.lbsToOrder && c.costPerLb) ? (c.lbsToOrder * c.costPerLb) : null;
const orderedDate = c.orderedAt ? new Date(c.orderedAt).toLocaleDateString('en-US',{month:'short',day:'numeric'}) : '—';
const orderedDate = c.orderedAt ? new Date(c.orderedAt).toLocaleDateString('en-US',{month:'short',day:'numeric'}) : '';
const lbsFmt = c.lbsToOrder ? parseFloat(c.lbsToOrder).toFixed(2) : '0.00';
// Find or create vendor group
@@ -928,7 +928,7 @@
${c.finish ? `<span class="badge bg-light text-dark border ms-1">${esc(c.finish)}</span>` : ''}
</td>
<td class="text-end fw-medium">${lbsFmt} lbs</td>
<td class="text-end">${estCost ? '$' + estCost.toFixed(2) : '<span class="text-muted">—</span>'}</td>
<td class="text-end">${estCost ? '$' + estCost.toFixed(2) : '<span class="text-muted"></span>'}</td>
<td class="text-muted small">${orderedDate}</td>
<td class="text-center">
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-${c.coatId}">
@@ -979,7 +979,7 @@
}
qtyInput.classList.remove('is-invalid');
// Custom powder (no inventory item) → open modal to add to inventory
// Custom powder (no inventory item) → open modal to add to inventory
if (!hasInv) {
const modal = document.getElementById('addPowderModal');
// Pre-fill hidden + text fields
@@ -1024,7 +1024,7 @@
return;
}
// Inventory item exists → receive directly
// Inventory item exists → receive directly
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
@@ -1065,7 +1065,7 @@
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
saveBtn.disabled = true;
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving…';
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving';
try {
const resp = await fetch('@Url.Action("AddCustomPowderToInventory", "Dashboard")', {
@@ -1094,7 +1094,7 @@
}
});
// ── AI Lookup for Add Powder modal ───────────────────────────────────────
// ── AI Lookup for Add Powder modal ───────────────────────────────────────
(function () {
const apmBtn = document.getElementById('apm-ai-btn');
const apmStatusEl = document.getElementById('apm-ai-status');
@@ -1144,7 +1144,7 @@
const hasInput = manufacturer || colorName || colorCode || partNumber || itemName;
if (!hasInput) {
apmShowStatus('warning', '<i class="bi bi-exclamation-triangle me-1"></i>Fill in at least one field — Manufacturer, Color Name, Color Code, or Item Name — then try again.');
apmShowStatus('warning', '<i class="bi bi-exclamation-triangle me-1"></i>Fill in at least one field Manufacturer, Color Name, Color Code, or Item Name then try again.');
return;
}
@@ -1153,7 +1153,7 @@
document.getElementById('apm-bad-match-btn')?.remove();
apmBtn.disabled = true;
apmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Looking up...';
apmShowStatus('info', '<i class="bi bi-hourglass-split me-1"></i>Searching for product specifications…');
apmShowStatus('info', '<i class="bi bi-hourglass-split me-1"></i>Searching for product specifications');
try {
const formData = new FormData();
@@ -1220,7 +1220,7 @@
: '';
apmShowStatus('success', `<i class="bi bi-check-circle me-1"></i>Auto-filled: ${filled.join(', ')}.${reasoning}`);
} else {
apmShowStatus('warning', '<i class="bi bi-info-circle me-1"></i>No new fields to fill — they may already be populated, or the product wasn\'t found.');
apmShowStatus('warning', '<i class="bi bi-info-circle me-1"></i>No new fields to fill they may already be populated, or the product wasn\'t found.');
}
} catch (err) {
@@ -1274,7 +1274,7 @@
(function () {
var DISMISSED_KEY = 'pcl_pwa_banner_dismissed';
// Already installed as standalone — never show
// Already installed as standalone never show
var isStandalone = window.navigator.standalone === true ||
window.matchMedia('(display-mode: standalone)').matches;
if (isStandalone) return;
@@ -1298,7 +1298,7 @@
var isSafari = /webkit/i.test(ua) && !/crios|chrome|fxios|opios/i.test(ua);
if (isSafari) {
titleEl.textContent = 'Add to Home Screen';
msgEl.innerHTML = 'For the best experience — and so the camera only asks once — open the ' +
msgEl.innerHTML = 'For the best experience and so the camera only asks once open the ' +
'<strong>Share menu</strong> <span style="font-size:1.1em">&#9650;</span> at the bottom of Safari ' +
'and tap <strong>Add to Home Screen</strong>.';
} else {
@@ -1,4 +1,4 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model List<EntityPurgeStat>
@{
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">
<i class="bi bi-exclamation-triangle-fill fs-4 flex-shrink-0 mt-1"></i>
<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.
Job photo blobs in Azure Storage are also deleted when purging job photo records.
</div>
@@ -83,8 +83,8 @@
<th style="width:36px"></th>
<th>Entity</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">30–90d</th>
<th class="text-end" style="width:100px">030d</th>
<th class="text-end" style="width:100px">3090d</th>
<th class="text-end" style="width:100px">&gt;90d</th>
<th style="width:130px">Oldest</th>
<th style="width:42px">
@@ -109,7 +109,7 @@
}
else
{
<span class="text-muted">—</span>
<span class="text-muted"></span>
}
</td>
<td class="text-end">
@@ -117,24 +117,24 @@
{
<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 class="text-end">
@if (s.Deleted30To90Days > 0)
{
<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 class="text-end">
@if (s.DeletedOlderThan90Days > 0)
{
<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 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 class="text-center">
<input type="checkbox" class="form-check-input entity-select"
@@ -149,7 +149,7 @@
</table>
</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="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>
@@ -164,7 +164,7 @@
</div>
<div class="mobile-card-title">
<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 class="mobile-card-body">
@@ -175,11 +175,11 @@
{
<span class="badge bg-secondary">@s.Total</span>
}
else { <span class="text-muted">—</span> }
else { <span class="text-muted"></span> }
</span>
</div>
<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>
</div>
</div>
@@ -279,7 +279,7 @@
<p class="mb-0 text-muted">Are you sure you want to continue?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmPurgeBtn">
<i class="bi bi-trash3-fill me-2"></i>Yes, Purge Permanently
</button>
@@ -300,7 +300,7 @@
const confirmModal = new bootstrap.Modal(document.getElementById('confirmModal'));
const confirmSummary= document.getElementById('confirmSummary');
// ── Select all ──────────────────────────────────────────────────────────
// ── Select all ──────────────────────────────────────────────────────────
selectAll.addEventListener('change', () => {
document.querySelectorAll('.entity-select:not(:disabled)').forEach(cb => {
cb.checked = selectAll.checked;
@@ -308,7 +308,7 @@
updatePurgeBtn();
});
// ── Group select all ────────────────────────────────────────────────────
// ── Group select all ────────────────────────────────────────────────────
document.querySelectorAll('.group-select-all').forEach(ga => {
ga.addEventListener('change', () => {
document.querySelectorAll(`.entity-select[data-group="${ga.dataset.group}"]:not(:disabled)`)
@@ -335,7 +335,7 @@
previewRes.classList.add('d-none');
}
// ── Preview ─────────────────────────────────────────────────────────────
// ── Preview ─────────────────────────────────────────────────────────────
previewBtn.addEventListener('click', async () => {
const entities = getSelectedEntities();
if (!entities.length) {
@@ -344,7 +344,7 @@
}
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 token = document.querySelector('input[name="__RequestVerificationToken"]').value;
@@ -379,7 +379,7 @@
}
});
// ── Purge button → modal ────────────────────────────────────────────────
// ── Purge button modal ────────────────────────────────────────────────
purgeBtn.addEventListener('click', () => {
const entities = getSelectedEntities();
const days = document.getElementById('olderThanDays').value;
@@ -390,7 +390,7 @@
confirmModal.show();
});
// ── Confirm → submit form ───────────────────────────────────────────────
// ── Confirm submit form ───────────────────────────────────────────────
document.getElementById('confirmPurgeBtn').addEventListener('click', () => {
const entities = getSelectedEntities();
const days = document.getElementById('olderThanDays').value;
@@ -8,7 +8,7 @@
<div class="row">
<div class="col-12">
<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
</a>
</div>
@@ -183,7 +183,7 @@
class="btn btn-outline-secondary btn-sm">
<i class="bi bi-box-arrow-up-right me-1"></i>Open in New Tab
</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>
@@ -7,7 +7,7 @@
<div class="col-lg-8">
<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
</a>
</div>
@@ -14,7 +14,7 @@
}
<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
</a>
<div class="d-flex gap-2">
@@ -9,7 +9,7 @@
<div class="col-lg-8">
<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
</a>
</div>
@@ -40,7 +40,7 @@
<a asp-controller="Dashboard" asp-action="Index" class="btn btn-primary">
<i class="bi bi-house-door"></i> Go to Dashboard
</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
</button>
</div>
@@ -606,7 +606,7 @@
</div>
</div>
<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>
@@ -633,7 +633,7 @@
</div>
</div>
<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>
@@ -37,7 +37,7 @@
</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
</a>
</div>
@@ -80,7 +80,7 @@
</div>
<div class="d-flex gap-2">
<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>
</form>
@@ -439,7 +439,7 @@
<div id="gcModalError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-warning" onclick="addGiftCertLineItem(this)">
<i class="bi bi-plus-circle me-1"></i>Add to Invoice
</button>
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.Invoice
@using PowderCoating.Application.DTOs.Invoice
@using PowderCoating.Core.Enums
@using PowderCoating.Web.Controllers
@model InvoiceDto
@@ -69,7 +69,7 @@
<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>
<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>.
</span>
</div>
@@ -168,12 +168,12 @@
<div class="col-md-4">
<label class="text-muted small mb-1">Due Date</label>
<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>
</div>
<div class="col-md-4">
<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>
@if (!string.IsNullOrWhiteSpace(Model.CustomerPO))
{
@@ -339,7 +339,7 @@
</span>
</td>
<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 class="text-end fw-semibold">@gcItem.TotalPrice.ToString("C")</td>
<td>
@@ -385,7 +385,7 @@
<tr>
<td>@p.PaymentDate.ToString("MM/dd/yyyy")</td>
<td>@p.PaymentMethodDisplay</td>
<td>@(p.Reference ?? "—")</td>
<td>@(p.Reference ?? "")</td>
<td>
@if (!string.IsNullOrEmpty(p.DepositAccountName))
{
@@ -393,10 +393,10 @@
}
else
{
<span class="text-muted">—</span>
<span class="text-muted"></span>
}
</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">
@if (!isVoided)
@@ -452,7 +452,7 @@
<td>@r.RefundDate.ToString("MM/dd/yyyy")</td>
<td>@r.RefundMethodDisplay</td>
<td>@r.Reason</td>
<td>@(r.Reference ?? "—")</td>
<td>@(r.Reference ?? "")</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-nowrap">
@@ -564,7 +564,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
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>
</a>
</div>
@@ -761,7 +761,7 @@
{
<div class="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>
<div class="input-group input-group-sm">
<input type="text" id="paymentLinkInput" class="form-control font-monospace"
@@ -899,7 +899,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
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>
</a>
</div>
@@ -1054,7 +1054,7 @@
</div>
</div>
<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>
@@ -1095,7 +1095,7 @@
</div>
</div>
<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>
@@ -1118,7 +1118,7 @@
</div>
<div id="refundAlertCredit" class="alert alert-success small mb-3 d-none">
<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 class="mb-3">
<label class="form-label fw-semibold">Amount <span class="text-danger">*</span></label>
@@ -1240,7 +1240,7 @@
<div class="mb-3">
<label class="form-label fw-semibold">Select Credit Memo <span class="text-danger">*</span></label>
<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)
{
<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"
max="@Model.BalanceDue.ToString("F2")" value="@Model.BalanceDue.ToString("F2")" required />
</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 class="modal-footer">
@@ -1336,7 +1336,7 @@
<div class="mb-3">
<label class="form-label">Bad Debt Expense Account</label>
<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)
{
@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));
document.getElementById('gcAmountInput').value = max.toFixed(2);
document.getElementById('gcAmountInput').max = max;
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>`;
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>`;
}
} catch { result.innerHTML = '<div class="alert alert-danger py-1 mb-0 small">Lookup failed.</div>'; }
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"><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">${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>
</tr>${errorRow}`;
}).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
@{
@@ -19,7 +19,7 @@
<i class="bi bi-layout-text-window-reverse fs-5"></i>
<div>
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>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
</div>
@@ -50,7 +50,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</div>
@@ -70,7 +70,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</div>
@@ -100,7 +100,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</div>
@@ -130,7 +130,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</div>
@@ -201,7 +201,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</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 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<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>
</p>
<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>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></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 id="cylinderInputs" style="display:none">
<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>
<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()">
<i class="bi bi-check-circle me-1"></i>Use This Value
</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}";
@@ -57,7 +57,7 @@
}
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 class="d-flex gap-2 flex-wrap">
@@ -217,7 +217,7 @@
</button>
</div>
<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>
@@ -263,7 +263,7 @@
<i class="bi bi-x-circle me-1"></i><small>Clear date</small>
</button>
<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>
@@ -273,7 +273,7 @@
<div class="d-flex align-items-center gap-2">
<select id="workerAssignmentSelect" class="form-select form-select-sm"
onchange="updateWorkerAssignment(this)">
<option value=""> Unassigned </option>
<option value=""> Unassigned </option>
@foreach (var w in (IEnumerable<SelectListItem>)ViewBag.Workers)
{
if (w.Value == Model.AssignedUserId)
@@ -287,7 +287,7 @@
}
</select>
<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 id="workerSavedTick" class="text-success small d-none">
<i class="bi bi-check-circle-fill"></i>
@@ -321,7 +321,7 @@
<div class="card-body">
@* ── Catalog Products ── *@
@* ── Catalog Products ── *@
@if (catalogItems.Any())
{
<h6 class="text-primary mb-3"><i class="bi bi-bag-check me-2"></i>Catalog Products</h6>
@@ -351,10 +351,10 @@
{
<br />
<small class="ms-3">
<strong>@coat.CoatName</strong>
<strong>@coat.CoatName</strong>
@if (!string.IsNullOrEmpty(coat.ColorName))
{
<text> @coat.ColorName</text>
<text> @coat.ColorName</text>
@if (!string.IsNullOrEmpty(coat.VendorName))
{
<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>
@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))
@@ -390,7 +390,7 @@
@foreach (var ps in item.PrepServices)
{
<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))
@@ -414,7 +414,7 @@
</div>
}
@* ── Custom Work ── *@
@* ── Custom Work ── *@
@if (customItems.Any())
{
<h6 class="text-success mb-3"><i class="bi bi-calculator me-2"></i>Custom Work</h6>
@@ -478,10 +478,10 @@
{
<br />
<small class="ms-3">
<strong>@coat.CoatName</strong>
<strong>@coat.CoatName</strong>
@if (!string.IsNullOrEmpty(coat.ColorName))
{
<text> @coat.ColorName</text>
<text> @coat.ColorName</text>
@if (!string.IsNullOrEmpty(coat.VendorName))
{
<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>
@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))
@@ -517,7 +517,7 @@
@foreach (var ps in item.PrepServices)
{
<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))
@@ -532,7 +532,7 @@
<text>@item.SurfaceAreaSqFt.ToString("F2") @ViewBag.AreaUnit</text>
<br /><small class="text-muted">per item</small>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted"></span> }
</td>
<td class="text-center">
@if (item.EstimatedMinutes > 0)
@@ -540,7 +540,7 @@
<text>@item.EstimatedMinutes min</text>
<br /><small class="text-muted">per item</small>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted"></span> }
</td>
<td class="text-center">
@if (totalPowderNeeded > 0)
@@ -548,7 +548,7 @@
<strong class="text-success">@totalPowderNeeded.ToString("F2") lbs</strong>
<br /><small class="text-muted">total batch</small>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted"></span> }
</td>
<td class="text-end">@item.UnitPrice.ToString("C")</td>
<td class="text-end fw-semibold">@item.TotalPrice.ToString("C")</td>
@@ -565,7 +565,7 @@
</div>
}
@* ── Labor ── *@
@* ── Labor ── *@
@if (laborItems.Any())
{
<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>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted"></span> }
</td>
<td class="text-end">@item.UnitPrice.ToString("C")</td>
<td class="text-end fw-semibold">@item.TotalPrice.ToString("C")</td>
@@ -616,7 +616,7 @@
</div>
}
@* ── Mobile cards ── *@
@* ── Mobile cards ── *@
<div class="d-lg-none mt-2">
@foreach (var item in Model.Items)
{
@@ -653,7 +653,7 @@
<span class="mobile-card-value">
@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>
</div>
@@ -704,7 +704,7 @@
<i class="bi bi-chevron-down collapse-chevron ms-1" style="transition:transform .2s;"></i>
</div>
<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 estimatedHrs = estimatedMins / 60m;
@@ -741,7 +741,7 @@
<tfoot class="table-light fw-semibold">
<tr>
<td colspan="3">Total</td>
<td class="text-end" id="timeEntriesTotalHours"></td>
<td class="text-end" id="timeEntriesTotalHours"></td>
<td colspan="3"></td>
</tr>
</tfoot>
@@ -1099,7 +1099,7 @@
<div class="modal-content">
<div class="modal-header">
<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>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
@@ -1117,7 +1117,7 @@
value="@(Model.IntakePartCount.HasValue ? Model.IntakePartCount.Value.ToString() : "")"
placeholder="@intakeExpectedCount" />
<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 class="mb-3">
@@ -1138,7 +1138,7 @@
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="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")
</button>
@@ -1195,7 +1195,7 @@
<div id="depositFormError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success" id="saveDepositBtn">
<i class="bi bi-check-circle me-1"></i>Save & Generate Receipt
</button>
@@ -1310,7 +1310,7 @@
<a asp-action="Intake" asp-route-id="@Model.Id"
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")">
<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>
}
@{
@@ -1368,7 +1368,7 @@
</div>
</div>
<!-- Pricing Summary (internal d-print-none) -->
<!-- Pricing Summary (internal d-print-none) -->
@{
var jobPb = ViewBag.JobPricingBreakdown as PowderCoating.Application.DTOs.Quote.QuotePricingBreakdownDto;
}
@@ -1400,7 +1400,7 @@
@if (jobPb.OvenBatchCost > 0)
{
<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>
</div>
}
@@ -1518,7 +1518,7 @@
}
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
{
@@ -1547,7 +1547,7 @@
@if (jobPb.FacilityOverheadCost > 0)
{
<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>
</div>
}
@@ -1712,11 +1712,11 @@
<div class="px-3 pt-3 pb-2">
<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="fw-semibold" id="costingRevenue"></span>
<span class="fw-semibold" id="costingRevenue"></span>
</div>
<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 id="costingPowder"></span>
<span id="costingPowder"></span>
</div>
<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;">
@@ -1725,7 +1725,7 @@
</div>
<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 id="costingLabor"></span>
<span id="costingLabor"></span>
</div>
<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;">
@@ -1734,12 +1734,12 @@
</div>
<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 id="costingOven"></span>
<span id="costingOven"></span>
</div>
<div id="costingReworkSection" style="display:none;">
<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 id="costingRework"></span>
<span id="costingRework"></span>
</div>
<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;">
@@ -1748,25 +1748,25 @@
</div>
<div class="d-flex justify-content-between small text-success mb-1 ps-2">
<span>Billed to Customer</span>
<span id="costingReworkBilled"></span>
<span id="costingReworkBilled"></span>
</div>
</div>
<hr class="my-2" />
<div class="d-flex justify-content-between small mb-1 ps-2">
<span class="text-muted">Total Costs</span>
<span id="costingTotal" class="text-danger"></span>
<span id="costingTotal" class="text-danger"></span>
</div>
<div class="d-flex justify-content-between fw-bold mb-1">
<span>Gross Profit</span>
<span id="costingProfit"></span>
<span id="costingProfit"></span>
</div>
<div class="d-flex justify-content-between small text-muted mb-1">
<span>Gross Margin</span>
<span id="costingMargin"></span>
<span id="costingMargin"></span>
</div>
<div class="d-flex justify-content-between small text-muted">
<span>Margin vs Quote</span>
<span id="costingQuotedMargin"></span>
<span id="costingQuotedMargin"></span>
</div>
</div>
<div id="costingNotes" class="px-3 pb-3" style="font-size:0.75rem;"></div>
@@ -1869,7 +1869,7 @@
</div>
<div class="mb-3">
<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>
<input type="hidden" id="photoTagsHidden" name="tags" />
<div id="photoTagsContainer"></div>
@@ -1895,7 +1895,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="jobPhotoModule.uploadPhoto()">
<i class="bi bi-upload me-1"></i>Upload
</button>
@@ -1948,7 +1948,7 @@
<textarea class="form-control" id="editPhotoCaption" rows="2" placeholder="Add a description or note..."></textarea>
</div>
<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" />
<div id="editPhotoTagsContainer"></div>
</div>
@@ -1962,10 +1962,10 @@
<button type="button" class="btn btn-danger" onclick="jobPhotoModule.deletePhoto()">
<i class="bi bi-trash me-1"></i>Delete
</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 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()">
<i class="bi bi-check-lg me-1"></i>Save Changes
</button>
@@ -2000,7 +2000,7 @@
<div class="mb-2">
<label class="form-label fw-semibold" for="smsMessageText">Message</label>
<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 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.
@@ -2012,7 +2012,7 @@
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" id="smsDismissBtn">
Skip don't send
Skip don't send
</button>
<button type="button" class="btn btn-info text-white" id="smsSendBtn">
<i class="bi bi-send me-1"></i>Send SMS
@@ -2059,7 +2059,7 @@
<p class="mb-0 small" id="deleteConfirmItemName"></p>
</div>
<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">
<i class="bi bi-trash me-1"></i>Delete
</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>
<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()">
<i class="bi bi-check-circle me-1"></i>Use This Value
</button>
@@ -2223,7 +2223,7 @@
<div class="col-md-6">
<label class="form-label">Specific Item (optional)</label>
<select class="form-select" id="rwJobItem">
<option value=""> Whole Job </option>
<option value=""> Whole Job </option>
@if (Model.Items != null)
{
@foreach (var item in Model.Items)
@@ -2285,9 +2285,9 @@
<div class="col-md-6">
<label class="form-label">Resolution</label>
<select class="form-select" id="rwResolution">
<option value=""> Pending </option>
<option value="0">Recoated No Charge</option>
<option value="1">Recoated Billed to Customer</option>
<option value=""> Pending </option>
<option value="0">Recoated No Charge</option>
<option value="1">Recoated Billed to Customer</option>
<option value="2">Customer Credited</option>
<option value="3">Written Off</option>
<option value="4">No Action Required</option>
@@ -2324,7 +2324,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-warning" id="reworkSaveBtn" onclick="rework.save()">
<i class="bi bi-floppy me-1"></i>Save
</button>
@@ -2346,7 +2346,7 @@
<div class="mb-3">
<label class="form-label fw-semibold">Worker <span class="text-danger">*</span></label>
<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> ?? []))
{
<option value="@w.Id">@w.Name</option>
@@ -2365,7 +2365,7 @@
</div>
<div class="mb-3">
<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">
<option value="Sandblasting"></option>
<option value="Masking & Taping"></option>
@@ -2380,7 +2380,7 @@
</div>
<div class="mb-3">
<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 class="text-danger small d-none" id="teError"></div>
</div>
@@ -2418,7 +2418,7 @@
<script src="~/js/job-photos.js" asp-append-version="true"></script>
<script src="~/js/customer-change.js" asp-append-version="true"></script>
<script>
// ── Inline date editing ──────────────────────────────────────────────
// ── Inline date editing ──────────────────────────────────────────────
const jobId = @Model.Id;
const antiForgeryToken = () => document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
@@ -2544,7 +2544,7 @@
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;
// Wrap wizardSave to set a flag before the modal hides
@@ -2562,12 +2562,12 @@
}
});
// ── Delete confirmation modal ─────────────────────────────────────
// ── Delete confirmation modal ─────────────────────────────────────
let pendingDeleteItemId = -1;
const deleteModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
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) {
const btn = e.target.closest('[data-delete-id]');
if (!btn) return;
@@ -2600,7 +2600,7 @@
});
</script>
<!-- ── Rework / Warranty ────────────────────────────────────────────── -->
<!-- ── Rework / Warranty ────────────────────────────────────────────── -->
<script>
const rework = (() => {
const jid = @Model.Id;
@@ -2645,12 +2645,12 @@
</div>
<div class="small mt-1 text-muted">${r.defectDescription}</div>
<div class="small text-muted mt-1">
Found: ${r.discoveredByDisplay} ${new Date(r.discoveredDate).toLocaleDateString()}
${r.reportedByName ? ' ' + r.reportedByName : ''}
Found: ${r.discoveredByDisplay} ${new Date(r.discoveredDate).toLocaleDateString()}
${r.reportedByName ? ' ' + r.reportedByName : ''}
${r.jobItemDescription ? ' | Item: ' + r.jobItemDescription : ''}
</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('');
}
@@ -2756,7 +2756,7 @@
})();
</script>
<!-- ── Job Costing ──────────────────────────────────────────────────── -->
<!-- ── Job Costing ──────────────────────────────────────────────────── -->
<script>
const costing = (() => {
const jid = @Model.Id;
@@ -2796,7 +2796,7 @@
document.getElementById('costingReworkBilled').textContent = fmt(d.reworkBilledToCustomer);
const rBody = document.getElementById('reworkCostLines');
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 fw-semibold">${fmt(l.cost)}</td></tr>`).join('');
} else {
@@ -2812,14 +2812,14 @@
document.getElementById('costingMargin').textContent = `${d.grossMargin}%`;
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
const pBody = document.getElementById('powderLines');
pBody.innerHTML = d.hasPowderData
? 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-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('')
: '<tr><td colspan="3" class="text-muted">No powder cost data on coats.</td></tr>';
@@ -2827,16 +2827,16 @@
const lBody = document.getElementById('laborLines');
lBody.innerHTML = d.hasLaborData
? d.laborLines.map(l => `<tr>
<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-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 fw-semibold">${fmt(l.total)}</td></tr>`).join('')
: '<tr><td colspan="3" class="text-muted">No time entries logged yet.</td></tr>';
// 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.');
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.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.');
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.');
document.getElementById('costingNotes').innerHTML = notes.map(n => `<div class="text-muted">${n}</div>`).join('');
@@ -2865,7 +2865,7 @@
})();
</script>
<!-- ── Time Tracking ─────────────────────────────────────────────────── -->
<!-- ── Time Tracking ─────────────────────────────────────────────────── -->
<script>
const timeTracking = (() => {
const jid = @Model.Id;
@@ -2873,7 +2873,7 @@
const modal = new bootstrap.Modal(document.getElementById('timeEntryModal'));
let entries = [];
// ── Load ──────────────────────────────────────────────────────────
// ── Load ──────────────────────────────────────────────────────────
async function load() {
const r = await fetch(`/Jobs/GetTimeEntries?jobId=${jid}`);
entries = await r.json();
@@ -2904,7 +2904,7 @@
<td class="fw-semibold">${esc(e.workerName)}</td>
<td class="small">${d}</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="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>
@@ -2916,12 +2916,12 @@
}
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('timeEntriesTotalHours').textContent = total > 0 ? total.toFixed(2) : '';
document.getElementById('timeEntriesTotalHours').textContent = total > 0 ? total.toFixed(2) : '';
}
// ── Modal helpers ─────────────────────────────────────────────────
// ── Modal helpers ─────────────────────────────────────────────────
function openAdd() {
document.getElementById('timeEntryModalTitle').textContent = 'Log Time';
document.getElementById('teEntryId').value = '0';
@@ -3027,7 +3027,7 @@
}
});
// ── Deposits ─────────────────────────────────────────────────────────────
// ── Deposits ─────────────────────────────────────────────────────────────
// Note: antiForgeryToken() is already defined above in this script block
document.getElementById('addDepositForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
@@ -3041,7 +3041,7 @@
}
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));
@@ -3083,7 +3083,7 @@
}
}
// ── Collapsible sections ──────────────────────────────────────────────────
// ── Collapsible sections ──────────────────────────────────────────────────
(function () {
const storageKey = 'jobDetailCollapse_@Model.Id';
const sections = ['collapseTimeTracking', 'collapsePartIntake', 'collapsePhotos', 'collapseDeposits', 'collapseMaterials'];
@@ -3122,7 +3122,7 @@
});
})();
// ── Part Intake Modal ─────────────────────────────────────────────────────
// ── Part Intake Modal ─────────────────────────────────────────────────────
(function () {
const expectedCount = @intakeExpectedCount;
const partCountInput = document.getElementById('intakePartCount');
@@ -3215,7 +3215,7 @@
<div class="mb-3">
<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"
placeholder="e.g. Wheel Refinish Standard 4pc">
placeholder="e.g. Wheel Refinish Standard 4pc">
</div>
<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
@{
@@ -26,7 +26,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</div>
@@ -45,7 +45,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</label>
@@ -57,7 +57,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</label>
@@ -85,7 +85,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</label>
@@ -170,7 +170,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
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>
</a>
</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 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<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>
</p>
<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>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></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 id="cylinderInputs" style="display:none">
<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>
<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()">
<i class="bi bi-check-circle me-1"></i>Use This Value
</button>
@@ -1,8 +1,8 @@
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
@using PowderCoating.Core.Entities
@{
ViewData["Title"] = $"Edit Items — {Model.JobNumber}";
ViewData["Title"] = $"Edit Items {Model.JobNumber}";
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 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<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>
</p>
<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>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></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 id="cylinderInputs" style="display:none">
<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>
<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()">
<i class="bi bi-check-circle me-1"></i>Use This Value
</button>
@@ -455,7 +455,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveWorkerAssignment">
<i class="bi bi-save me-2"></i>Save Assignment
</button>
@@ -491,7 +491,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="savePriority">
<i class="bi bi-save me-2"></i>Save Priority
</button>
@@ -540,7 +540,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveStatus">
<i class="bi bi-save me-2"></i>Save Status
</button>
@@ -527,7 +527,7 @@
<p class="text-muted small mb-0">This will update the job status immediately.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btn-confirm-advance">
<i class="bi bi-check-lg me-2"></i>Yes, Advance
</button>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Job.JobDto
@model PowderCoating.Application.DTOs.Job.JobDto
@{
var emailDefault = ViewBag.EmailDefaultOnComplete == true;
var preLoggedPowder = ViewBag.PreLoggedPowder as Dictionary<int, decimal> ?? new Dictionary<int, decimal>();
@@ -97,7 +97,7 @@
@if (preFilledLbs > 0)
{
<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>
}
</td>
@@ -111,7 +111,7 @@
<td colspan="5">
<small class="text-muted fst-italic">
<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>
</td>
</tr>
@@ -122,7 +122,7 @@
</div>
<div class="alert alert-info alert-permanent mb-0">
<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>
}
@@ -152,7 +152,7 @@
}
</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">
<i class="bi bi-check-circle me-1"></i>Mark as Completed
</button>
@@ -98,14 +98,14 @@
</div>
</div>
<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>
}
else
{
<!-- Simple single delete -->
<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">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@Model.Id" />
@@ -1,4 +1,4 @@
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Entities
@model ManufacturerLookupPattern
@{
ViewData["Title"] = "Add Manufacturer Pattern";
@@ -6,7 +6,7 @@
<div class="container-fluid py-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>
</a>
<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}" />
<div class="form-text">
Supported placeholders:
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> — color name transformed by Slug Transform below,
<code>{colorCode}</code> — color code as-is.
<code>{partNumber}</code> manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> color name transformed by Slug Transform below,
<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.
</div>
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
@@ -50,10 +50,10 @@
<div class="mb-3">
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
<select asp-for="SlugTransform" class="form-select">
<option value="LowerHyphen">LowerHyphen — 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="AsIs">AsIs — color name unchanged</option>
<option value="LowerHyphen">LowerHyphen 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="AsIs">AsIs color name unchanged</option>
</select>
<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>
@@ -6,7 +6,7 @@
<div class="container-fluid py-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>
</a>
<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
@{
ViewData["Title"] = "Edit Pattern";
@@ -6,7 +6,7 @@
<div class="container-fluid py-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>
</a>
<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}" />
<div class="form-text">
Supported placeholders:
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> — color name transformed by Slug Transform below,
<code>{colorCode}</code> — color code as-is.
<code>{partNumber}</code> manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> color name transformed by Slug Transform below,
<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.
</div>
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
@@ -51,10 +51,10 @@
<div class="mb-3">
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
<select asp-for="SlugTransform" class="form-select">
<option value="LowerHyphen">LowerHyphen — 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="AsIs">AsIs — color name unchanged</option>
<option value="LowerHyphen">LowerHyphen 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="AsIs">AsIs color name unchanged</option>
</select>
<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>
@@ -9,7 +9,7 @@
<div class="container-fluid py-3" style="max-width:800px">
<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
</a>
<h4 class="mb-0"><i class="bi bi-bell me-2 text-primary"></i>Notification Details</h4>
@@ -96,7 +96,7 @@
</div>
<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
</a>
<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";
var isSuperAdmin = ViewBag.IsSuperAdmin as bool? ?? false;
@@ -198,7 +198,7 @@
<hr />
<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
</a>
<div class="d-flex gap-2">
@@ -289,7 +289,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning">
<i class="bi bi-key"></i> Reset Password
</button>
@@ -323,7 +323,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger"><i class="bi bi-slash-circle"></i> Ban User</button>
</div>
</form>
@@ -87,7 +87,7 @@
</div>
<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
</a>
<button type="submit" class="btn btn-primary">
@@ -1,4 +1,4 @@
@{
@{
ViewData["Title"] = "Grant SuperAdmin Access";
ViewData["PageIcon"] = "bi-shield-plus";
}
@@ -100,7 +100,7 @@
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">
<i class="bi bi-shield-plus"></i> Grant SuperAdmin
</button>
@@ -1,4 +1,4 @@
@model PagedResult<PowderCoating.Application.DTOs.User.PlatformUserListDto>
@model PagedResult<PowderCoating.Application.DTOs.User.PlatformUserListDto>
@section Styles {
<style>
@@ -188,7 +188,7 @@
}
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>
</span>
}
@@ -234,7 +234,7 @@
</table>
</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-list">
@foreach (var user in Model.Items)
@@ -295,7 +295,7 @@
</div>
</div>
<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>
</a>
}
@@ -349,7 +349,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning">
<i class="bi bi-key"></i> Reset Password
</button>
@@ -382,7 +382,7 @@
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">
<i class="bi bi-shield-x"></i> Revoke Access
</button>
@@ -7,7 +7,7 @@
<div class="container-fluid py-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>
</a>
<div>
@@ -7,7 +7,7 @@
<div class="container-fluid py-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>
</a>
<div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Powder.PowderInsightsDashboardDto
@model PowderCoating.Application.DTOs.Powder.PowderInsightsDashboardDto
@{
ViewData["Title"] = "Powder Insights";
ViewData["PageIcon"] = "bi-graph-up";
@@ -6,12 +6,12 @@
}
<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
</a>
</div>
@* ── KPI cards ── *@
@* ── KPI cards ── *@
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
@@ -49,7 +49,7 @@
</div>
</div>
@* ── Tabs ── *@
@* ── Tabs ── *@
<ul class="nav nav-tabs mb-3" id="insightsTabs" role="tablist">
<li class="nav-item" role="presentation">
<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">
@* ── 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">
@if (!Model.LowStockAlerts.Any())
{
@@ -87,7 +87,7 @@
{
<div class="card border-0 shadow-sm">
<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 class="table-responsive">
<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">@item.ScheduledDemandLbs.ToString("0.##") lbs</td>
<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 class="text-center">@item.ActiveJobCount</td>
<td class="text-center">
@@ -141,7 +141,7 @@
}
</div>
@* ── Tab 2: Coverage Efficiency (Layer 2) ── *@
@* ── Tab 2: Coverage Efficiency (Layer 2) ── *@
<div class="tab-pane fade" id="efficiency" role="tabpanel">
@if (!readiness.IsLayer2Ready)
{
@@ -157,7 +157,7 @@
else if (!Model.EfficiencyBySku.Any())
{
<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>
}
else
@@ -188,11 +188,11 @@
<strong>@eff.Name</strong>
@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)
{
<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 class="text-end">@eff.CatalogCoverageSqFtPerLb.ToString("0.#") sq ft/lb</td>
@@ -214,7 +214,7 @@
}
</div>
@* ── Tab 3: Predictive (Layer 3, gated) ── *@
@* ── Tab 3: Predictive (Layer 3, gated) ── *@
<div class="tab-pane fade" id="predictive" role="tabpanel">
@if (!readiness.IsLayer3Ready)
{
@@ -238,9 +238,9 @@
<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>
<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>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>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>Per-powder efficiency corrections</strong> suggests updating coverage defaults based on real data</li>
</ul>
</div>
</div>
@@ -258,7 +258,7 @@
</div>
@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
{
@@ -284,7 +284,7 @@
<strong>@s.Name</strong>
@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 class="text-end">@s.CurrentStockLbs.ToString("0.#") lbs</td>
@@ -311,7 +311,7 @@
@* Waste Patterns *@
<div class="card border-0 shadow-sm">
<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>
@if (!Model.WastePatterns.Any())
{
@@ -344,7 +344,7 @@
<br /><small class="text-muted">@w.CoatName</small>
</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.ActualLbs.ToString("0.##") lbs</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
@{
@@ -51,7 +51,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
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>
</a>
</h5>
@@ -146,7 +146,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
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>
</a>
</h5>
@@ -210,7 +210,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
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>
</a>
</h5>
@@ -253,7 +253,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
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>
</a>
</h5>
@@ -314,7 +314,7 @@
<div class="form-check">
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="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>
</div>
</div>
@@ -329,7 +329,7 @@
<a tabindex="0" class="help-icon text-white" role="button"
data-bs-toggle="popover" data-bs-placement="left"
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>
</a>
</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 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<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>
</p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -379,7 +379,7 @@
<div class="row g-3" id="stagedPhotoGrid"></div>
<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>
<small class="text-muted">Uploading…</small>
<small class="text-muted">Uploading</small>
</div>
</div>
</div>
@@ -446,7 +446,7 @@
<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>
</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 id="cylinderInputs" style="display:none">
<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>
<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()">
<i class="bi bi-check-circle me-1"></i>Use This Value
</button>
@@ -681,7 +681,7 @@
<script src="~/lib/tom-select/js/tom-select.complete.min.js"></script>
<script src="~/js/item-wizard.js?v=@DateTime.Now.Ticks"></script>
<script>
// ── Quick / Full quote mode toggle ──────────────────────────────────
// ── Quick / Full quote mode toggle ──────────────────────────────────
(function () {
const STORAGE_KEY = 'pcl_quote_mode';
const form = document.getElementById('quoteForm');
@@ -690,7 +690,7 @@
function applyMode(mode) {
if (mode === 'simple') {
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 {
form.classList.remove('quote-simple-mode');
hint.textContent = '';
@@ -758,8 +758,8 @@
smsNote.style.display = 'inline';
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
smsNote.innerHTML = hasSms
? '<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 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';
} else {
smsNote.style.display = 'none';
}
@@ -32,7 +32,7 @@
<button onclick="printQuotePdf(@Model.Id)" class="btn btn-info">
<i class="bi bi-printer me-1"></i>Print
</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
</a>
@if (Model.IsProspect && Model.StatusCode == "APPROVED")
@@ -796,10 +796,10 @@
<button type="button" class="btn btn-danger" id="qpDeleteBtn" onclick="qpGallery.deletePhoto()">
<i class="bi bi-trash me-1"></i>Delete
</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 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()">
<i class="bi bi-check-lg me-1"></i>Save Caption
</button>
@@ -1897,7 +1897,7 @@
<div id="depositFormError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success" id="saveDepositBtn">
<i class="bi bi-check-circle me-1"></i>Save & Generate Receipt
</button>
@@ -2156,7 +2156,7 @@
<div id="quoteAdHocEmailError" class="text-danger small mt-1 d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="sendQuoteToAdHocEmail(@Model.Id)">
<i class="bi bi-send me-1"></i>Send
</button>
@@ -2186,7 +2186,7 @@
</div>
</div>
<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>
@@ -2213,7 +2213,7 @@
</div>
</div>
<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>
@@ -2254,7 +2254,7 @@
</div>
</div>
<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>
+18 -18
View File
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Quote.UpdateQuoteDto
@model PowderCoating.Application.DTOs.Quote.UpdateQuoteDto
@using PowderCoating.Core.Entities
@{
@@ -109,7 +109,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
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>
</a>
</h5>
@@ -173,7 +173,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
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>
</a>
</h5>
@@ -216,7 +216,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
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>
</a>
</h5>
@@ -277,7 +277,7 @@
<div class="form-check">
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="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>
</div>
</div>
@@ -292,7 +292,7 @@
<a tabindex="0" class="help-icon text-white" role="button"
data-bs-toggle="popover" data-bs-placement="left"
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>
</a>
</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 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<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>
</p>
<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>
<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-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>
}
@@ -407,7 +407,7 @@
</div>
<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>
<small class="text-muted">Uploading…</small>
<small class="text-muted">Uploading</small>
</div>
</div>
</div>
@@ -435,11 +435,11 @@
<span id="smsNotifyNote" class="badge @(editHasSms ? "bg-info text-white" : "bg-warning text-dark")">
@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
{
<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>
}
@@ -483,7 +483,7 @@
<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>
</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 id="cylinderInputs" style="display:none">
<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>
<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()">
<i class="bi bi-check-circle me-1"></i>Use This Value
</button>
@@ -579,7 +579,7 @@
@Html.Raw(Json.Serialize(ViewBag.BlastSetups ?? new List<object>()))
</script>
<!-- Existing items — always populated on Edit -->
<!-- Existing items always populated on Edit -->
<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 {
description = item.Description,
@@ -740,8 +740,8 @@
smsNote.style.display = 'inline';
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
smsNote.innerHTML = hasSms
? '<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 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';
} else {
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 () {
const quoteId = @Model.Id;
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>
<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-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>
@@ -343,7 +343,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveStatus">
<i class="bi bi-save me-2"></i>Save Status
</button>
@@ -5,7 +5,7 @@
<div class="container py-4" style="max-width:800px">
<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
</a>
<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="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
</a>
<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 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>
@@ -1,4 +1,4 @@
@model List<PowderCoating.Core.Entities.Company>
@model List<PowderCoating.Core.Entities.Company>
@{
ViewData["Title"] = "Seed Data Management";
ViewData["PageIcon"] = "bi-database-fill-gear";
@@ -196,7 +196,7 @@
</table>
</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-list">
@foreach (var company in Model)
@@ -363,7 +363,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning" id="unseedSubmitBtn" disabled>
<i class="bi bi-database-fill-dash me-1"></i>Remove Selected Data
</button>
@@ -228,7 +228,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="applyLaborCalc()">
<i class="bi bi-check2-circle me-1"></i>Apply &amp; Close
</button>
@@ -298,7 +298,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="applyEquipCalc()">
<i class="bi bi-check2-circle me-1"></i>Apply &amp; Close
</button>
@@ -371,7 +371,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="applyPowderCalc()">
<i class="bi bi-check2-circle me-1"></i>Apply &amp; Close
</button>
@@ -1503,10 +1503,10 @@
aria-label="Toggle sidebar">
<i class="bi bi-list" style="font-size: 1.5rem;"></i>
</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)
{
<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>
@if (ViewData["PageHelpContent"] != null)
@@ -2287,7 +2287,7 @@
<p class="text-muted mb-0" id="globalConfirmMessage"></p>
</div>
<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>
</div>
</div>
@@ -274,7 +274,7 @@
</div>
</div>
<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>
@@ -13,7 +13,7 @@
<div class="container-fluid py-3" style="max-width:960px">
<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
</a>
<h4 class="mb-0">
@@ -67,7 +67,7 @@
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center py-2">
<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
</button>
</div>
@@ -1,8 +1,8 @@
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Enums
@model Company
@{
ViewData["Title"] = $"Manage – {Model.CompanyName}";
ViewData["Title"] = $"Manage {Model.CompanyName}";
var planConfigs = (dynamic)ViewBag.PlanConfigs;
string PlanName(int plan)
@@ -22,11 +22,11 @@
<div class="container-fluid py-3" style="max-width:900px">
<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
</a>
<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
</a>
<h4 class="mb-0">
@@ -66,9 +66,9 @@
<dt class="col-7 text-muted">Users</dt>
<dd class="col-5 fw-semibold">@ViewBag.UserCount</dd>
<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>
<dd class="col-5"><code class="small">@(Model.StripeSubscriptionId ?? "—")</code></dd>
<dd class="col-5"><code class="small">@(Model.StripeSubscriptionId ?? "")</code></dd>
</dl>
</div>
</div>
@@ -99,7 +99,7 @@
<form method="post" asp-action="UpdateSubscription" asp-route-id="@Model.Id">
@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-header border-0 py-3 @(Model.IsComped ? "bg-success bg-opacity-10" : "bg-white")">
<h6 class="mb-0 fw-semibold">
@@ -351,11 +351,11 @@
</div>
<dl class="row small mb-3">
<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>
<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>
<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>
<div class="mb-3">
<label class="form-label fw-medium">Refund Amount</label>
@@ -374,7 +374,7 @@
<div id="refund-result" class="d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="refund-submit-btn" onclick="submitRefund()">
<i class="bi bi-arrow-counterclockwise me-1"></i>Issue Refund
</button>
@@ -402,7 +402,7 @@
@section Scripts {
<script>
// ── State for the refund modal ────────────────────────────────────────────────
// ── State for the refund modal ────────────────────────────────────────────────
let _refundPaymentIntentId = null;
let _refundMaxCents = 0;
let _refundAmountPaid = '';
@@ -442,7 +442,7 @@ async function loadPaymentHistory() {
: '';
const refundedCell = ch.amountRefunded
? `<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>`;
// 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) {
_refundPaymentIntentId = chargeId; // reusing variable — now holds charge ID
_refundPaymentIntentId = chargeId; // reusing variable now holds charge ID
_refundMaxCents = refundableCents;
_refundAmountPaid = amountPaid;
_refundInvoiceNumber = displayLabel;
@@ -509,7 +509,7 @@ async function submitRefund() {
}
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 {
const formData = new FormData();
@@ -204,7 +204,7 @@
<small class="text-muted me-auto">
<i class="bi bi-lightbulb me-1"></i>Easter egg unlocked! 🎉
</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>
+131
View File
@@ -1020,4 +1020,135 @@ a.tag-index-badge:hover {
.mw-xs { max-width: 280px; }
.mw-sm { max-width: 360px; }
.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; }