Fix inline item editing never activating on details pages

The script IIFE was reading window.inlineItemEdit at load time, before
the inline <script> block in @section Scripts had executed to set it.
Config read moved inside DOMContentLoaded so it fires after all inline
scripts in the section have run, regardless of src vs. inline order.
cfg is now passed as a parameter to makeEditable and attachListeners
instead of being captured from the outer IIFE scope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 22:37:27 -04:00
parent eaab0af51f
commit 0e480adbf6
@@ -2,13 +2,11 @@
/// Shared inline-edit behaviour for quote, job, and invoice item rows. /// Shared inline-edit behaviour for quote, job, and invoice item rows.
/// Activated when the page sets window.inlineItemEdit = { patchUrl, canEdit, totals }. /// Activated when the page sets window.inlineItemEdit = { patchUrl, canEdit, totals }.
/// totals: { subtotal, tax, total, finalPrice, balance } — CSS selectors, any subset. /// totals: { subtotal, tax, total, finalPrice, balance } — CSS selectors, any subset.
/// Config is read inside DOMContentLoaded so script load order vs. config assignment doesn't matter.
/// </summary> /// </summary>
(function () { (function () {
'use strict'; 'use strict';
const cfg = window.inlineItemEdit;
if (!cfg || !cfg.canEdit) return;
function fmt(val) { function fmt(val) {
return val.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); return val.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
} }
@@ -26,7 +24,7 @@
setTimeout(() => el.remove(), 4000); setTimeout(() => el.remove(), 4000);
} }
function updateTotals(data) { function updateTotals(cfg, data) {
const t = cfg.totals || {}; const t = cfg.totals || {};
[ [
[t.subtotal, data.subtotal], [t.subtotal, data.subtotal],
@@ -41,7 +39,14 @@
}); });
} }
function makeEditable(span) { function attachListeners(cfg, span) {
span.style.cursor = 'text';
span.title = 'Click to edit';
span.classList.add('inline-editable');
span.addEventListener('click', () => makeEditable(cfg, span), { once: true });
}
function makeEditable(cfg, span) {
const field = span.dataset.inlineField; const field = span.dataset.inlineField;
const row = span.closest('tr[data-item-id]'); const row = span.closest('tr[data-item-id]');
if (!row) return; if (!row) return;
@@ -73,7 +78,7 @@
function revert() { function revert() {
span.innerHTML = savedHTML; span.innerHTML = savedHTML;
attachListeners(span); attachListeners(cfg, span);
} }
async function commit() { async function commit() {
@@ -136,10 +141,10 @@
if (totalCell) totalCell.textContent = fmt(data.lineTotal); if (totalCell) totalCell.textContent = fmt(data.lineTotal);
// Update document-level totals // Update document-level totals
updateTotals(data); updateTotals(cfg, data);
// Re-attach click listener for next edit // Re-attach click listener for next edit
attachListeners(span); attachListeners(cfg, span);
} catch { } catch {
showError('Could not save &mdash; check your connection and try again.'); showError('Could not save &mdash; check your connection and try again.');
@@ -154,14 +159,11 @@
}); });
} }
function attachListeners(span) { // Read config inside DOMContentLoaded — by then the inline <script> that sets
span.style.cursor = 'text'; // window.inlineItemEdit has already executed regardless of script load order.
span.title = 'Click to edit';
span.classList.add('inline-editable');
span.addEventListener('click', () => makeEditable(span), { once: true });
}
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-inline-field]').forEach(attachListeners); const cfg = window.inlineItemEdit;
if (!cfg || !cfg.canEdit) return;
document.querySelectorAll('[data-inline-field]').forEach(span => attachListeners(cfg, span));
}); });
})(); })();