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:
2026-06-03 15:57:46 -04:00
parent 0d5553f3b2
commit ad986561c9
4 changed files with 63 additions and 42 deletions
@@ -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,