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:
@@ -106,6 +106,11 @@
|
||||
<i class="bi bi-tablet"></i> Kiosk
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="custom-formulas-tab" data-bs-toggle="tab" data-bs-target="#custom-formulas" type="button" role="tab">
|
||||
<i class="bi bi-calculator"></i> Custom Formulas
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Tabs Content -->
|
||||
@@ -2054,6 +2059,143 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Custom Formula Item Templates ──────────────────────────────── -->
|
||||
<div class="tab-pane fade" id="custom-formulas" role="tabpanel">
|
||||
<div class="card shadow-sm mt-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-calculator me-2"></i>Custom Formula Item Templates</h5>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="cfShowCreate()">
|
||||
<i class="bi bi-plus-circle me-1"></i>New Template
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-3">
|
||||
Define reusable pricing formulas for complex fabricated items (roof curbs, enclosures, frames).
|
||||
When a user adds a formula item to a quote or job, they fill in the measurements and the formula
|
||||
calculates the price automatically.
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle" id="cfTemplatesTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Output Mode</th>
|
||||
<th>Fields</th>
|
||||
<th>Active</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cfTemplatesBody">
|
||||
<tr><td colspan="5" class="text-muted text-center py-3">Loading…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Formula Template Modal -->
|
||||
<div class="modal fade" id="cfModal" tabindex="-1" aria-labelledby="cfModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="cfModalLabel">New Formula Template</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="cfId" value="0" />
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name <span class="text-danger">*</span></label>
|
||||
<input type="text" id="cfName" class="form-control" placeholder="e.g. Roof Curb" maxlength="100" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<input type="text" id="cfDescription" class="form-control" maxlength="500" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Output Mode <span class="text-danger">*</span></label>
|
||||
<select id="cfOutputMode" class="form-select" onchange="cfToggleRateFields()">
|
||||
<option value="FixedRate">Fixed Rate — formula → $ amount</option>
|
||||
<option value="SurfaceAreaSqFt">Surface Area — formula → sq ft (standard pricing engine prices it)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="cfRateFields">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Default Rate</label>
|
||||
<input type="number" id="cfDefaultRate" class="form-control" step="0.01" placeholder="e.g. 0.85" />
|
||||
<div class="form-text">Used as the <code>rate</code> variable if not overridden per-quote.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Rate Label</label>
|
||||
<input type="text" id="cfRateLabel" class="form-control" maxlength="50" placeholder="e.g. $/sq ft" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Formula <span class="text-danger">*</span></label>
|
||||
<input type="text" id="cfFormula" class="form-control font-monospace" placeholder="e.g. 2*(L*W + L*H + W*H)/144 * rate" />
|
||||
<div class="form-text">NCalc expression. Available variables: field names + <code>rate</code>.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea id="cfNotes" class="form-control" rows="2" maxlength="1000"></textarea>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" id="cfIsActive" class="form-check-input" checked />
|
||||
<label class="form-check-label" for="cfIsActive">Active (show in quote/job wizard)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Fields</label>
|
||||
<div class="form-text mb-2">Define the measurement inputs users will fill in.</div>
|
||||
<div id="cfFieldsList"></div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mt-2" onclick="cfAddField()">
|
||||
<i class="bi bi-plus"></i> Add Field
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Formula Test</label>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="cfTestFormula()">
|
||||
<i class="bi bi-play-circle"></i> Run
|
||||
</button>
|
||||
<span id="cfTestResult" class="fw-bold"></span>
|
||||
</div>
|
||||
<div class="form-text">Uses the default values from your field list.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Diagram / Shop Drawing</label>
|
||||
<div id="cfDiagramPreview" class="mb-2" style="display:none;">
|
||||
<img id="cfDiagramImg" src="" alt="Diagram" class="img-fluid rounded border" style="max-height:180px;" />
|
||||
</div>
|
||||
<input type="file" id="cfDiagramFile" class="form-control form-control-sm" accept="image/*" onchange="cfPreviewDiagram(event)" />
|
||||
<div class="form-text">Optional. Upload a shop drawing or photo to help users recognize this item.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">AI Formula Generator</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="cfAiPrompt" class="form-control" placeholder="Describe the item, e.g. 'Rectangular roof curb with flanged base'" />
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="cfGenerateFromAi()" id="cfAiBtn">
|
||||
<i class="bi bi-stars"></i> Generate
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">Claude will suggest a formula, fields, and mode. You can edit before saving.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="cfSave()">
|
||||
<i class="bi bi-floppy me-1"></i>Save Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3289,6 +3431,17 @@
|
||||
const btn = document.querySelector('[data-bs-target="#kiosk"]');
|
||||
if (btn) new bootstrap.Tab(btn).show();
|
||||
}
|
||||
if (urlParams.get('tab') === 'custom-formulas') {
|
||||
const btn = document.querySelector('[data-bs-target="#custom-formulas"]');
|
||||
if (btn) new bootstrap.Tab(btn).show();
|
||||
}
|
||||
</script>
|
||||
<script src="~/js/company-settings-custom-formulas.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
// Load formula templates when the tab is first shown
|
||||
document.querySelector('[data-bs-target="#custom-formulas"]').addEventListener('shown.bs.tab', () => {
|
||||
if (!window._cfLoaded) { cfLoadTemplates(); window._cfLoaded = true; }
|
||||
});
|
||||
</script>
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user