Fix flat-rate coat wizard UX, — literal, select caret overlap, and walkthrough modal sizing

- Flat-rate items now default coat type to Custom so Color Name field is immediately visible
- Catalog search blur copies typed text to Color Name when no catalog result was selected
- Item card shows 'No color specified' badge when coat has powder-to-order but no color name
- Color Name label marked required with '(shows on quote)' hint
- Coat name select min-width prevents text overlapping Bootstrap caret arrow
- Remove extra unbalanced </div> from renderSalesFields
- Fix literal &mdash; in quote simple-mode hint (textContent → innerHTML)
- Formula walkthrough modal fixed at 700px so all steps render at identical window size

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 23:01:33 -04:00
parent ed35362c7a
commit 7e31846777
3 changed files with 60 additions and 12 deletions
@@ -2284,7 +2284,7 @@
<!-- Custom Formula Walkthrough Modal -->
<div class="modal fade" id="cfWalkthroughModal" tabindex="-1" aria-labelledby="cfWalkthroughLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-content" style="height:700px">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title" id="cfWalkthroughLabel">
<i class="bi bi-calculator text-info me-2"></i>Custom Formula Templates &mdash; How It Works
@@ -590,7 +590,7 @@
function applyMode(mode) {
if (mode === 'simple') {
form.classList.add('quote-simple-mode');
hint.textContent = 'Advanced fields are hidden &mdash; switch to Full Quote to see them.';
hint.innerHTML = 'Advanced fields are hidden &mdash; switch to Full Quote to see them.';
} else {
form.classList.remove('quote-simple-mode');
hint.textContent = '';
+58 -10
View File
@@ -209,11 +209,20 @@ function wizardNext() {
if (wz.step === 1) {
wz.step = 2;
} else if (wz.step === 2) {
// Generic / labor / sales: step 2 is the last step
if (wz.itemType === 'generic' || wz.itemType === 'labor' || wz.itemType === 'sales') {
// Labor / sales: step 2 is the last step
if (wz.itemType === 'labor' || wz.itemType === 'sales') {
wizardSave();
return;
}
// Flat-Rate Charge: go to step 3 only if user opted in to specifying coatings
if (wz.itemType === 'generic') {
if (wz.data.genericHasCoatings) {
wz.step = 3;
} else {
wizardSave();
return;
}
}
// AI: step 2 must be accepted before proceeding
if (wz.itemType === 'ai') {
if (!wz.ai.accepted) {
@@ -223,6 +232,11 @@ function wizardNext() {
}
wz.step = 3;
} else if (wz.step === 3) {
// Flat-Rate Charge with coatings stops here — no prep or catalog-save step
if (wz.itemType === 'generic') {
wizardSave();
return;
}
// Step 4 (prep services) for product, calculated, and ai items — always shown
// so users with no prep services configured see the empty-state prompt
wz.step = 4;
@@ -615,6 +629,18 @@ function renderGenericFields() {
<label class="form-label fw-semibold">Notes <small class="text-muted">(optional)</small></label>
<input type="text" class="form-control" id="wz_notes" placeholder="Internal notes about this charge…" maxlength="500">
</div>
</div>
<div class="d-flex align-items-center border rounded py-2 px-3 mt-3 gap-2">
<div class="form-check form-switch mb-0 flex-shrink-0">
<input class="form-check-input" type="checkbox" id="wz_genericHasCoatings"
${wz.data.genericHasCoatings ? 'checked' : ''}
onchange="wz.data.genericHasCoatings = this.checked; updateStepDots(); updateWizardButtons();"
style="cursor:pointer">
</div>
<label for="wz_genericHasCoatings" class="mb-0" style="cursor:pointer; line-height:1.3">
<strong>Specify powder coating</strong>
<span class="d-block text-muted fw-normal small">Add coat &amp; color specs for powder ordering and tracking (price stays flat)</span>
</label>
</div>`;
}
@@ -1795,7 +1821,7 @@ function addCoatRow() {
const coats = wz.data.coats = wz.data.coats || [];
const i = coats.length;
const defaultName = i === 0 ? 'Base Coat' : '';
const coat = { coatName: defaultName, sequence: i + 1, inventoryItemId: null, powderType: 'stock' };
const coat = { coatName: defaultName, sequence: i + 1, inventoryItemId: null, powderType: wz.itemType === 'generic' ? 'custom' : 'stock' };
coats.push(coat);
const container = document.getElementById('coatsListContainer');
@@ -1816,7 +1842,7 @@ function buildCoatNameHtml(i, currentName) {
`<option value="${n}"${selectVal === n ? ' selected' : ''}>${n}</option>`
).join('');
return `
<select class="form-select form-select-sm" style="max-width:160px"
<select class="form-select form-select-sm" style="min-width:150px;max-width:200px"
id="coat_name_sel_${i}" onchange="onCoatNameSelect(${i})">
<option value="">-- Select --</option>
${options}
@@ -1932,6 +1958,7 @@ function buildCoatRowHtml(i, coat) {
placeholder="Lookup from catalog (color name or SKU)…"
oninput="customPowderCatalogInput(${i})"
onkeydown="if(event.key==='Escape'){customPowderCatalogClose(${i})}"
onblur="catalogQBlur(${i})"
autocomplete="off">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="customPowderCatalogClose(${i})" title="Clear lookup">
<i class="bi bi-x" style="font-size:.8rem;"></i>
@@ -1945,8 +1972,8 @@ function buildCoatRowHtml(i, coat) {
</div>
</div>
<div class="col-sm-6">
<label class="form-label form-label-sm">Color Name</label>
<input type="text" class="form-control form-control-sm" id="coat_colorName_${i}" value="${escHtml(coat.colorName || '')}" placeholder="e.g., Gloss Black">
<label class="form-label form-label-sm fw-semibold">Color Name <span class="text-danger">*</span> <small class="text-muted fw-normal">(shows on quote)</small></label>
<input type="text" class="form-control form-control-sm" id="coat_colorName_${i}" value="${escHtml(coat.colorName || '')}" placeholder="e.g., Gloss Black, Matte Red…" required>
</div>
<div class="col-sm-3">
<label class="form-label form-label-sm">Color Code</label>
@@ -2265,6 +2292,19 @@ function customPowderCatalogClose(i) {
if (qEl) qEl.value = '';
}
// When the catalog search loses focus without a catalog result being selected,
// copy whatever the user typed into Color Name (if Color Name is still empty).
// This handles the common case of typing a color name in the search, not finding
// a catalog match, and expecting the typed text to become the color name.
function catalogQBlur(i) {
const q = document.getElementById(`coat_catalog_q_${i}`);
const catId = document.getElementById(`coat_custom_catalogItemId_${i}`);
const cn = document.getElementById(`coat_colorName_${i}`);
if (q && !catId?.value && q.value.trim() && cn && !cn.value.trim()) {
cn.value = q.value.trim();
}
}
function applyCustomCatalogResult(i, r) {
customPowderCatalogClose(i);
// Fill in the custom fields from the catalog result
@@ -2967,9 +3007,12 @@ function buildCardHtml(item, i) {
const orderBadge = (!c.inventoryItemId && c.powderToOrder)
? ` <span class="badge bg-warning text-dark" style="font-size:.65em;vertical-align:middle;" title="Custom powder — must be purchased before coating"><i class="bi bi-cart me-1"></i>ORDER ${parseFloat(c.powderToOrder).toFixed(2)} lbs</span>`
: '';
const noColorHint = (!color && !c.inventoryItemId && c.powderToOrder)
? ` <span class="badge bg-danger" style="font-size:.65em;vertical-align:middle;" title="Edit this item and enter a Color Name for the coat">No color specified</span>`
: '';
const coatNotes = c.notes ? `<span class="fst-italic ms-1 opacity-75">— ${escHtml(c.notes)}</span>` : '';
return `<div style="font-size:.8rem;" class="text-muted mt-1">
<i class="bi bi-layers me-1"></i><span class="fw-semibold">${escHtml(c.coatName || 'Coat')}</span>${color ? `${color}${code}` : ''}${orderBadge}${coatNotes}
<i class="bi bi-layers me-1"></i><span class="fw-semibold">${escHtml(c.coatName || 'Coat')}</span>${color ? `${color}${code}` : ''}${orderBadge}${noColorHint}${coatNotes}
</div>`;
}).join('');
@@ -3352,7 +3395,9 @@ function updateWizardButtons() {
if (!btnBack || !btnNext || !btnSave) return;
const isLast = (wz.step === 4 && wz.itemType !== 'calculated' && wz.itemType !== 'ai')
|| (wz.step === 2 && (wz.itemType === 'generic' || wz.itemType === 'labor' || wz.itemType === 'sales'));
|| (wz.step === 3 && wz.itemType === 'generic' && wz.data.genericHasCoatings)
|| (wz.step === 2 && (wz.itemType === 'labor' || wz.itemType === 'sales'))
|| (wz.step === 2 && wz.itemType === 'generic' && !wz.data.genericHasCoatings);
const isCatalogStep = wz.step === 5;
btnBack.classList.toggle('d-none', wz.step === 1);
@@ -3369,7 +3414,8 @@ function updateStepDots() {
const step2Line = document.getElementById('step2Line');
const step3Line = document.getElementById('step3Line');
const hasCoats = wz.itemType !== 'generic' && wz.itemType !== 'labor' && wz.itemType !== 'sales';
const hasCoats = wz.itemType !== 'labor' && wz.itemType !== 'sales'
&& !(wz.itemType === 'generic' && !wz.data.genericHasCoatings);
if (step3Dot) step3Dot.classList.toggle('skip', !hasCoats);
if (step4Dot) step4Dot.classList.toggle('skip', !hasCoats);
@@ -3383,7 +3429,9 @@ function updateStepDots() {
else if (s < wz.step) dot.classList.add('done');
});
const totalSteps = hasCoats ? 4 : 2;
// Flat-Rate + coatings stops at step 3 (no prep or catalog save); all others follow normal flow
const totalSteps = (wz.itemType === 'generic' && wz.data.genericHasCoatings) ? 3
: hasCoats ? 4 : 2;
const currentDisplay = Math.min(wz.step, totalSteps);
const label = document.getElementById('wizardStepLabel');
if (label) label.textContent = wz.step === 5 ? 'Final Step' : `Step ${currentDisplay} of ${totalSteps}`;