Add Formula Library ratings, Job Profitability report, and Quote Revision History improvements

- 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>
This commit is contained in:
2026-06-01 09:02:07 -04:00
parent 81119035c7
commit ed35362c7a
24 changed files with 12273 additions and 75 deletions
@@ -91,32 +91,32 @@
<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">
@* 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">
<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 flex-shrink-0">
<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">Fixed Rate</span>
<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">Surface Area</span>
<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">
<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">
<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>
}
@@ -156,21 +156,51 @@
}
@* 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>
<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">
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"
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)