Fix AI quote blast rate: single formula path, correct client preview
Root cause: company-settings-lookups.js had its own baseByCfm/multiplier tables that were completely different from ShopCapabilityCalculator.cs, so the UI showed an inflated rate (e.g. 82 sqft/hr) while the AI prompt received the server-computed rate (e.g. 9 sqft/hr). - Add CompanySettingsController.DeriveBlastRate endpoint — thin GET that calls ShopCapabilityCalculator directly; now the single formula path - Delete all client-side formula code (baseByCfm, multiplier tables, deriveBlastRate) — ~30 lines removed - Modal live preview calls /CompanySettings/DeriveBlastRate with 250ms debounce instead of computing locally - Blast setup table uses setup.derivedRate from GetBlastSetups (already server-computed) instead of recalculating client-side - QuotesController.AiAnalyzeItem: when no blast setup is explicitly selected, fall back to the company's default blast setup so the configured rate is always used Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1094,30 +1094,31 @@
|
||||
3: 'Powder Coat'
|
||||
};
|
||||
|
||||
// Nozzle multipliers matching ShopCapabilityCalculator
|
||||
const blastNozzleMultipliers = [0, 0, 0.35, 0.55, 0.75, 1.00, 1.30, 1.65, 2.00];
|
||||
const blastSetupModalTypeMultipliers = { 0: 0.55, 1: 0.70, 2: 1.00, 3: 0.45 };
|
||||
const blastSubstrateMultipliers = { 0: 1.00, 1: 0.80, 2: 1.40, 3: 0.90 };
|
||||
// No client-side blast-rate formula here — ShopCapabilityCalculator.cs is the single
|
||||
// source of truth. The table uses derivedRate from the server response; the modal
|
||||
// live-preview calls /CompanySettings/DeriveBlastRate instead.
|
||||
|
||||
function baseByCfm(cfm) {
|
||||
if (cfm <= 0) return 0;
|
||||
if (cfm <= 5) return 15;
|
||||
if (cfm <= 10) return 30;
|
||||
if (cfm <= 15) return 50;
|
||||
if (cfm <= 25) return 80;
|
||||
if (cfm <= 40) return 130;
|
||||
if (cfm <= 60) return 200;
|
||||
return 300;
|
||||
}
|
||||
let _deriveRateTimer = null;
|
||||
|
||||
function deriveBlastRate(cfm, nozzle, setupType, substrate, override) {
|
||||
if (override && parseFloat(override) > 0) return parseFloat(override);
|
||||
const base = baseByCfm(parseFloat(cfm) || 0);
|
||||
if (base === 0) return 0;
|
||||
const nm = blastNozzleMultipliers[parseInt(nozzle)] || 1.00;
|
||||
const sm = blastSetupModalTypeMultipliers[parseInt(setupType)] || 1.00;
|
||||
const bm = blastSubstrateMultipliers[parseInt(substrate)] || 1.00;
|
||||
return Math.round(base * nm * sm * bm * 10) / 10;
|
||||
function updateBlastSetupDerivedRate() {
|
||||
clearTimeout(_deriveRateTimer);
|
||||
_deriveRateTimer = setTimeout(function () {
|
||||
const cfm = document.getElementById('blastSetupCfm').value;
|
||||
const nozzle = document.getElementById('blastSetupNozzleSize').value;
|
||||
const setupType = document.getElementById('blastSetupModalType').value;
|
||||
const substrate = document.getElementById('blastSetupSubstrate').value;
|
||||
const override = document.getElementById('blastSetupOverride').value;
|
||||
const el = document.getElementById('blastSetupDerivedRate');
|
||||
if (!el) return;
|
||||
|
||||
const params = new URLSearchParams({ cfm, nozzle, setupType, substrate });
|
||||
if (override && parseFloat(override) > 0) params.set('rateOverride', override);
|
||||
|
||||
fetch('/CompanySettings/DeriveBlastRate?' + params)
|
||||
.then(r => r.json())
|
||||
.then(data => { el.textContent = data.rate > 0 ? data.rate + ' sqft/hr' : '—'; })
|
||||
.catch(() => { el.textContent = '—'; });
|
||||
}, 250);
|
||||
}
|
||||
|
||||
window.loadBlastSetups = function () {
|
||||
@@ -1150,7 +1151,7 @@
|
||||
window.blastSetups.forEach(function (setup) {
|
||||
const rate = setup.blastRateSqFtPerHourOverride > 0
|
||||
? setup.blastRateSqFtPerHourOverride + ' sqft/hr <span class="badge bg-secondary">Override</span>'
|
||||
: deriveBlastRate(setup.compressorCfm, setup.blastNozzleSize, setup.setupType, setup.primarySubstrate, 0) + ' sqft/hr';
|
||||
: (setup.derivedRate > 0 ? setup.derivedRate + ' sqft/hr' : '<span class="text-muted">—</span>');
|
||||
|
||||
const defaultBadge = setup.isDefault
|
||||
? ' <span class="badge bg-primary ms-1">Default</span>'
|
||||
@@ -1179,20 +1180,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function updateBlastSetupDerivedRate() {
|
||||
const cfm = document.getElementById('blastSetupCfm').value;
|
||||
const nozzle = document.getElementById('blastSetupNozzleSize').value;
|
||||
const setupType = document.getElementById('blastSetupModalType').value;
|
||||
const substrate = document.getElementById('blastSetupSubstrate').value;
|
||||
const override = document.getElementById('blastSetupOverride').value;
|
||||
|
||||
const rate = deriveBlastRate(cfm, nozzle, setupType, substrate, override);
|
||||
const el = document.getElementById('blastSetupDerivedRate');
|
||||
if (el) {
|
||||
el.textContent = rate > 0 ? rate + ' sqft/hr' : '—';
|
||||
}
|
||||
}
|
||||
|
||||
window.showBlastSetupModal = function (setupId = null) {
|
||||
const modal = new bootstrap.Modal(document.getElementById('blastSetupModal'));
|
||||
const form = document.getElementById('blastSetupForm');
|
||||
|
||||
@@ -1510,8 +1510,11 @@ async function aiAnalyze() {
|
||||
document.getElementById('ai_resultsSection')?.classList.add('d-none');
|
||||
document.getElementById('ai_errorAlert')?.classList.add('d-none');
|
||||
|
||||
const blastSetupIdEl = document.getElementById('ai_blastSetupId');
|
||||
const blastSetupId = blastSetupIdEl ? (parseInt(blastSetupIdEl.value) || null) : null;
|
||||
const blastSetupIdEl = document.getElementById('ai_blastSetupId');
|
||||
const _defaultSetup = blastSetupData.find(s => s.isDefault) || blastSetupData[0];
|
||||
const blastSetupId = blastSetupIdEl
|
||||
? (parseInt(blastSetupIdEl.value) || null)
|
||||
: (_defaultSetup ? _defaultSetup.id : null);
|
||||
|
||||
const payload = {
|
||||
photoTempIds: wz.ai.tempIds,
|
||||
@@ -1590,8 +1593,11 @@ async function aiSendFollowup() {
|
||||
const weightLbs = isNaN(weightLbsRaw) || weightLbsRaw <= 0 ? null : weightLbsRaw;
|
||||
wz.data.quantity = qty; // persist before renderStep re-renders
|
||||
|
||||
const blastSetupIdEl2 = document.getElementById('ai_blastSetupId');
|
||||
const blastSetupId2 = blastSetupIdEl2 ? (parseInt(blastSetupIdEl2.value) || null) : null;
|
||||
const blastSetupIdEl2 = document.getElementById('ai_blastSetupId');
|
||||
const _defaultSetup2 = blastSetupData.find(s => s.isDefault) || blastSetupData[0];
|
||||
const blastSetupId2 = blastSetupIdEl2
|
||||
? (parseInt(blastSetupIdEl2.value) || null)
|
||||
: (_defaultSetup2 ? _defaultSetup2.id : null);
|
||||
|
||||
const payload = {
|
||||
photoTempIds: wz.ai.tempIds,
|
||||
|
||||
Reference in New Issue
Block a user