Add Custom Formula Item Templates with AI generation and wizard integration
Introduces per-company reusable NCalc2 pricing formula templates for complex fabricated items (roof curbs, enclosures, welded frames). Templates support two output modes — FixedRate (formula yields a dollar amount) and SurfaceAreaSqFt (formula yields sq ft fed into the standard coating engine). Includes: - CustomItemTemplate entity, migration (AddCustomItemTemplates), IUnitOfWork repo - IsCustomFormulaItem / CustomItemTemplateId / FormulaFieldValuesJson flags on QuoteItem, JobItem, CreateQuoteItemDto; mapped in all 3 JobItemAssemblyService overloads and all existingItemsData JSON projections + pageMeta blocks - ICustomFormulaAiService / CustomFormulaAiService: Claude-powered formula generator (natural language + optional diagram image) and NCalc2 evaluator - CompanySettings CRUD endpoints: GetCustomItemTemplates, Create/Update/Delete, UploadTemplateDiagram, TemplateDiagram (blob serve), EvaluateFormula, GenerateFormulaFromAi - Company Settings "Custom Formulas" tab + cfModal + company-settings-custom-formulas.js - item-wizard.js: formula item type card, renderFormulaFields, wzFormulaRecalc (live evaluate via POST), collectStep2 formula branch, buildCardHtml / emitHiddenFields - Formula badge in Quotes/Details and Jobs/Details; AI badge gap fixed in Jobs/Details - Help article (CustomFormulaTemplates.cshtml), Help Index card, HelpController action, HelpKnowledgeBase entry; 225/225 unit tests passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -489,9 +489,12 @@ public class JobsController : Controller
|
||||
manualUnitPrice = ji.ManualUnitPrice ?? (ji.IsGenericItem || ji.IsSalesItem ? ji.UnitPrice : (decimal?)null),
|
||||
powderCostOverride = ji.PowderCostOverride,
|
||||
isGenericItem = ji.IsGenericItem || (!ji.CatalogItemId.HasValue && !ji.Coats.Any() && !ji.IsSalesItem),
|
||||
isLaborItem = ji.IsLaborItem,
|
||||
isSalesItem = ji.IsSalesItem,
|
||||
isAiItem = ji.IsAiItem,
|
||||
isLaborItem = ji.IsLaborItem,
|
||||
isSalesItem = ji.IsSalesItem,
|
||||
isAiItem = ji.IsAiItem,
|
||||
isCustomFormulaItem = ji.IsCustomFormulaItem,
|
||||
customItemTemplateId = ji.CustomItemTemplateId,
|
||||
formulaFieldValuesJson = ji.FormulaFieldValuesJson,
|
||||
sku = ji.Sku,
|
||||
requiresSandblasting = ji.RequiresSandblasting,
|
||||
requiresMasking = ji.RequiresMasking,
|
||||
@@ -1279,9 +1282,12 @@ public class JobsController : Controller
|
||||
CatalogItemId = ji.CatalogItemId,
|
||||
ManualUnitPrice = ji.ManualUnitPrice ?? (ji.IsGenericItem ? ji.UnitPrice : null),
|
||||
PowderCostOverride = ji.PowderCostOverride,
|
||||
IsGenericItem = ji.IsGenericItem || (!ji.CatalogItemId.HasValue && ji.Coats.Count == 0),
|
||||
IsLaborItem = ji.IsLaborItem,
|
||||
IsAiItem = ji.IsAiItem,
|
||||
IsGenericItem = ji.IsGenericItem || (!ji.CatalogItemId.HasValue && ji.Coats.Count == 0),
|
||||
IsLaborItem = ji.IsLaborItem,
|
||||
IsAiItem = ji.IsAiItem,
|
||||
IsCustomFormulaItem = ji.IsCustomFormulaItem,
|
||||
CustomItemTemplateId = ji.CustomItemTemplateId,
|
||||
FormulaFieldValuesJson = ji.FormulaFieldValuesJson,
|
||||
RequiresSandblasting = ji.RequiresSandblasting,
|
||||
RequiresMasking = ji.RequiresMasking,
|
||||
Notes = ji.Notes,
|
||||
@@ -1852,6 +1858,25 @@ public class JobsController : Controller
|
||||
{
|
||||
ViewBag.AiPhotoQuotesEnabled = await _subscriptionService.CanUseAiPhotoQuoteAsync(companyId);
|
||||
|
||||
var formulaTemplates = await _unitOfWork.CustomItemTemplates.FindAsync(
|
||||
t => t.CompanyId == companyId && t.IsActive);
|
||||
ViewBag.CustomFormulaTemplates = formulaTemplates
|
||||
.OrderBy(t => t.DisplayOrder).ThenBy(t => t.Name)
|
||||
.Select(t => new
|
||||
{
|
||||
id = t.Id,
|
||||
name = t.Name,
|
||||
description = t.Description,
|
||||
outputMode = t.OutputMode,
|
||||
fieldsJson = t.FieldsJson,
|
||||
formula = t.Formula,
|
||||
defaultRate = t.DefaultRate,
|
||||
rateLabel = t.RateLabel,
|
||||
diagramImagePath = string.IsNullOrEmpty(t.DiagramImagePath)
|
||||
? (string?)null
|
||||
: Url.Action("TemplateDiagram", "CompanySettings", new { templateId = t.Id })
|
||||
}).ToList();
|
||||
|
||||
await PopulateDropdowns();
|
||||
await PopulatePrepServicesAsync(companyId);
|
||||
var costs = await _pricingService.GetOperatingCostsAsync(companyId);
|
||||
@@ -2981,9 +3006,12 @@ public class JobsController : Controller
|
||||
CatalogItemId = ji.CatalogItemId,
|
||||
ManualUnitPrice = ji.ManualUnitPrice ?? (ji.IsGenericItem ? ji.UnitPrice : null),
|
||||
PowderCostOverride = ji.PowderCostOverride,
|
||||
IsGenericItem = ji.IsGenericItem || (!ji.CatalogItemId.HasValue && ji.Coats.Count == 0),
|
||||
IsLaborItem = ji.IsLaborItem,
|
||||
IsAiItem = ji.IsAiItem,
|
||||
IsGenericItem = ji.IsGenericItem || (!ji.CatalogItemId.HasValue && ji.Coats.Count == 0),
|
||||
IsLaborItem = ji.IsLaborItem,
|
||||
IsAiItem = ji.IsAiItem,
|
||||
IsCustomFormulaItem = ji.IsCustomFormulaItem,
|
||||
CustomItemTemplateId = ji.CustomItemTemplateId,
|
||||
FormulaFieldValuesJson = ji.FormulaFieldValuesJson,
|
||||
RequiresSandblasting = ji.RequiresSandblasting,
|
||||
RequiresMasking = ji.RequiresMasking,
|
||||
Notes = ji.Notes,
|
||||
@@ -3172,10 +3200,13 @@ public class JobsController : Controller
|
||||
SurfaceAreaSqFt = ji.SurfaceAreaSqFt,
|
||||
EstimatedMinutes = ji.EstimatedMinutes,
|
||||
CatalogItemId = ji.CatalogItemId,
|
||||
IsGenericItem = ji.IsGenericItem,
|
||||
IsLaborItem = ji.IsLaborItem,
|
||||
IsSalesItem = ji.IsSalesItem,
|
||||
IsAiItem = ji.IsAiItem,
|
||||
IsGenericItem = ji.IsGenericItem,
|
||||
IsLaborItem = ji.IsLaborItem,
|
||||
IsSalesItem = ji.IsSalesItem,
|
||||
IsAiItem = ji.IsAiItem,
|
||||
IsCustomFormulaItem = ji.IsCustomFormulaItem,
|
||||
CustomItemTemplateId = ji.CustomItemTemplateId,
|
||||
FormulaFieldValuesJson = ji.FormulaFieldValuesJson,
|
||||
ManualUnitPrice = ji.ManualUnitPrice ?? ((ji.IsGenericItem || ji.IsSalesItem) ? ji.UnitPrice : (decimal?)null),
|
||||
IncludePrepCost = ji.IncludePrepCost,
|
||||
Coats = ji.Coats.OrderBy(c => c.Sequence).Select(c => new CreateQuoteItemCoatDto
|
||||
|
||||
Reference in New Issue
Block a user