Add formula template export/import and unsaved-changes guard

- Export: GET /CompanySettings/ExportCustomItemTemplates downloads all
  company templates as an indented JSON backup (strips internal IDs/paths)
- Import: POST /CompanySettings/ImportCustomItemTemplates restores from
  that file; runs full field + formula validation, skips name duplicates,
  returns per-item results (imported / skipped / errors)
- Unsaved-changes guard: cfModal now intercepts backdrop/ESC/X when the
  form is dirty and prompts before discarding work
- Export and Import buttons added to the Custom Formulas card header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 09:24:02 -04:00
parent cf07356147
commit b23bea6db0
3 changed files with 277 additions and 0 deletions
@@ -4,6 +4,7 @@
(function () {
let cfFields = [];
let cfEditing = false;
let cfFormDirty = false;
// ── Load & Render ─────────────────────────────────────────────────────────
@@ -221,9 +222,11 @@
document.getElementById('cfDiagramImg').src = `/CompanySettings/TemplateDiagram?templateId=${t.id}`;
document.getElementById('cfDiagramPreview').style.display = '';
}
cfFormDirty = false;
}
function cfResetForm() {
cfFormDirty = false;
document.getElementById('cfId').value = '0';
document.getElementById('cfName').value = '';
document.getElementById('cfDescription').value = '';
@@ -528,6 +531,7 @@
});
}
cfFormDirty = false;
bootstrap.Modal.getInstance(document.getElementById('cfModal'))?.hide();
cfLoadTemplates();
} catch (e) {
@@ -862,4 +866,103 @@
cfWtStep = i;
cfRenderWtStep();
};
// ── Import ────────────────────────────────────────────────────────────────
window.cfShowImport = function () {
document.getElementById('cfImportFile').value = '';
document.getElementById('cfImportResults').classList.add('d-none');
document.getElementById('cfImportSummary').innerHTML = '';
const btn = document.getElementById('cfImportBtn');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-upload me-1"></i>Import';
new bootstrap.Modal(document.getElementById('cfImportModal')).show();
};
window.cfSubmitImport = async function () {
const fileInput = document.getElementById('cfImportFile');
if (!fileInput.files.length) {
showCfError('Please select a .json export file first.');
return;
}
const btn = document.getElementById('cfImportBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Importing&hellip;';
const form = new FormData();
form.append('file', fileInput.files[0]);
form.append('__RequestVerificationToken', getAntiForgeryToken());
try {
const res = await fetch('/CompanySettings/ImportCustomItemTemplates', { method: 'POST', body: form });
const data = await res.json();
const resultsEl = document.getElementById('cfImportResults');
const summaryEl = document.getElementById('cfImportSummary');
resultsEl.classList.remove('d-none');
if (!data.success) {
summaryEl.innerHTML = `<div class="alert alert-danger alert-permanent mb-0"><i class="bi bi-x-circle me-2"></i>${escHtml(data.message)}</div>`;
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-upload me-1"></i>Import';
return;
}
let html = '';
if (data.imported > 0)
html += `<div class="alert alert-success alert-permanent mb-2"><i class="bi bi-check-circle me-2"></i><strong>${data.imported}</strong> template${data.imported !== 1 ? 's' : ''} imported successfully.</div>`;
if (data.skipped > 0) {
const names = (data.skippedNames || []).map(n => `<li>${escHtml(n)}</li>`).join('');
html += `<div class="alert alert-warning alert-permanent mb-2">
<i class="bi bi-skip-forward me-2"></i><strong>${data.skipped}</strong> skipped &mdash; name already exists:
<ul class="mb-0 mt-1 small">${names}</ul>
</div>`;
}
if (data.errors && data.errors.length) {
const items = data.errors.map(e => `<li>${escHtml(e)}</li>`).join('');
html += `<div class="alert alert-danger alert-permanent mb-2">
<i class="bi bi-exclamation-triangle me-2"></i><strong>${data.errors.length}</strong> error${data.errors.length !== 1 ? 's' : ''}:
<ul class="mb-0 mt-1 small">${items}</ul>
</div>`;
}
if (data.imported === 0 && data.skipped === 0 && (!data.errors || !data.errors.length))
html = '<div class="alert alert-info alert-permanent mb-0"><i class="bi bi-info-circle me-2"></i>The file contained no templates to import.</div>';
summaryEl.innerHTML = html;
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-check me-1"></i>Done';
if (data.imported > 0) cfLoadTemplates();
} catch (e) {
showCfError('Import request failed: ' + e.message);
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-upload me-1"></i>Import';
}
};
// ── Unsaved-changes guard ─────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', function () {
const modal = document.getElementById('cfModal');
if (!modal) return;
// Any user interaction inside the modal marks the form dirty
modal.addEventListener('input', function () { cfFormDirty = true; });
modal.addEventListener('change', function () { cfFormDirty = true; });
// Intercept backdrop click, ESC, and the X button when there is unsaved work
modal.addEventListener('hide.bs.modal', function (e) {
if (!cfFormDirty) return;
e.preventDefault();
if (confirm('You have unsaved changes. Close anyway and lose your work?')) {
cfFormDirty = false;
bootstrap.Modal.getInstance(modal)?.hide();
}
});
});
})();