Fix garbled encoding symbols in oven display, bill/invoice tooltips, and profile timezone dropdown

- Replace mojibake × with × in oven batch cost row across Jobs Create/Edit/EditItems and Quotes Create/Edit
- Fix Qty × Unit Price tooltip in Bills/Edit and Invoices/Edit
- Fix all â€" (garbled em dash) and São Paulo in Profile timezone dropdown option labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 18:54:35 -04:00
parent 357ef84001
commit f34ee749be
8 changed files with 78 additions and 78 deletions
@@ -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>
@@ -560,7 +560,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 = '';
@@ -628,8 +628,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';
}
+13 -13
View File
@@ -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">
@@ -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,
@@ -613,8 +613,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';
}
@@ -639,7 +639,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")';