Sweep all .cshtml files for encoding corruption; add pre-commit guard
Replace all corruption variants with HTML entities across 226 view files: - 3-char UTF-8-as-Win1252 sequences (ae-corruption) - Standalone smart/curly quotes that break C# Razor expressions - Partially re-corrupted variants where the 3rd byte was normalised to ASCII tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the script itself never contains a literal non-ASCII character; supports -DryRun .githooks/pre-commit: blocks commits containing the ae-corruption byte signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the hook is repo-committed and active for all future work on this machine. Build clean; 225 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <strong>Existing Customer</strong> if this person is already in your system. Choose <strong>New Prospect/Walk-In</strong> 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.<br><br><a href='/Help/Quotes#prospect-conversion' target='_blank'>Learn more →</a>">
|
||||
data-bs-content="Choose <strong>Existing Customer</strong> if this person is already in your system. Choose <strong>New Prospect/Walk-In</strong> 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.<br><br><a href='/Help/Quotes#prospect-conversion' target='_blank'>Learn more â†'</a>">
|
||||
<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 <strong>Expiration Date</strong> is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The <strong>Customer PO</strong> field is optional — use it if the customer provides their own purchase order number.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more →</a>">
|
||||
data-bs-content="Set the quote date, expiration, and any internal notes. The <strong>Expiration Date</strong> is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The <strong>Customer PO</strong> field is optional — use it if the customer provides their own purchase order number.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more â†'</a>">
|
||||
<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 & 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="<strong>Calculated</strong> — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.<br><strong>Custom Work</strong> — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.<br><strong>AI Photo</strong> — upload photos and let the AI estimate surface area and complexity for you.<br><br><a href='/Help/Quotes#quote-items' target='_blank'>Learn more →</a>">
|
||||
data-bs-content="<strong>Calculated</strong> — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.<br><strong>Custom Work</strong> — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.<br><strong>AI Photo</strong> — upload photos and let the AI estimate surface area and complexity for you.<br><br><a href='/Help/Quotes#quote-items' target='_blank'>Learn more â†'</a>">
|
||||
<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 <strong>Tier Discount</strong> appears automatically if the customer has a pricing tier assigned. A <strong>Rush Fee</strong> is added when Rush Job is checked.<br><br><a href='/Help/Quotes#pricing-breakdown' target='_blank'>Learn more →</a>">
|
||||
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 <strong>Tier Discount</strong> appears automatically if the customer has a pricing tier assigned. A <strong>Rush Fee</strong> is added when Rush Job is checked.<br><br><a href='/Help/Quotes#pricing-breakdown' target='_blank'>Learn more â†'</a>">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</h5>
|
||||
@@ -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>
|
||||
@@ -552,7 +552,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');
|
||||
@@ -561,7 +561,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 = '';
|
||||
@@ -631,8 +631,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';
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
{
|
||||
<div class="alert alert-warning alert-permanent">
|
||||
<h5 class="alert-heading">
|
||||
<i class="bi bi-lock-fill me-2"></i>Quote Is Locked — Linked Job Exists
|
||||
<i class="bi bi-lock-fill me-2"></i>Quote Is Locked — Linked Job Exists
|
||||
</h5>
|
||||
<p class="mb-1">
|
||||
Quote <strong>@Model.QuoteNumber</strong> was used to create
|
||||
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@linkedJobId" class="fw-semibold alert-link">@linkedJobNumber</a>.
|
||||
It cannot be deleted while that job exists — the quote is the original pricing and approval record for the job.
|
||||
It cannot be deleted while that job exists — the quote is the original pricing and approval record for the job.
|
||||
</p>
|
||||
<p class="mb-0">To delete this quote, delete <strong>@linkedJobNumber</strong> first.</p>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<a tabindex="0" class="help-icon ms-1" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right"
|
||||
data-bs-title="Quote Statuses"
|
||||
data-bs-content="<strong>Draft</strong> — saved but not yet sent. Edit freely.<br><strong>Sent</strong> — delivered to the customer, awaiting response.<br><strong>Approved</strong> — customer accepted. You can convert this to a Job.<br><strong>Rejected</strong> — customer declined.<br><strong>Expired</strong> — validity period has passed. Edit to extend it.<br><strong>Converted</strong> — a job has been created from this quote.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more →</a>">
|
||||
data-bs-content="<strong>Draft</strong> — saved but not yet sent. Edit freely.<br><strong>Sent</strong> — delivered to the customer, awaiting response.<br><strong>Approved</strong> — customer accepted. You can convert this to a Job.<br><strong>Rejected</strong> — customer declined.<br><strong>Expired</strong> — validity period has passed. Edit to extend it.<br><strong>Converted</strong> — a job has been created from this quote.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more →</a>">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</p>
|
||||
@@ -334,7 +334,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-warning text-dark" style="font-size: 0.7em;" title="Custom powder — must be purchased before coating">
|
||||
<span class="badge bg-warning text-dark" style="font-size: 0.7em;" title="Custom powder — must be purchased before coating">
|
||||
<i class="bi bi-cart me-1"></i>ORDER: @coat.PowderToOrder?.ToString("0.##") lbs
|
||||
</span>
|
||||
}
|
||||
@@ -438,7 +438,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-warning text-dark" style="font-size: 0.7em;" title="Custom powder — must be purchased before coating">
|
||||
<span class="badge bg-warning text-dark" style="font-size: 0.7em;" title="Custom powder — must be purchased before coating">
|
||||
<i class="bi bi-cart me-1"></i>ORDER: @coat.PowderToOrder?.ToString("0.##") lbs
|
||||
</span>
|
||||
}
|
||||
@@ -463,7 +463,7 @@
|
||||
<br />
|
||||
<small class="ms-3">
|
||||
• <strong>@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}")</strong>
|
||||
<span class="text-muted">— @ps.EstimatedMinutes min</span>
|
||||
<span class="text-muted">— @ps.EstimatedMinutes min</span>
|
||||
</small>
|
||||
}
|
||||
}
|
||||
@@ -665,7 +665,7 @@
|
||||
{
|
||||
<div class="mb-1">
|
||||
<strong>@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}")</strong>
|
||||
<span class="text-muted">— @ps.EstimatedMinutes min</span>
|
||||
<span class="text-muted">— @ps.EstimatedMinutes min</span>
|
||||
</div>
|
||||
}
|
||||
</span>
|
||||
@@ -753,7 +753,7 @@
|
||||
</div>
|
||||
<div id="photoUploadProgress" 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>
|
||||
@@ -1110,7 +1110,7 @@
|
||||
var allCatalog = Model.QuoteItems != null && Model.QuoteItems.All(i => i.CatalogItemId.HasValue);
|
||||
}
|
||||
|
||||
@* ── SECTION 1: Item Cost Breakdown ─────────────────────── *@
|
||||
@* -- SECTION 1: Item Cost Breakdown ----------------------- *@
|
||||
<div class="mb-3">
|
||||
<div class="text-uppercase text-muted fw-semibold small mb-2" style="letter-spacing:.05em;">
|
||||
<i class="bi bi-boxes me-1"></i>Item Costs
|
||||
@@ -1140,7 +1140,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
|
||||
{
|
||||
@@ -1152,7 +1152,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── SECTION 2: Quote-Level Additions ───────────────────── *@
|
||||
@* -- SECTION 2: Quote-Level Additions --------------------- *@
|
||||
@if (pb.OvenBatchCost > 0 || pb.FacilityOverheadCost > 0 || pb.ShopSuppliesAmount > 0 || pb.OverheadCosts > 0)
|
||||
{
|
||||
<div class="mb-3">
|
||||
@@ -1190,7 +1190,7 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── SECTION 3: Final Calculation ────────────────────────── *@
|
||||
@* -- SECTION 3: Final Calculation -------------------------- *@
|
||||
<div class="mb-2">
|
||||
<div class="text-uppercase text-muted fw-semibold small mb-2" style="letter-spacing:.05em;">
|
||||
<i class="bi bi-receipt me-1"></i>Final Calculation
|
||||
@@ -1245,13 +1245,13 @@
|
||||
var priceBeforeDiscount = pb.Total + pb.DiscountAmount;
|
||||
var marginBeforeDiscount = priceBeforeDiscount > 0 ? ((priceBeforeDiscount - totalDirectCost) / priceBeforeDiscount * 100m) : 0m;
|
||||
<div class="small text-muted">
|
||||
Without discount: @marginBeforeDiscount.ToString("F1")% — discount cost you @((marginBeforeDiscount - effectiveMargin).ToString("F1")) margin points
|
||||
Without discount: @marginBeforeDiscount.ToString("F1")% — discount cost you @((marginBeforeDiscount - effectiveMargin).ToString("F1")) margin points
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ── SECTION 4: Per-Item Cost Breakdown ─────────────────── *@
|
||||
@* -- SECTION 4: Per-Item Cost Breakdown ------------------- *@
|
||||
@{
|
||||
var hasItemCostData = Model.QuoteItems != null && Model.QuoteItems.Any(i =>
|
||||
i.ItemMaterialCost > 0 || i.ItemLaborCost > 0 || i.ItemEquipmentCost > 0);
|
||||
@@ -1273,13 +1273,13 @@
|
||||
var tier = ViewBag.PricingTier as PowderCoating.Core.Entities.PricingTier;
|
||||
<div class="alert alert-success alert-permanent py-2 px-3 mb-3 small">
|
||||
<i class="bi bi-tag me-1"></i>
|
||||
<strong>Pricing Tier:</strong> @tier.TierName — @tier.DiscountPercent% discount applied to this customer
|
||||
<strong>Pricing Tier:</strong> @tier.TierName — @tier.DiscountPercent% discount applied to this customer
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!hasItemCostData)
|
||||
{
|
||||
<div class="text-muted small fst-italic mb-3">Per-item cost detail not available — re-save this quote to capture the breakdown.</div>
|
||||
<div class="text-muted small fst-italic mb-3">Per-item cost detail not available — re-save this quote to capture the breakdown.</div>
|
||||
}
|
||||
|
||||
@* Per-item breakdown cards *@
|
||||
@@ -1427,7 +1427,7 @@
|
||||
<div class="d-flex justify-content-between text-muted ps-2 border-start border-2 mb-1">
|
||||
<div>
|
||||
@coat.CoatName
|
||||
@if (!string.IsNullOrEmpty(coat.ColorName)) { <text> — @coat.ColorName</text> }
|
||||
@if (!string.IsNullOrEmpty(coat.ColorName)) { <text> — @coat.ColorName</text> }
|
||||
@if (!string.IsNullOrEmpty(coat.ColorCode)) { <text> (@coat.ColorCode)</text> }
|
||||
@if (coat.InventoryItemId.HasValue)
|
||||
{
|
||||
@@ -1467,11 +1467,11 @@
|
||||
}
|
||||
else if (item.CatalogItemId.HasValue && !item.Coats.Any())
|
||||
{
|
||||
<div class="px-3 py-2 text-muted fst-italic">Catalog fixed price — no cost split available.</div>
|
||||
<div class="px-3 py-2 text-muted fst-italic">Catalog fixed price — no cost split available.</div>
|
||||
}
|
||||
else if (item.IsAiItem)
|
||||
{
|
||||
<div class="px-3 py-2 text-muted fst-italic">AI-estimated price — cost breakdown not applicable.</div>
|
||||
<div class="px-3 py-2 text-muted fst-italic">AI-estimated price — cost breakdown not applicable.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -1480,7 +1480,7 @@
|
||||
@if (hasItemCostData)
|
||||
{
|
||||
<div class="d-flex justify-content-between fw-semibold border-top pt-2 mt-1 small">
|
||||
<span class="text-muted">All items — material / labor / equipment / total</span>
|
||||
<span class="text-muted">All items — material / labor / equipment / total</span>
|
||||
<span>@pb.MaterialCosts.ToString("C") / @pb.LaborCosts.ToString("C") / @pb.EquipmentCosts.ToString("C") / @pb.ItemsSubtotal.ToString("C")</span>
|
||||
</div>
|
||||
}
|
||||
@@ -1506,7 +1506,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-0">Markup</td>
|
||||
<td class="text-end pe-0">@dbgCosts.GeneralMarkupPercentage.ToString("F1")% — applied to material costs</td>
|
||||
<td class="text-end pe-0">@dbgCosts.GeneralMarkupPercentage.ToString("F1")% — applied to material costs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-0">Additional coat charge</td>
|
||||
@@ -1598,7 +1598,7 @@
|
||||
@if (!detHasMobile && !detHasEmail && !detProspectHasPhone && string.IsNullOrWhiteSpace(Model.ProspectEmail))
|
||||
{
|
||||
<div class="alert alert-warning alert-permanent py-1 px-2 small">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>No email or mobile number on file — update the customer record to send this quote electronically.
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>No email or mobile number on file — update the customer record to send this quote electronically.
|
||||
</div>
|
||||
}
|
||||
@if (detHasMobile && !detHasSmsConsent)
|
||||
@@ -1607,7 +1607,7 @@
|
||||
}
|
||||
@if (detProspectHasPhone && !Model.ProspectSmsConsent)
|
||||
{
|
||||
<div class="text-muted small"><i class="bi bi-phone-slash me-1"></i>SMS consent not recorded — edit the quote to enable SMS for this prospect.</div>
|
||||
<div class="text-muted small"><i class="bi bi-phone-slash me-1"></i>SMS consent not recorded — edit the quote to enable SMS for this prospect.</div>
|
||||
}
|
||||
@if (!Model.ConvertedToJobId.HasValue)
|
||||
{
|
||||
@@ -2412,7 +2412,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></td>
|
||||
</tr>${errorRow}`;
|
||||
}).join('');
|
||||
@@ -2449,7 +2449,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Deposits ─────────────────────────────────────────────────────────
|
||||
// -- Deposits ---------------------------------------------------------
|
||||
function getAntiForgeryToken() {
|
||||
return document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
|
||||
}
|
||||
@@ -2466,7 +2466,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));
|
||||
try {
|
||||
|
||||
@@ -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 <strong>Expiration Date</strong> is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The <strong>Customer PO</strong> field is optional — use it if the customer provides their own purchase order number.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more →</a>">
|
||||
data-bs-content="Set the quote date, expiration, and any internal notes. The <strong>Expiration Date</strong> is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The <strong>Customer PO</strong> field is optional — use it if the customer provides their own purchase order number.<br><br><a href='/Help/Quotes#quote-statuses' target='_blank'>Learn more â†'</a>">
|
||||
<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 & 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="<strong>Calculated</strong> — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.<br><strong>Custom Work</strong> — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.<br><strong>AI Photo</strong> — upload photos and let the AI estimate surface area and complexity for you.<br><br><a href='/Help/Quotes#quote-items' target='_blank'>Learn more →</a>">
|
||||
data-bs-content="<strong>Calculated</strong> — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.<br><strong>Custom Work</strong> — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.<br><strong>AI Photo</strong> — upload photos and let the AI estimate surface area and complexity for you.<br><br><a href='/Help/Quotes#quote-items' target='_blank'>Learn more â†'</a>">
|
||||
<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 <strong>Tier Discount</strong> appears automatically if the customer has a pricing tier assigned. A <strong>Rush Fee</strong> is added when Rush Job is checked.<br><br><a href='/Help/Quotes#pricing-breakdown' target='_blank'>Learn more →</a>">
|
||||
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 <strong>Tier Discount</strong> appears automatically if the customer has a pricing tier assigned. A <strong>Rush Fee</strong> is added when Rush Job is checked.<br><br><a href='/Help/Quotes#pricing-breakdown' target='_blank'>Learn more â†'</a>">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</h5>
|
||||
@@ -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>
|
||||
}
|
||||
@@ -488,7 +488,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,
|
||||
@@ -614,8 +614,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';
|
||||
}
|
||||
@@ -640,7 +640,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")';
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<i class="bi bi-speedometer2 fs-5 flex-shrink-0 mt-1"></i>
|
||||
<div>
|
||||
<strong>Your quoting isn't calibrated yet.</strong>
|
||||
AI photo quotes and calculated item time estimates are using generic industry averages — they may not match your shop's actual throughput.
|
||||
AI photo quotes and calculated item time estimates are using generic industry averages — they may not match your shop's actual throughput.
|
||||
<a href="/CompanySettings#quoting-calibration" class="alert-link ms-1">Set up your equipment profile →</a>
|
||||
</div>
|
||||
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
|
||||
Reference in New Issue
Block a user