Add oven batch cost to AI Quick Quote (1 batch, DefaultOvenCycleMinutes or 50 min)
Previously the quick quote omitted the oven charge entirely, so saved quotes were under-priced relative to full quotes from the same items. Pricing: CalculatePricing now calculates ovenBatchCost = (cycleMin/60) × OvenOperatingCostPerHour using DefaultOvenCycleMinutes (fallback 50 min), then adds it to the total as a quote-level charge matching how PricingCalculationService handles oven costs. Save path: SaveQuickQuoteRequest gains OvenBatchCost + OvenCycleMinutes; the Quote record now stores OvenBatchCost, OvenCycleMinutes, and Total = ItemsSubtotal + OvenBatchCost. Display: results card shows a sub-line under the estimate price: "incl. oven 1 batch 50 min: $12.00" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,8 @@ public class SaveQuickQuoteRequest
|
||||
public decimal EstimatedUnitPrice { get; set; }
|
||||
public decimal MaterialCost { get; set; }
|
||||
public decimal LaborCost { get; set; }
|
||||
public decimal OvenBatchCost { get; set; }
|
||||
public int OvenCycleMinutes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Internal JSON schema returned by Claude for quick quote analysis.</summary>
|
||||
|
||||
@@ -257,9 +257,14 @@ When estimating from a verbal description:
|
||||
subtotal = costs.ShopMinimumCharge;
|
||||
|
||||
var unitPrice = Math.Max(0, Math.Round(subtotal, 2));
|
||||
var total = unitPrice * request.Quantity;
|
||||
var markupAmount = (materialCost + consumablesSurcharge) * (costs.GeneralMarkupPercentage / 100m);
|
||||
var ovenCycleMinutes = costs.DefaultOvenCycleMinutes > 0 ? costs.DefaultOvenCycleMinutes : 45;
|
||||
|
||||
// Oven batch charge: 1 batch, DefaultOvenCycleMinutes (fallback 50 min).
|
||||
// Added at quote level (not baked into unitPrice) to match how the regular pricing engine works.
|
||||
var ovenCycleMinutes = costs.DefaultOvenCycleMinutes > 0 ? costs.DefaultOvenCycleMinutes : 50;
|
||||
var ovenBatchCost = Math.Round((ovenCycleMinutes / 60m) * costs.OvenOperatingCostPerHour, 2);
|
||||
|
||||
var total = unitPrice * request.Quantity + ovenBatchCost;
|
||||
|
||||
var breakdown = new AiPricingBreakdown
|
||||
{
|
||||
@@ -273,7 +278,7 @@ When estimating from a verbal description:
|
||||
MinFloorApplied = false,
|
||||
LaborCost = Math.Round(laborCost, 2),
|
||||
OvenCycleMinutes = ovenCycleMinutes,
|
||||
OvenCost = 0m,
|
||||
OvenCost = ovenBatchCost,
|
||||
RequiresPreheat = ai.RequiresPreheat,
|
||||
PreheatMinutes = preheatMinutes,
|
||||
PreheatCost = Math.Round(preheatCost, 2),
|
||||
|
||||
@@ -107,6 +107,9 @@ public class AiQuickQuoteController : Controller
|
||||
var quoteNumber = await GenerateQuoteNumberAsync(companyId);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var itemsSubtotal = request.EstimatedUnitPrice * request.Quantity;
|
||||
var ovenCycleMinutes = request.OvenCycleMinutes > 0 ? request.OvenCycleMinutes : 50;
|
||||
|
||||
var quote = new Quote
|
||||
{
|
||||
CompanyId = companyId,
|
||||
@@ -121,12 +124,14 @@ public class AiQuickQuoteController : Controller
|
||||
CustomerPO = request.Reference,
|
||||
MaterialCosts = request.MaterialCost,
|
||||
LaborCosts = request.LaborCost,
|
||||
ItemsSubtotal = request.EstimatedUnitPrice * request.Quantity,
|
||||
SubTotal = request.EstimatedUnitPrice * request.Quantity,
|
||||
Total = request.EstimatedUnitPrice * request.Quantity,
|
||||
ItemsSubtotal = itemsSubtotal,
|
||||
OvenBatches = 1,
|
||||
OvenCycleMinutes = ovenCycleMinutes,
|
||||
OvenBatchCost = request.OvenBatchCost,
|
||||
SubTotal = itemsSubtotal,
|
||||
Total = itemsSubtotal + request.OvenBatchCost,
|
||||
TaxPercent = 0,
|
||||
TaxAmount = 0,
|
||||
OvenBatches = 1
|
||||
TaxAmount = 0
|
||||
};
|
||||
|
||||
if (draftStatus != null)
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
<div>
|
||||
<div class="small text-muted">Estimate</div>
|
||||
<div class="fs-5 fw-bold text-success" id="qq-res-price"></div>
|
||||
<div id="qq-res-oven" class="text-muted" style="font-size:0.73rem;"></div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="small text-muted">Confidence</div>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
resComplexity:document.getElementById('qq-res-complexity'),
|
||||
resMinutes: document.getElementById('qq-res-minutes'),
|
||||
resPrice: document.getElementById('qq-res-price'),
|
||||
resOven: document.getElementById('qq-res-oven'),
|
||||
resConfidence:document.getElementById('qq-res-confidence'),
|
||||
resReasoning: document.getElementById('qq-res-reasoning'),
|
||||
powderSection:document.getElementById('qq-powder-section'),
|
||||
@@ -150,6 +151,15 @@
|
||||
el.resComplexity.textContent = r.complexity || '—';
|
||||
el.resMinutes.textContent = r.estimatedMinutes ? r.estimatedMinutes + ' min' : '—';
|
||||
el.resPrice.textContent = formatCurrency(r.estimatedTotal || r.estimatedUnitPrice);
|
||||
|
||||
const ovenCost = r.breakdown?.ovenCost;
|
||||
const ovenMin = r.breakdown?.ovenCycleMinutes;
|
||||
if (ovenCost && ovenCost > 0) {
|
||||
el.resOven.textContent = `incl. oven 1 batch ${ovenMin ? ovenMin + ' min' : ''}: ${formatCurrency(ovenCost)}`;
|
||||
} else {
|
||||
el.resOven.textContent = '';
|
||||
}
|
||||
|
||||
el.resReasoning.textContent = r.reasoning || '';
|
||||
|
||||
// Confidence badge
|
||||
@@ -220,7 +230,9 @@
|
||||
coatCount: parseInt(el.coats.value, 10) || 1,
|
||||
estimatedUnitPrice: lastResult.estimatedUnitPrice,
|
||||
materialCost: lastResult.breakdown?.materialCost ?? 0,
|
||||
laborCost: lastResult.breakdown?.laborCost ?? 0
|
||||
laborCost: lastResult.breakdown?.laborCost ?? 0,
|
||||
ovenBatchCost: lastResult.breakdown?.ovenCost ?? 0,
|
||||
ovenCycleMinutes: lastResult.breakdown?.ovenCycleMinutes ?? 50
|
||||
};
|
||||
|
||||
const response = await post('/AiQuickQuote/Save', body);
|
||||
|
||||
Reference in New Issue
Block a user