ca7e905832
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>
165 lines
7.8 KiB
JavaScript
165 lines
7.8 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
const importModal = new bootstrap.Modal(document.getElementById('importModal'));
|
|
let currentLibraryItemId = null;
|
|
|
|
// Open preview modal when any "Preview & Import" button is clicked
|
|
document.getElementById('libraryGrid')?.addEventListener('click', function (e) {
|
|
const btn = e.target.closest('.btn-import');
|
|
if (!btn) return;
|
|
|
|
currentLibraryItemId = parseInt(btn.dataset.itemId, 10);
|
|
const itemName = btn.dataset.itemName;
|
|
|
|
document.getElementById('importModalLabel').textContent = 'Import — ' + itemName;
|
|
document.getElementById('importModalBody').innerHTML =
|
|
'<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>';
|
|
document.getElementById('btnConfirmImport').disabled = true;
|
|
|
|
importModal.show();
|
|
|
|
fetch('/FormulaLibrary/Detail/' + currentLibraryItemId)
|
|
.then(r => r.json())
|
|
.then(renderDetail)
|
|
.catch(() => {
|
|
document.getElementById('importModalBody').innerHTML =
|
|
'<div class="alert alert-danger">Failed to load formula details.</div>';
|
|
});
|
|
});
|
|
|
|
function renderDetail(d) {
|
|
let fields = [];
|
|
try { fields = JSON.parse(d.fieldsJson || '[]'); } catch (_) { }
|
|
|
|
const alreadyBadge = d.alreadyImported
|
|
? '<span class="badge bg-success ms-2"><i class="bi bi-check-lg me-1"></i>Already in your library</span>'
|
|
: '';
|
|
|
|
const inspiredRow = (d.inspiredByName)
|
|
? `<div class="alert alert-light border fst-italic small py-2 mb-3">
|
|
<i class="bi bi-diagram-2 me-1"></i>Inspired by
|
|
“${escHtml(d.inspiredByName)}” from ${escHtml(d.inspiredByCompanyName)}
|
|
</div>`
|
|
: '';
|
|
|
|
const modeBadge = d.outputMode === 'FixedRate'
|
|
? '<span class="badge bg-primary">Fixed Rate</span>'
|
|
: '<span class="badge bg-info">Surface Area (sq ft)</span>';
|
|
|
|
const fieldRows = fields.map(f =>
|
|
`<tr><td>${escHtml(f.label || f.name)}</td><td class="text-muted">${escHtml(f.unit || '')}</td><td>${escHtml(String(f.defaultValue ?? ''))}</td></tr>`
|
|
).join('');
|
|
|
|
const diagramHtml = d.diagramImagePath
|
|
? `<div class="mb-3"><img src="/FormulaLibrary/Diagram?path=${encodeURIComponent(d.diagramImagePath)}" class="img-fluid rounded border" style="max-height:200px" alt="Formula diagram" /></div>`
|
|
: '';
|
|
|
|
document.getElementById('importModalBody').innerHTML = `
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<p class="mb-1"><strong>${escHtml(d.name)}</strong>${alreadyBadge}</p>
|
|
<p class="text-muted small mb-2"><i class="bi bi-building me-1"></i>${escHtml(d.sourceCompanyName)}</p>
|
|
${inspiredRow}
|
|
${d.description ? `<p class="text-muted small mb-3">${escHtml(d.description)}</p>` : ''}
|
|
<div class="d-flex gap-2 mb-3">
|
|
${modeBadge}
|
|
${d.industryHint ? `<span class="badge bg-secondary">${escHtml(d.industryHint)}</span>` : ''}
|
|
</div>
|
|
${d.defaultRate != null ? `<p class="small mb-1"><strong>Default rate:</strong> ${escHtml(String(d.defaultRate))} ${escHtml(d.rateLabel || '')}</p>` : ''}
|
|
${d.notes ? `<p class="small text-muted">${escHtml(d.notes)}</p>` : ''}
|
|
</div>
|
|
<div class="col-md-6">
|
|
${diagramHtml}
|
|
${fields.length > 0 ? `
|
|
<h6 class="small fw-semibold mb-2">Input Fields (${fields.length})</h6>
|
|
<table class="table table-sm table-bordered">
|
|
<thead><tr><th>Field</th><th>Unit</th><th>Default</th></tr></thead>
|
|
<tbody>${fieldRows}</tbody>
|
|
</table>` : '<p class="text-muted small">No fields defined.</p>'}
|
|
<div class="mt-2">
|
|
<h6 class="small fw-semibold mb-1">Formula Expression</h6>
|
|
<code class="d-block bg-light border rounded p-2 small text-break">${escHtml(d.formula)}</code>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
const importBtn = document.getElementById('btnConfirmImport');
|
|
if (d.alreadyImported) {
|
|
importBtn.disabled = true;
|
|
importBtn.innerHTML = '<i class="bi bi-check-lg me-1"></i>Already Imported';
|
|
importBtn.classList.replace('btn-primary', 'btn-success');
|
|
} else {
|
|
importBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Confirm import
|
|
document.getElementById('btnConfirmImport')?.addEventListener('click', function () {
|
|
if (!currentLibraryItemId) return;
|
|
|
|
this.disabled = true;
|
|
this.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Importing…';
|
|
|
|
const form = new FormData();
|
|
form.append('libraryItemId', currentLibraryItemId);
|
|
form.append('__RequestVerificationToken', document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '');
|
|
|
|
fetch('/FormulaLibrary/Import', { method: 'POST', body: form })
|
|
.then(r => r.json())
|
|
.then(res => {
|
|
if (res.success) {
|
|
importModal.hide();
|
|
showToast('Formula imported to your library!', 'success');
|
|
// Mark button on the card
|
|
const card = document.querySelector(`.btn-import[data-item-id="${currentLibraryItemId}"]`);
|
|
if (card) {
|
|
card.classList.replace('btn-outline-primary', 'btn-outline-success');
|
|
card.innerHTML = '<i class="bi bi-check-lg me-1"></i><span>Already Imported</span>';
|
|
card.disabled = true;
|
|
card.closest('.card')?.classList.add('border-start', 'border-success', 'border-3');
|
|
}
|
|
} else {
|
|
showToast(res.message || 'Import failed.', 'danger');
|
|
this.disabled = false;
|
|
this.innerHTML = '<i class="bi bi-cloud-download me-1"></i>Import to My Formulas';
|
|
}
|
|
})
|
|
.catch(() => {
|
|
showToast('Import failed. Please try again.', 'danger');
|
|
this.disabled = false;
|
|
this.innerHTML = '<i class="bi bi-cloud-download me-1"></i>Import to My Formulas';
|
|
});
|
|
});
|
|
|
|
function escHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function showToast(msg, type) {
|
|
const container = document.getElementById('toastContainer')
|
|
|| (() => {
|
|
const c = document.createElement('div');
|
|
c.id = 'toastContainer';
|
|
c.className = 'toast-container position-fixed bottom-0 end-0 p-3';
|
|
c.style.zIndex = '1100';
|
|
document.body.appendChild(c);
|
|
return c;
|
|
})();
|
|
|
|
const el = document.createElement('div');
|
|
el.className = `toast align-items-center text-white bg-${type} border-0`;
|
|
el.setAttribute('role', 'alert');
|
|
el.innerHTML = `<div class="d-flex"><div class="toast-body">${escHtml(msg)}</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
|
container.appendChild(el);
|
|
new bootstrap.Toast(el, { delay: 4000 }).show();
|
|
el.addEventListener('hidden.bs.toast', () => el.remove());
|
|
}
|
|
})();
|