Add Custom Powder Order line item and fix CSV import FinalPrice crash
Custom powder/incoming powder material cost now flows into a separate auto-generated 'Custom Powder Order' line item instead of rolling into individual item prices, so users can add shipping charges before the customer sees the total. A dashed yellow preview card in the wizard shows the material cost and lets users edit the total (including shipping) before saving. After first save the price is user-owned. Also fixes a fatal CSV import crash when FinalPrice contains a non-numeric value (e.g. 'false' from a spreadsheet formula): the job CSV importer now streams rows one at a time with a lenient decimal converter, treating bad values as $0 with a per-row warning instead of aborting the entire import. Updated HelpKnowledgeBase.cs and Help articles (Jobs, Quotes) with Custom Powder Order behavior and a new Data Import / Export section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,13 @@
|
||||
|
||||
let quoteItems = []; // Array of item objects matching CreateQuoteItemDto shape
|
||||
|
||||
// Custom Powder Order preview state — tracks the auto-calculated material cost and any
|
||||
// user override (e.g. adding shipping). Reset when the server says no powder is needed.
|
||||
let _customPowderAutoAmount = 0; // last server-calculated material cost
|
||||
let _customPowderUserAmount = null; // null = use auto; number = user override
|
||||
let _customPowderPreviewLabel = 'Custom Powder Order';
|
||||
let _pricingPayloadHadUserPowderItem = false; // set just before each pricing fetch
|
||||
|
||||
const wz = { // Wizard state
|
||||
step: 1,
|
||||
editIndex: -1, // -1 = new item; >= 0 = editing
|
||||
@@ -903,7 +910,13 @@ function renderFormulaFields() {
|
||||
</div>
|
||||
</div>
|
||||
<div id="wz_formula_fields">${fieldsHtml}</div>
|
||||
<div id="err_formulaCalc" class="text-danger small mt-1 d-none">Please calculate the formula first.</div>`;
|
||||
<div id="err_formulaCalc" class="text-danger small mt-1 d-none">Please calculate the formula first.</div>
|
||||
<div class="form-text mt-2 text-muted" style="font-size:0.75rem;">
|
||||
<i class="bi bi-info-circle me-1"></i>Shop rates automatically applied:
|
||||
<code class="ms-1">standard_labor_rate</code>
|
||||
<code class="ms-1">additional_coat_labor_pct</code>
|
||||
<code class="ms-1">markup_pct</code>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
window.wzFormulaTemplateChanged = function () {
|
||||
@@ -2942,6 +2955,8 @@ function buildCardHtml(item, i) {
|
||||
? `${item.quantity} hr${item.quantity !== 1 ? 's' : ''}`
|
||||
: item.isSalesItem
|
||||
? `$${fmtNum(item.manualUnitPrice)} × ${item.quantity}${item.sku ? ` · <span class="text-muted small">${escHtml(item.sku)}</span>` : ''}`
|
||||
: item.isCustomFormulaItem && item.manualUnitPrice != null
|
||||
? `Total: $${fmtNum(item.manualUnitPrice)}${item.quantity > 1 ? ` · Qty: ${item.quantity}` : ''}`
|
||||
: item.surfaceAreaSqFt
|
||||
? `${item.quantity} × ${fmtNum(item.surfaceAreaSqFt)} ${pageMeta.areaUnit || 'sq ft'}`
|
||||
: `Qty: ${item.quantity}`;
|
||||
@@ -3095,6 +3110,27 @@ function writeHiddenFields() {
|
||||
});
|
||||
});
|
||||
|
||||
// When a Custom Powder Order is pending (preview visible), include it as a submitted item
|
||||
// so the server uses this price (user's override or auto amount) and skips auto-creation.
|
||||
if (_customPowderAutoAmount > 0) {
|
||||
const amount = _customPowderUserAmount ?? _customPowderAutoAmount;
|
||||
const n = quoteItems.length;
|
||||
const prefix = pageMeta.itemsFieldPrefix || 'QuoteItems';
|
||||
const p = `${prefix}[${n}]`;
|
||||
fields.push(h(p + '.Description', _customPowderPreviewLabel));
|
||||
fields.push(h(p + '.Quantity', 1));
|
||||
fields.push(h(p + '.IsGenericItem', 'true'));
|
||||
fields.push(h(p + '.ManualUnitPrice', amount.toFixed(2)));
|
||||
fields.push(h(p + '.SurfaceAreaSqFt', 0));
|
||||
fields.push(h(p + '.EstimatedMinutes', 0));
|
||||
fields.push(h(p + '.IsLaborItem', 'false'));
|
||||
fields.push(h(p + '.IsSalesItem', 'false'));
|
||||
fields.push(h(p + '.RequiresSandblasting', 'false'));
|
||||
fields.push(h(p + '.RequiresMasking', 'false'));
|
||||
fields.push(h(p + '.IncludePrepCost', 'false'));
|
||||
fields.push(h(p + '.Complexity', 'Simple'));
|
||||
}
|
||||
|
||||
container.innerHTML = fields.join('');
|
||||
|
||||
// Write all AI photo tempIds as top-level form fields for photo promotion on save
|
||||
@@ -3140,8 +3176,24 @@ async function runAutoPricing() {
|
||||
const ovenBatches = parseInt(document.getElementById('OvenBatches')?.value) || 1;
|
||||
const ovenCycleMinutes = parseInt(document.getElementById('OvenCycleMinutes')?.value) || null;
|
||||
|
||||
// When the user has overridden the powder order price, inject it as a virtual
|
||||
// Generic item so the server treats it as an existing Custom Powder Order and
|
||||
// prices the total at the user's amount (including any added shipping).
|
||||
const pricingItems = [...quoteItems];
|
||||
_pricingPayloadHadUserPowderItem = false;
|
||||
if (_customPowderAutoAmount > 0 && _customPowderUserAmount !== null) {
|
||||
pricingItems.push({
|
||||
description: _customPowderPreviewLabel,
|
||||
isGenericItem: true,
|
||||
manualUnitPrice: _customPowderUserAmount,
|
||||
quantity: 1,
|
||||
coats: []
|
||||
});
|
||||
_pricingPayloadHadUserPowderItem = true;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
items: quoteItems,
|
||||
items: pricingItems,
|
||||
customerId,
|
||||
taxPercent,
|
||||
discountType,
|
||||
@@ -3178,6 +3230,36 @@ function updatePricingDisplay(r) {
|
||||
const show = (id, visible) => document.getElementById(id)?.classList.toggle('d-none', !visible);
|
||||
const setText = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; };
|
||||
|
||||
// Custom Powder Order preview — server returns amount > 0 only when powder must be ordered
|
||||
// and no existing Custom Powder Order item is already in the submitted list.
|
||||
if (r.customPowderOrderAmount > 0) {
|
||||
_customPowderAutoAmount = r.customPowderOrderAmount;
|
||||
const colors = r.customPowderOrderColors || [];
|
||||
_customPowderPreviewLabel = colors.length > 0
|
||||
? `Custom Powder Order (${colors.join(', ')})`
|
||||
: 'Custom Powder Order';
|
||||
} else if (!_pricingPayloadHadUserPowderItem) {
|
||||
// Server found no powder and we didn't inject a user item — truly none needed
|
||||
_customPowderAutoAmount = 0;
|
||||
_customPowderUserAmount = null;
|
||||
}
|
||||
const hasPowderPreview = _customPowderAutoAmount > 0;
|
||||
const preview = document.getElementById('customPowderOrderPreview');
|
||||
if (preview) {
|
||||
preview.classList.toggle('d-none', !hasPowderPreview);
|
||||
if (hasPowderPreview) {
|
||||
const descEl = document.getElementById('customPowderOrderPreviewDesc');
|
||||
if (descEl) descEl.textContent = _customPowderPreviewLabel;
|
||||
const priceEl = document.getElementById('customPowderOrderPreviewPrice');
|
||||
if (priceEl) priceEl.textContent = 'Material: $' + fmtNum(_customPowderAutoAmount);
|
||||
// Only reset the input when user hasn't overridden
|
||||
if (_customPowderUserAmount === null) {
|
||||
const input = document.getElementById('customPowderOrderPriceInput');
|
||||
if (input) input.value = _customPowderAutoAmount.toFixed(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('pricingPlaceholder')?.classList.add('d-none');
|
||||
show('itemsSubtotalRow', true);
|
||||
setText('itemsSubtotalDisplay', '$' + fmtNum(r.itemsSubtotal));
|
||||
@@ -3242,6 +3324,17 @@ function resetPricingDisplay() {
|
||||
document.getElementById('pricingPlaceholder')?.classList.remove('d-none');
|
||||
['itemsSubtotalRow','ovenBatchCostRow','pricingTierDiscountRow','quoteDiscountRow','rushFeeRow',
|
||||
'shopSuppliesRow','subtotalRow','taxRow','pricingDivider','totalRow'].forEach(hide);
|
||||
document.getElementById('customPowderOrderPreview')?.classList.add('d-none');
|
||||
_customPowderAutoAmount = 0;
|
||||
_customPowderUserAmount = null;
|
||||
}
|
||||
|
||||
/// Called when the user edits the Custom Powder Order price input.
|
||||
/// Stores the override and re-runs pricing so the total reflects the new amount.
|
||||
function onCustomPowderPriceEdit(val) {
|
||||
const v = parseFloat(val);
|
||||
_customPowderUserAmount = (!val || isNaN(v) || v <= 0) ? null : v;
|
||||
scheduleAutoPricing();
|
||||
}
|
||||
|
||||
// ─── Wizard UI helpers ────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user