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 --> <!-- Custom Formula Walkthrough Modal -->
<div class="modal fade" id="cfWalkthroughModal" tabindex="-1" aria-labelledby="cfWalkthroughLabel" aria-hidden="true"> <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-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"> <div class="modal-header border-0 pb-0">
<h5 class="modal-title" id="cfWalkthroughLabel"> <h5 class="modal-title" id="cfWalkthroughLabel">
<i class="bi bi-calculator text-info me-2"></i>Custom Formula Templates &mdash; How It Works <i class="bi bi-calculator text-info me-2"></i>Custom Formula Templates &mdash; How It Works
@@ -590,7 +590,7 @@
function applyMode(mode) { function applyMode(mode) {
if (mode === 'simple') { if (mode === 'simple') {
form.classList.add('quote-simple-mode'); form.classList.add('quote-simple-mode');
hint.textContent = 'Advanced fields are hidden &mdash; switch to Full Quote to see them.'; hint.innerHTML = 'Advanced fields are hidden &mdash; switch to Full Quote to see them.';
} else { } else {
form.classList.remove('quote-simple-mode'); form.classList.remove('quote-simple-mode');
hint.textContent = ''; hint.textContent = '';
+58 -10
View File
@@ -209,11 +209,20 @@ function wizardNext() {
if (wz.step === 1) { if (wz.step === 1) {
wz.step = 2; wz.step = 2;
} else if (wz.step === 2) { } else if (wz.step === 2) {
// Generic / labor / sales: step 2 is the last step // Labor / sales: step 2 is the last step
if (wz.itemType === 'generic' || wz.itemType === 'labor' || wz.itemType === 'sales') { if (wz.itemType === 'labor' || wz.itemType === 'sales') {
wizardSave(); wizardSave();
return; 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 // AI: step 2 must be accepted before proceeding
if (wz.itemType === 'ai') { if (wz.itemType === 'ai') {
if (!wz.ai.accepted) { if (!wz.ai.accepted) {
@@ -223,6 +232,11 @@ function wizardNext() {
} }
wz.step = 3; wz.step = 3;
} else if (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 // Step 4 (prep services) for product, calculated, and ai items — always shown
// so users with no prep services configured see the empty-state prompt // so users with no prep services configured see the empty-state prompt
wz.step = 4; wz.step = 4;
@@ -615,6 +629,18 @@ function renderGenericFields() {
<label class="form-label fw-semibold">Notes <small class="text-muted">(optional)</small></label> <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"> <input type="text" class="form-control" id="wz_notes" placeholder="Internal notes about this charge…" maxlength="500">
</div> </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>`; </div>`;
} }
@@ -1795,7 +1821,7 @@ function addCoatRow() {
const coats = wz.data.coats = wz.data.coats || []; const coats = wz.data.coats = wz.data.coats || [];
const i = coats.length; const i = coats.length;
const defaultName = i === 0 ? 'Base Coat' : ''; 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); coats.push(coat);
const container = document.getElementById('coatsListContainer'); const container = document.getElementById('coatsListContainer');
@@ -1816,7 +1842,7 @@ function buildCoatNameHtml(i, currentName) {
`<option value="${n}"${selectVal === n ? ' selected' : ''}>${n}</option>` `<option value="${n}"${selectVal === n ? ' selected' : ''}>${n}</option>`
).join(''); ).join('');
return ` 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})"> id="coat_name_sel_${i}" onchange="onCoatNameSelect(${i})">
<option value="">-- Select --</option> <option value="">-- Select --</option>
${options} ${options}
@@ -1932,6 +1958,7 @@ function buildCoatRowHtml(i, coat) {
placeholder="Lookup from catalog (color name or SKU)…" placeholder="Lookup from catalog (color name or SKU)…"
oninput="customPowderCatalogInput(${i})" oninput="customPowderCatalogInput(${i})"
onkeydown="if(event.key==='Escape'){customPowderCatalogClose(${i})}" onkeydown="if(event.key==='Escape'){customPowderCatalogClose(${i})}"
onblur="catalogQBlur(${i})"
autocomplete="off"> autocomplete="off">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="customPowderCatalogClose(${i})" title="Clear lookup"> <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> <i class="bi bi-x" style="font-size:.8rem;"></i>
@@ -1945,8 +1972,8 @@ function buildCoatRowHtml(i, coat) {
</div> </div>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<label class="form-label form-label-sm">Color Name</label> <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"> <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>
<div class="col-sm-3"> <div class="col-sm-3">
<label class="form-label form-label-sm">Color Code</label> <label class="form-label form-label-sm">Color Code</label>
@@ -2265,6 +2292,19 @@ function customPowderCatalogClose(i) {
if (qEl) qEl.value = ''; 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) { function applyCustomCatalogResult(i, r) {
customPowderCatalogClose(i); customPowderCatalogClose(i);
// Fill in the custom fields from the catalog result // Fill in the custom fields from the catalog result
@@ -2967,9 +3007,12 @@ function buildCardHtml(item, i) {
const orderBadge = (!c.inventoryItemId && c.powderToOrder) 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>` ? ` <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>` : ''; 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"> 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>`; </div>`;
}).join(''); }).join('');
@@ -3352,7 +3395,9 @@ function updateWizardButtons() {
if (!btnBack || !btnNext || !btnSave) return; if (!btnBack || !btnNext || !btnSave) return;
const isLast = (wz.step === 4 && wz.itemType !== 'calculated' && wz.itemType !== 'ai') 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; const isCatalogStep = wz.step === 5;
btnBack.classList.toggle('d-none', wz.step === 1); btnBack.classList.toggle('d-none', wz.step === 1);
@@ -3369,7 +3414,8 @@ function updateStepDots() {
const step2Line = document.getElementById('step2Line'); const step2Line = document.getElementById('step2Line');
const step3Line = document.getElementById('step3Line'); 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 (step3Dot) step3Dot.classList.toggle('skip', !hasCoats);
if (step4Dot) step4Dot.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'); 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 currentDisplay = Math.min(wz.step, totalSteps);
const label = document.getElementById('wizardStepLabel'); const label = document.getElementById('wizardStepLabel');
if (label) label.textContent = wz.step === 5 ? 'Final Step' : `Step ${currentDisplay} of ${totalSteps}`; if (label) label.textContent = wz.step === 5 ? 'Final Step' : `Step ${currentDisplay} of ${totalSteps}`;