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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user