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
@@ -4,6 +4,62 @@
const importModal = new bootstrap.Modal(document.getElementById('importModal'));
let currentLibraryItemId = null;
// ── Rating (thumbs up / down) ─────────────────────────────────────────
document.getElementById('libraryGrid')?.addEventListener('click', function (e) {
const voteBtn = e.target.closest('.btn-vote');
if (voteBtn) {
handleVote(voteBtn);
return;
}
});
function handleVote(btn) {
const itemId = parseInt(btn.dataset.itemId, 10);
const positive = btn.dataset.isPositive === 'true';
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
btn.disabled = true;
fetch('/FormulaLibrary/Rate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': token,
},
body: JSON.stringify({ libraryItemId: itemId, isPositive: positive }),
})
.then(r => r.json())
.then(res => {
if (!res.success) { showToast(res.message || 'Rating failed.', 'danger'); return; }
// Update counts and active-vote state on both buttons in the group
const group = document.querySelector(`[data-rating-group="${itemId}"]`);
if (!group) return;
const upBtn = group.querySelector('[data-is-positive="true"]');
const downBtn = group.querySelector('[data-is-positive="false"]');
if (upBtn) upBtn.querySelector('.vote-up-count').textContent = res.thumbsUp;
if (downBtn) downBtn.querySelector('.vote-down-count').textContent = res.thumbsDown;
// Apply active styles
const myVote = res.myVote; // true / false / null
if (upBtn) {
upBtn.classList.toggle('btn-success', myVote === true);
upBtn.classList.toggle('active-vote', myVote === true);
upBtn.classList.toggle('btn-outline-secondary', myVote !== true);
}
if (downBtn) {
downBtn.classList.toggle('btn-danger', myVote === false);
downBtn.classList.toggle('active-vote', myVote === false);
downBtn.classList.toggle('btn-outline-secondary', myVote !== false);
}
})
.catch(() => showToast('Rating failed. Please try again.', 'danger'))
.finally(() => { btn.disabled = false; });
}
// ── Import modal ──────────────────────────────────────────────────────
// Open preview modal when any "Preview & Import" button is clicked
document.getElementById('libraryGrid')?.addEventListener('click', function (e) {
const btn = e.target.closest('.btn-import');