ed35362c7a
- Formula Library ratings: thumbs up/down per company per formula; toggle on/off; sorts by net score; own formulas not rateable; FormulaLibraryRating entity + migration AddFormulaLibraryRatings - Job Profitability report: actual labor cost (logged hours x StandardLaborRate) vs powder cost vs billed price per job; gross margin % color-coded; time-tracked-only filter; totals footer - Quote Revision History: track Total price changes on every save; log Sent/Resent events with recipient email; replace flat table with grouped timeline UI (icons per event type, total-change badge on header) - Setup Wizard: cap CompletedCount at TotalSteps so old 10-step data no longer shows 10/5 - Formula Library card: fix badge overflow on long titles; add Rate: label to make voting buttons discoverable - Help docs and AI knowledge base updated for all three features Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
254 lines
14 KiB
Plaintext
254 lines
14 KiB
Plaintext
@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-start align-items-sm-center justify-content-between flex-column flex-sm-row gap-3 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 flex-shrink-0">
|
|
<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-12 col-md-1">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-funnel-fill"></i><span class="d-md-none ms-1">Apply Filters</span>
|
|
</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 — min-w-0 on both sides prevents long titles from pushing badges off-card *@
|
|
<div class="d-flex align-items-start gap-2 mb-2" style="min-width:0">
|
|
<div class="flex-grow-1" style="min-width:0;overflow:hidden">
|
|
<h6 class="fw-semibold mb-0 text-truncate" title="@item.Name">@item.Name</h6>
|
|
<small class="text-muted text-truncate d-block">
|
|
<i class="bi bi-building me-1"></i>@item.SourceCompanyName
|
|
</small>
|
|
</div>
|
|
<div class="d-flex flex-column align-items-end gap-1" style="flex-shrink:0;max-width:50%">
|
|
@if (item.OutputMode == "FixedRate")
|
|
{
|
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle text-nowrap">Fixed Rate</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-info-subtle text-info border border-info-subtle text-nowrap">Surface Area</span>
|
|
}
|
|
@if (item.IsOwnFormula)
|
|
{
|
|
<span class="badge bg-warning-subtle text-warning border border-warning-subtle text-nowrap">
|
|
<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 text-nowrap">
|
|
<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 gap-2">
|
|
@* Left: import count + rating buttons *@
|
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
<small class="text-muted text-nowrap">
|
|
<i class="bi bi-download me-1"></i>@item.ImportCount import@(item.ImportCount == 1 ? "" : "s")
|
|
</small>
|
|
@if (!item.IsOwnFormula)
|
|
{
|
|
<div class="d-flex align-items-center gap-1"
|
|
data-rating-group="@item.Id"
|
|
title="Rate this formula">
|
|
<span class="text-muted small me-1">Rate:</span>
|
|
<button type="button"
|
|
class="btn btn-sm btn-vote @(item.MyVote == true ? "btn-success active-vote" : "btn-outline-secondary")"
|
|
data-item-id="@item.Id"
|
|
data-is-positive="true"
|
|
title="Helpful"
|
|
aria-label="Thumbs up">
|
|
<i class="bi bi-hand-thumbs-up"></i>
|
|
<span class="vote-up-count ms-1">@item.ThumbsUp</span>
|
|
</button>
|
|
<button type="button"
|
|
class="btn btn-sm btn-vote @(item.MyVote == false ? "btn-danger active-vote" : "btn-outline-secondary")"
|
|
data-item-id="@item.Id"
|
|
data-is-positive="false"
|
|
title="Not helpful"
|
|
aria-label="Thumbs down">
|
|
<i class="bi bi-hand-thumbs-down"></i>
|
|
<span class="vote-down-count ms-1">@item.ThumbsDown</span>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
@* Right: action button *@
|
|
@if (item.IsOwnFormula)
|
|
{
|
|
<a asp-controller="CompanySettings" asp-action="Index" asp-fragment="custom-formulas"
|
|
class="btn btn-sm btn-outline-warning flex-shrink-0">
|
|
<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 flex-shrink-0"
|
|
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 modal-dialog-scrollable">
|
|
<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>
|
|
}
|