Add Community Formula Library feature
Companies can now share their custom formula templates to a platform-wide community library. Other tenants can browse, preview, and import formulas as independent local copies. Includes attribution (source company name), "Inspired by" lineage for re-contributed formulas, import counts, own-formula badge, cascade diagram nullification, and AI assistant + help docs updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2177,6 +2177,10 @@
|
||||
<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>
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-controller="FormulaLibrary" asp-action="Index"
|
||||
class="btn btn-outline-info btn-sm">
|
||||
<i class="bi bi-collection me-1"></i>Community Library
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="cfShowWalkthrough()">
|
||||
<i class="bi bi-question-circle me-1"></i>How it works
|
||||
</button>
|
||||
@@ -2190,6 +2194,8 @@
|
||||
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.
|
||||
Browse the <a asp-controller="FormulaLibrary" asp-action="Index">Community Library</a> to import
|
||||
formulas shared by other shops.
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle" id="cfTemplatesTable">
|
||||
@@ -2199,11 +2205,12 @@
|
||||
<th>Output Mode</th>
|
||||
<th>Fields</th>
|
||||
<th>Active</th>
|
||||
<th>Library</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cfTemplatesBody">
|
||||
<tr><td colspan="5" class="text-muted text-center py-3">Loading…</td></tr>
|
||||
<tr><td colspan="6" class="text-muted text-center py-3">Loading…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -2212,6 +2219,52 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Share modal lives inside the AllowCustomFormulas block so it is always in the DOM
|
||||
when the Share button can appear — prevents stale-cache mismatches. *@
|
||||
@if (ViewBag.AllowCustomFormulas == true)
|
||||
{
|
||||
<div class="modal fade" id="cfShareModal" tabindex="-1" aria-labelledby="cfShareModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="cfShareModalLabel">
|
||||
<i class="bi bi-collection me-2 text-info"></i>Share to Community Library
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="cfShareTemplateId" value="0" />
|
||||
<p class="text-muted small mb-3">
|
||||
Your formula will be visible to all Powder Coating Logix users and can be imported
|
||||
into their local library. You can remove it from the community library at any time —
|
||||
anyone who has already imported it will keep their copy.
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Tags <small class="text-muted">(optional, comma-separated)</small></label>
|
||||
<input type="text" class="form-control" id="cfShareTags"
|
||||
placeholder="e.g. HVAC, Sheet Metal, Enclosures" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Industry Hint <small class="text-muted">(optional)</small></label>
|
||||
<input type="text" class="form-control" id="cfShareIndustryHint"
|
||||
placeholder="e.g. HVAC, Automotive, Structural" />
|
||||
</div>
|
||||
<div id="cfShareInspiredBy" class="alert alert-light border fst-italic small py-2" style="display:none">
|
||||
<i class="bi bi-diagram-2 me-1"></i>
|
||||
This formula will be listed as “Inspired by” the original community entry.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-info text-white" id="cfShareConfirmBtn" onclick="cfConfirmShare()">
|
||||
<i class="bi bi-collection me-1"></i>Share to Library
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2283,7 +2336,9 @@
|
||||
</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" />
|
||||
<textarea id="cfFormula" class="form-control font-monospace" rows="3"
|
||||
style="resize:vertical;min-height:4rem"
|
||||
placeholder="e.g. 2*(L*W + L*H + W*H)/144 * rate"></textarea>
|
||||
<div class="form-text mt-1">
|
||||
<span class="me-1">Variables (click to insert):</span>
|
||||
<span id="cfVariablePills"></span>
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
@model IEnumerable<PowderCoating.Application.DTOs.Company.FormulaLibraryCardDto>
|
||||
@using PowderCoating.Application.DTOs.Company
|
||||
@{
|
||||
ViewData["Title"] = "Community Formula Library";
|
||||
var search = ViewBag.Search as string;
|
||||
var outputMode = ViewBag.OutputMode as string;
|
||||
var industryHint = ViewBag.IndustryHint as string;
|
||||
var totalCount = (int)(ViewBag.TotalCount ?? 0);
|
||||
}
|
||||
|
||||
<div class="container-fluid px-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-collection me-2 text-primary"></i>Community Formula Library
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Browse and import pricing formulas shared by the Powder Coating Logix community.</p>
|
||||
</div>
|
||||
<a asp-controller="CompanySettings" asp-action="Index" asp-fragment="custom-formulas"
|
||||
class="btn btn-outline-secondary">
|
||||
<i class="bi bi-gear me-1"></i>My Formulas
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@* Search + Filter Bar *@
|
||||
<div class="card mb-4 border-0 shadow-sm">
|
||||
<div class="card-body py-3">
|
||||
<form method="get" asp-action="Index" class="row g-2 align-items-end">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label small fw-semibold mb-1">Search</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" name="search" value="@search"
|
||||
class="form-control" placeholder="Name, description, tags, company…" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small fw-semibold mb-1">Output Mode</label>
|
||||
<select name="outputMode" class="form-select">
|
||||
<option value="">All modes</option>
|
||||
<option value="FixedRate" selected="@(outputMode == "FixedRate")">Fixed Rate</option>
|
||||
<option value="SurfaceAreaSqFt" selected="@(outputMode == "SurfaceAreaSqFt")">Surface Area (sq ft)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small fw-semibold mb-1">Industry</label>
|
||||
<input type="text" name="industryHint" value="@industryHint"
|
||||
class="form-control" placeholder="HVAC, Automotive…" />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-funnel-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Results header *@
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="text-muted small">
|
||||
@totalCount formula@(totalCount == 1 ? "" : "s") in the library
|
||||
@if (!string.IsNullOrWhiteSpace(search) || !string.IsNullOrWhiteSpace(outputMode) || !string.IsNullOrWhiteSpace(industryHint))
|
||||
{
|
||||
<span>— <a asp-action="Index" class="text-decoration-none">clear filters</a></span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-collection display-4 text-muted mb-3 d-block"></i>
|
||||
<h5 class="text-muted">No formulas found</h5>
|
||||
@if (!string.IsNullOrWhiteSpace(search) || !string.IsNullOrWhiteSpace(outputMode))
|
||||
{
|
||||
<p class="text-muted mb-0">Try broadening your search or <a asp-action="Index">view all formulas</a>.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted mb-0">Be the first to share a formula from <a asp-controller="CompanySettings" asp-action="Index" asp-fragment="custom-formulas">your templates</a>!</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-3" id="libraryGrid">
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<div class="card h-100 border-0 shadow-sm formula-card @(item.IsOwnFormula ? "border-start border-warning border-3" : item.AlreadyImported ? "border-start border-success border-3" : "")">
|
||||
<div class="card-body d-flex flex-column">
|
||||
|
||||
@* Header row *@
|
||||
<div class="d-flex align-items-start gap-2 mb-2">
|
||||
<div class="flex-grow-1 min-w-0">
|
||||
<h6 class="fw-semibold mb-0 text-truncate" title="@item.Name">@item.Name</h6>
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-building me-1"></i>@item.SourceCompanyName
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-end gap-1 flex-shrink-0">
|
||||
@if (item.OutputMode == "FixedRate")
|
||||
{
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">Fixed Rate</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-info-subtle text-info border border-info-subtle">Surface Area</span>
|
||||
}
|
||||
@if (item.IsOwnFormula)
|
||||
{
|
||||
<span class="badge bg-warning-subtle text-warning border border-warning-subtle">
|
||||
<i class="bi bi-star-fill me-1"></i>Your Formula
|
||||
</span>
|
||||
}
|
||||
else if (item.AlreadyImported)
|
||||
{
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">
|
||||
<i class="bi bi-check-lg me-1"></i>Imported
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Description *@
|
||||
@if (!string.IsNullOrWhiteSpace(item.Description))
|
||||
{
|
||||
<p class="text-muted small mb-2 flex-grow-1" style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">
|
||||
@item.Description
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="flex-grow-1"></div>
|
||||
}
|
||||
|
||||
@* Inspired by *@
|
||||
@if (!string.IsNullOrWhiteSpace(item.InspiredByName))
|
||||
{
|
||||
<p class="text-muted small mb-2 fst-italic">
|
||||
<i class="bi bi-diagram-2 me-1"></i>Inspired by
|
||||
“@item.InspiredByName” from @item.InspiredByCompanyName
|
||||
</p>
|
||||
}
|
||||
|
||||
@* Tags *@
|
||||
@if (!string.IsNullOrWhiteSpace(item.Tags))
|
||||
{
|
||||
<div class="mb-2">
|
||||
@foreach (var tag in item.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle me-1">@tag.Trim()</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Footer row *@
|
||||
<div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-download me-1"></i>@item.ImportCount import@(item.ImportCount == 1 ? "" : "s")
|
||||
</small>
|
||||
@if (item.IsOwnFormula)
|
||||
{
|
||||
<a asp-controller="CompanySettings" asp-action="Index" asp-fragment="custom-formulas"
|
||||
class="btn btn-sm btn-outline-warning">
|
||||
<i class="bi bi-gear me-1"></i><span>Manage</span>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button"
|
||||
class="btn btn-sm @(item.AlreadyImported ? "btn-outline-success" : "btn-outline-primary") btn-import"
|
||||
data-item-id="@item.Id"
|
||||
data-item-name="@item.Name">
|
||||
@if (item.AlreadyImported)
|
||||
{
|
||||
<i class="bi bi-check-lg me-1"></i><span>Already Imported</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bi bi-cloud-download me-1"></i><span>Preview & Import</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Import Preview Modal *@
|
||||
<div class="modal fade" id="importModal" tabindex="-1" aria-labelledby="importModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="importModalLabel">
|
||||
<i class="bi bi-cloud-download me-2"></i>Import Formula
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="importModalBody">
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<p class="mt-2 text-muted">Loading formula details…</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="btnConfirmImport" disabled>
|
||||
<i class="bi bi-cloud-download me-1"></i>Import to My Formulas
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/formula-library.js" asp-append-version="true"></script>
|
||||
}
|
||||
@@ -141,6 +141,59 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5"><i class="bi bi-collection text-primary me-2"></i>Community Formula Library</h2>
|
||||
<p>
|
||||
The Community Formula Library lets companies share their custom templates with the entire
|
||||
Powder Coating Logix user base. Any company can browse published formulas, preview the
|
||||
fields and expression, and import a copy into their own library in one click.
|
||||
</p>
|
||||
<p>
|
||||
Access the library from <strong>Company Settings → Custom Formulas → Community Library</strong>.
|
||||
</p>
|
||||
|
||||
<h3 class="h6 mt-3">Sharing a formula</h3>
|
||||
<ol>
|
||||
<li>Open <strong>Company Settings → Custom Formulas</strong>.</li>
|
||||
<li>Find the template you want to share. The <strong>Library</strong> column shows its current status.</li>
|
||||
<li>Click <strong>Share</strong>. Optionally add comma-separated <strong>Tags</strong> and an <strong>Industry Hint</strong> to help others discover it.</li>
|
||||
<li>Click <strong>Publish to Library</strong>. The template is immediately visible to all users.</li>
|
||||
</ol>
|
||||
<p class="text-muted small">
|
||||
Only templates you created fresh, or imported templates you have since modified, are eligible to share.
|
||||
Unmodified copies of someone else’s formula cannot be re-published (this keeps the library from filling with duplicates).
|
||||
</p>
|
||||
|
||||
<h3 class="h6 mt-3">Browsing and importing</h3>
|
||||
<ol>
|
||||
<li>Open the library via the <strong>Community Library</strong> button on the Custom Formulas tab.</li>
|
||||
<li>Use the search bar, <strong>Output Mode</strong> filter, or <strong>Industry</strong> field to narrow results.</li>
|
||||
<li>Click <strong>Preview & Import</strong> on any card to see the full formula, fields, and diagram before committing.</li>
|
||||
<li>Click <strong>Import to My Formulas</strong> in the preview modal. A private copy is added to your local template library.</li>
|
||||
</ol>
|
||||
<p class="text-muted small">
|
||||
Imported copies are fully independent — you can edit, rename, or delete them without affecting the original.
|
||||
If the original creator removes their diagram image, the image is also cleared from your copy automatically.
|
||||
</p>
|
||||
|
||||
<h3 class="h6 mt-3">Attribution & “Inspired by”</h3>
|
||||
<p>
|
||||
Every library card shows the <strong>source company name</strong> so credit stays with the original creator.
|
||||
If you import a formula, modify it, and then share your version back to the community, your card will display
|
||||
an <em>“Inspired by …”</em> line crediting the formula it was derived from.
|
||||
</p>
|
||||
|
||||
<h3 class="h6 mt-3">Your own shared formulas</h3>
|
||||
<p>
|
||||
Formulas your company has published appear in the library with a gold <strong>Your Formula</strong> badge.
|
||||
Clicking <strong>Manage</strong> on your own card takes you back to Company Settings where you can edit or unshare it.
|
||||
To remove a formula from the community library, click <strong>Unshare</strong> in the Library column —
|
||||
it disappears from the browse page immediately, but anyone who already imported it keeps their copy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">NCalc formula reference</h2>
|
||||
|
||||
@@ -81,6 +81,10 @@
|
||||
asp-controller="Help" asp-action="Settings">
|
||||
<i class="bi bi-gear"></i> Settings
|
||||
</a>
|
||||
<a class="nav-link py-2 px-3 d-flex align-items-center gap-2 @(currentAction == "CustomFormulaTemplates" ? "active fw-semibold text-primary" : "text-body")"
|
||||
asp-controller="Help" asp-action="CustomFormulaTemplates">
|
||||
<i class="bi bi-calculator"></i> Custom Formulas
|
||||
</a>
|
||||
|
||||
<div class="px-3 pt-2 pb-1">
|
||||
<span class="text-muted text-uppercase" style="font-size:.65rem; letter-spacing:.07em; font-weight:600;">Account</span>
|
||||
|
||||
@@ -1504,7 +1504,7 @@ var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports")
|
||||
{
|
||||
<li><a class="dropdown-item" asp-controller="CompanyUsers" asp-action="Index"><i class="bi bi-people-fill me-2"></i>Manage Users</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="PricingTiers" asp-action="Index"><i class="bi bi-tags me-2"></i>Pricing Tiers</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="TaxRates" asp-action="Index"><i class="bi bi-percent me-2"></i>Tax Rates</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="TaxRates" asp-action="Index"><i class="bi bi-percent me-2"></i>Tax Rates</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="Kiosk" asp-action="Activate"><i class="bi bi-tablet me-2"></i>Kiosk Setup</a></li>
|
||||
}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
Reference in New Issue
Block a user