Fix oven batch conversion, invoice quantity, AI photo pricing, and enforce pricing flag propagation

- Carry OvenBatches/OvenCycleMinutes from Quote → Job entity (was missing fields; all job pricing recalcs hardcoded 1/null)
- Fix invoice creation from job always showing Quantity=1 (was using TotalPrice as UnitPrice with qty 1)
- Add IsAiItem to JobItem + migration; map in all 3 JobItemAssemblyService.CreateJobItem overloads so AI photo jobs no longer double-price on first edit after quote→job conversion
- Propagate IsAiItem through all existingItemsData JSON blocks in Jobs views (Edit, EditItems, Create) so the wizard preserves AI routing on re-edit
- Add PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem structural test + 3 behavioral IsAiItem tests to JobItemAssemblyServiceTests
- Consolidate item wizard partials (_ItemWizardModal, _SqFtCalculatorModal) and item-wizard.css into shared locations
- Document pricing flag propagation checklist in CLAUDE.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 16:54:22 -04:00
parent 7e79a13cb1
commit 539c6c2559
24 changed files with 22175 additions and 994 deletions
@@ -0,0 +1,44 @@
/* Item Wizard — shared styles used by all views that host the wizard modal */
/* Step indicator dots and connector lines */
.wizard-step-dot {
width: 22px; height: 22px; border-radius: 50%;
background: #dee2e6; display: inline-block; cursor: default;
border: 2px solid #dee2e6; transition: all .2s; flex-shrink: 0;
}
.wizard-step-dot.active { background: #0d6efd; border-color: #0d6efd; }
.wizard-step-dot.done { background: #198754; border-color: #198754; }
.wizard-step-dot.skip { background: #adb5bd; border-color: #adb5bd; }
.wizard-step-line { flex: 1; height: 2px; background: #dee2e6; min-width: 30px; }
/* Item type picker cards (Step 1) */
.item-type-card {
border: 2px solid #dee2e6; border-radius: .75rem; padding: 1.25rem 1rem;
cursor: pointer; transition: all .15s; text-align: center;
background: #fff; user-select: none;
}
.item-type-card:hover { border-color: #86b7fe; background: #f0f6ff; }
.item-type-card.selected { border-color: #0d6efd; background: #eef3ff; }
.item-type-card .item-type-icon { font-size: 2rem; margin-bottom: .5rem; }
[data-bs-theme="dark"] .item-type-card { background: var(--bs-tertiary-bg); border-color: var(--bs-border-color); color: var(--bs-body-color); }
[data-bs-theme="dark"] .item-type-card:hover { border-color: #86b7fe; background: var(--bs-secondary-bg); }
[data-bs-theme="dark"] .item-type-card.selected { border-color: #0d6efd; background: #1a2a4a; }
/* Catalog listbox — custom scrollable list replacing a native <select> */
.catalog-list-item { cursor: pointer; border-bottom: 1px solid var(--bs-border-color); font-size: .9rem; transition: background .1s; }
.catalog-list-item:last-child { border-bottom: none; }
.catalog-list-item:hover { background: var(--bs-tertiary-bg); }
.catalog-list-item.selected { background: #eef3ff; color: #0d6efd; font-weight: 600; }
[data-bs-theme="dark"] .catalog-list-item.selected { background: #1a2a4a; color: #86b7fe; }
/* Summary item cards (displayed below wizard after adding items) */
.quote-item-card {
border: 1px solid #dee2e6; border-radius: .5rem;
padding: .75rem 1rem; margin-bottom: .5rem; background: #fafafa;
}
.quote-item-card .item-badge { font-size: .7rem; }
[data-bs-theme="dark"] .quote-item-card { background: var(--bs-tertiary-bg); border-color: var(--bs-border-color); color: var(--bs-body-color); }
/* Coat rows inside the wizard Step 3 */
.coat-row { border: 1px solid #dee2e6; border-radius: .5rem; padding: .75rem; margin-bottom: .5rem; }
[data-bs-theme="dark"] .coat-row { background: var(--bs-tertiary-bg); border-color: var(--bs-border-color); }
@@ -3426,3 +3426,53 @@ function loadItemsFromTemplate(templateItems) {
renderAllCards();
scheduleAutoPricing();
}
// ── Surface area calculator modal ─────────────────────────────────────────────
let _sqFtTargetInput = null;
function openSqFtCalculator(inputId) {
_sqFtTargetInput = inputId;
document.getElementById('rectLength').value = 0;
document.getElementById('rectWidth').value = 0;
document.getElementById('calcResult').textContent = '0.00';
new bootstrap.Modal(document.getElementById('sqFtCalculatorModal')).show();
}
function toggleShapeInputs() {
const shape = document.getElementById('calcShape').value;
document.getElementById('rectangleInputs').style.display = shape === 'rectangle' ? 'block' : 'none';
document.getElementById('cylinderInputs').style.display = shape === 'cylinder' ? 'block' : 'none';
document.getElementById('circleInputs').style.display = shape === 'circle' ? 'block' : 'none';
calculateSqFt();
}
function calculateSqFt() {
const useMetric = !!(pageMeta && pageMeta.useMetric);
const divisor = useMetric ? 10000 : 144;
const shape = document.getElementById('calcShape').value;
let result = 0;
if (shape === 'rectangle') {
const l = parseFloat(document.getElementById('rectLength').value) || 0;
const w = parseFloat(document.getElementById('rectWidth').value) || 0;
result = (l * w) / divisor;
} else if (shape === 'cylinder') {
const d = parseFloat(document.getElementById('cylDiameter').value) || 0;
const h = parseFloat(document.getElementById('cylHeight').value) || 0;
const r = d / 2;
result = (2 * Math.PI * r * r + 2 * Math.PI * r * h) / divisor;
} else {
const d = parseFloat(document.getElementById('circDiameter').value) || 0;
const r = d / 2;
result = (Math.PI * r * r) / divisor;
}
document.getElementById('calcResult').textContent = result.toFixed(4);
}
function useSqFtResult() {
const val = document.getElementById('calcResult').textContent;
if (_sqFtTargetInput) {
const el = document.getElementById(_sqFtTargetInput) || document.querySelector(`[name="${_sqFtTargetInput}"]`);
if (el) { el.value = parseFloat(val).toFixed(2); el.dispatchEvent(new Event('change')); }
}
bootstrap.Modal.getInstance(document.getElementById('sqFtCalculatorModal'))?.hide();
}