/** * Item details tab: bundle management, section editing, tag search. * * Loaded once in dashboard-item.html. Re-initializes on HTMX tab swap * via htmx:afterSwap. Reads item ID from data-item-id on the container. */ (function() { function init() { var container = document.getElementById('item-details-tab'); if (!container) return; var itemId = container.dataset.itemId; if (!itemId) return; try { initBundles(itemId); } catch(e) { console.error('initBundles failed:', e); } initSections(itemId); initTagSearch(itemId); initAiTierToggle(); initItemImage(itemId); } // ── Bundles ── function initBundles(bundleId) { // "Add existing item" dropdown var addBtn = document.getElementById('bundle-add-btn'); if (addBtn) { addBtn.addEventListener('click', function() { var select = document.getElementById('bundle-add-select'); var itemId = select.value; if (!itemId) return; this.disabled = true; fetch('/api/items/' + bundleId + '/bundle/add', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ item_id: itemId }) }) .then(function(res) { if (!res.ok) return apiErrorMessage(res, 'Failed to add').then(function(m) { throw new Error(m); }); select.remove(select.selectedIndex); select.value = ''; var empty = document.getElementById('bundle-empty'); if (empty) empty.remove(); // Reload the tab to show updated table var detailsBtn = document.getElementById('tab-details'); if (detailsBtn) detailsBtn.click(); }) .catch(function(err) { showToast(err.message); }) .finally(function() { addBtn.disabled = false; }); }); } // "Add Item" row button var addRowBtn = document.getElementById('bundle-add-row-btn'); if (addRowBtn && !addRowBtn.dataset.bound) { addRowBtn.dataset.bound = '1'; addRowBtn.addEventListener('click', function() { var container = document.getElementById('bundle-new-rows'); var row = document.createElement('div'); row.style.cssText = 'display: grid; grid-template-columns: 1fr 1.5fr auto; gap: 0.5rem; align-items: start; padding: 0.5rem 0; border-bottom: 1px solid var(--border);'; row.innerHTML = '' + '' + '
' + '' + '' + '
'; container.appendChild(row); var titleInput = row.querySelector('.bundle-new-title'); titleInput.focus(); row.querySelector('.bundle-cancel-btn').addEventListener('click', function() { row.remove(); }); row.querySelector('.bundle-create-btn').addEventListener('click', function() { var title = titleInput.value.trim(); if (!title) { titleInput.focus(); return; } var desc = row.querySelector('.bundle-new-desc').value.trim(); var btn = this; btn.disabled = true; btn.textContent = '...'; fetch('/api/items/' + bundleId + '/bundle/create-child', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ title: title, description: desc || null }) }) .then(function(res) { if (!res.ok) return res.json().then(function(d) { throw new Error(d.error || 'Failed'); }); return res.json(); }) .then(function(data) { row.remove(); var empty = document.getElementById('bundle-empty'); if (empty) empty.remove(); // Add to table var tbody = document.getElementById('bundle-items-list'); var tr = document.createElement('tr'); tr.className = 'bundle-row'; tr.dataset.childId = data.item_id; tr.style.borderBottom = '1px solid var(--border)'; tr.innerHTML = '' + escapeHtml(data.title) + '' + '' + escapeHtml(desc) + '' + 'Manage files' + ''; tbody.appendChild(tr); attachBundleRowHandlers(tr, bundleId); updateBundleCount(1); }) .catch(function(err) { btn.disabled = false; btn.textContent = 'Create'; showToast(err.message); }); }); // Enter key creates titleInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); row.querySelector('.bundle-create-btn').click(); } }); row.querySelector('.bundle-new-desc').addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); row.querySelector('.bundle-create-btn').click(); } }); }); } // Attach handlers to existing rows document.querySelectorAll('.bundle-row').forEach(function(row) { attachBundleRowHandlers(row, bundleId); }); } function attachBundleRowHandlers(row, bundleId) { var removeBtn = row.querySelector('.bundle-remove-btn'); if (removeBtn) { removeBtn.addEventListener('click', function() { var childId = this.dataset.childId; fetch('/api/items/' + bundleId + '/bundle/' + childId, { method: 'DELETE', headers: csrfHeaders() }) .then(function(res) { if (!res.ok) return apiErrorMessage(res, 'Failed to remove').then(function(m) { throw new Error(m); }); row.remove(); updateBundleCount(-1); }) .catch(function(err) { showToast(err.message); }); }); } } function updateBundleCount(delta) { var el = document.getElementById('bundle-count'); if (el) el.textContent = parseInt(el.textContent) + delta; } // ── Sections ── function initSections(itemId) { var addBtn = document.getElementById('add-sec-btn'); if (!addBtn) return; addBtn.addEventListener('click', function() { var title = document.getElementById('new-sec-title').value.trim(); var body = document.getElementById('new-sec-body').value; var status = document.getElementById('sec-add-status'); if (!title) { status.textContent = 'Title is required'; return; } this.disabled = true; status.textContent = ''; fetch('/api/items/' + itemId + '/sections', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ title: title, body: body }) }) .then(function(res) { if (!res.ok) return res.json().then(function(d) { throw new Error(d.error || 'Failed'); }); return res.json(); }) .then(function(sec) { var empty = document.getElementById('sections-empty'); if (empty) empty.remove(); var row = document.createElement('div'); row.className = 'section-mgmt-row'; row.dataset.id = sec.id; row.style.cssText = 'display:flex;align-items:center;gap:0.75rem;padding:0.6rem 0;border-bottom:1px solid var(--border);'; row.innerHTML = '' + escapeHtml(sec.title) + '' + '' + (sec.body || '').length + ' chars' + '' + ''; document.getElementById('sections-list').appendChild(row); attachSectionRowHandlers(row, itemId); updateSectionCount(1); document.getElementById('new-sec-title').value = ''; document.getElementById('new-sec-body').value = ''; document.getElementById('section-add-details').removeAttribute('open'); }) .catch(function(err) { status.textContent = err.message; }) .finally(function() { addBtn.disabled = false; }); }); document.getElementById('save-sec-btn').addEventListener('click', function() { var id = document.getElementById('edit-sec-id').value; var title = document.getElementById('edit-sec-title').value.trim(); var body = document.getElementById('edit-sec-body').value; var status = document.getElementById('sec-edit-status'); if (!title) { status.textContent = 'Title is required'; return; } this.disabled = true; fetch('/api/sections/' + id, { method: 'PUT', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ title: title, body: body }) }) .then(function(res) { if (!res.ok) return res.json().then(function(d) { throw new Error(d.error || 'Failed'); }); return res.json(); }) .then(function(sec) { var row = document.querySelector('.section-mgmt-row[data-id="' + id + '"]'); if (row) { row.querySelector('span[style*="font-weight"]').textContent = sec.title; row.querySelector('span[style*="opacity"]').textContent = (sec.body || '').length + ' chars'; } document.getElementById('section-edit-modal').style.display = 'none'; }) .catch(function(err) { status.textContent = err.message; }) .finally(function() { document.getElementById('save-sec-btn').disabled = false; }); }); document.getElementById('cancel-sec-btn').addEventListener('click', function() { document.getElementById('section-edit-modal').style.display = 'none'; }); document.querySelectorAll('.section-mgmt-row').forEach(function(row) { attachSectionRowHandlers(row, itemId); }); } function attachSectionRowHandlers(row, itemId) { row.querySelector('.section-del-btn').addEventListener('click', function() { var id = this.dataset.id; if (!confirm('Delete this section?')) return; fetch('/api/sections/' + id, { method: 'DELETE', headers: csrfHeaders() }) .then(function(res) { if (res.ok) { row.remove(); updateSectionCount(-1); } else apiErrorMessage(res, 'Failed to delete').then(function(m) { showToast(m); }); }) .catch(function() { showToast('Failed to delete'); }); }); row.querySelector('.section-edit-btn').addEventListener('click', function() { var id = this.dataset.id; var modal = document.getElementById('section-edit-modal'); document.getElementById('edit-sec-id').value = id; document.getElementById('edit-sec-title').value = row.querySelector('span[style*="font-weight"]').textContent; document.getElementById('edit-sec-body').value = ''; document.getElementById('sec-edit-status').textContent = 'Loading...'; modal.style.display = 'block'; fetch('/api/items/' + itemId + '/sections') .then(function(r) { return r.json(); }) .then(function(resp) { var sec = resp.data.find(function(s) { return s.id === id; }); if (sec) { document.getElementById('edit-sec-title').value = sec.title; document.getElementById('edit-sec-body').value = sec.body; } document.getElementById('sec-edit-status').textContent = ''; }) .catch(function() { document.getElementById('sec-edit-status').textContent = 'Failed to load'; }); }); } function updateSectionCount(delta) { var el = document.getElementById('section-count'); if (el) el.textContent = parseInt(el.textContent) + delta; } // ── Tag Search ── function initTagSearch(itemId) { var input = document.getElementById('item-tags'); if (!input) return; var tagSearchTimeout; window.searchTags = function(q) { clearTimeout(tagSearchTimeout); var suggestions = document.getElementById('tag-suggestions'); if (!q.trim()) { suggestions.style.display = 'none'; return; } tagSearchTimeout = setTimeout(function() { fetch('/api/tags/search?q=' + encodeURIComponent(q)) .then(function(r) { return r.json(); }) .then(function(tags) { if (!tags.length) { suggestions.style.display = 'none'; return; } suggestions.innerHTML = ''; tags.forEach(function(t) { var div = document.createElement('div'); div.style.cssText = 'padding: 0.4rem 0.6rem; cursor: pointer; font-size: 0.85rem;'; div.textContent = t.name; div.addEventListener('mouseover', function() { this.style.background = 'var(--border)'; }); div.addEventListener('mouseout', function() { this.style.background = 'transparent'; }); div.addEventListener('click', function() { addTagById(t.id); }); suggestions.appendChild(div); }); suggestions.style.display = 'block'; }) .catch(function() { suggestions.style.display = 'none'; }); }, 200); }; function addTagById(tagId) { var form = new FormData(); form.append('tag_id', tagId); fetch('/api/items/' + itemId + '/tags', { method: 'POST', headers: csrfHeaders(), body: form }) .then(function(r) { // Success: server returns an HTML fragment for the new tag. // Failure: server returns JSON `{error: "..."}` via json_error_layer. if (!r.ok) return apiErrorMessage(r, 'Failed to add tag').then(function(m) { throw new Error(m); }); return r.text(); }) .then(function(html) { if (html) { document.getElementById('tags-container').insertAdjacentHTML('beforeend', html); } document.getElementById('item-tags').value = ''; document.getElementById('tag-suggestions').style.display = 'none'; htmx.process(document.getElementById('tags-container')); }) .catch(function(err) { var msg = document.getElementById('item-save-status'); if (msg) msg.textContent = err.message || 'Failed to add tag. Please try again.'; }); } document.addEventListener('click', function(e) { if (!e.target.closest('#item-tags') && !e.target.closest('#tag-suggestions')) { var el = document.getElementById('tag-suggestions'); if (el) el.style.display = 'none'; } }); } // ── AI Tier Toggle ── function initAiTierToggle() { var tierSelect = document.getElementById('ai_tier'); var disclosureRow = document.getElementById('ai-disclosure-row'); if (tierSelect && disclosureRow) { tierSelect.addEventListener('change', function() { disclosureRow.classList.toggle('hidden', this.value !== 'assisted'); }); } } // ── Item Image ── function initItemImage(itemId) { var input = document.getElementById('item-image-input'); if (!input) return; input.addEventListener('change', function() { var file = this.files[0]; if (!file) return; var status = document.getElementById('item-image-status'); status.textContent = 'Uploading...'; fetch('/api/items/image/presign', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ item_id: itemId, file_name: file.name, content_type: file.type || 'image/jpeg' }) }) .then(function(res) { if (!res.ok) throw new Error('Presign failed'); return res.json(); }) .then(function(data) { var xhr = new XMLHttpRequest(); xhr.open('PUT', data.upload_url); xhr.setRequestHeader('Content-Type', file.type || 'image/jpeg'); if (data.cache_control) xhr.setRequestHeader('Cache-Control', data.cache_control); return new Promise(function(resolve, reject) { xhr.onload = function() { xhr.status < 300 ? resolve(data.s3_key) : reject(new Error('Upload failed')); }; xhr.onerror = function() { reject(new Error('Network error')); }; xhr.send(file); }); }) .then(function(s3Key) { return fetch('/api/items/image/confirm', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ item_id: itemId, s3_key: s3Key }) }); }) .then(function(res) { if (!res.ok) throw new Error('Confirm failed'); return res.json(); }) .then(function(data) { status.textContent = 'Saved.'; var preview = document.getElementById('item-image-preview'); preview.innerHTML = 'Item image'; setTimeout(function() { status.textContent = ''; }, 2000); }) .catch(function(err) { status.textContent = err.message; }); }); } // ── Shared ── function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } // Run on initial load init(); // Re-run when HTMX swaps in tab content document.body.addEventListener('htmx:afterSwap', function(e) { if (e.detail.target && e.detail.target.id === 'tab-content') { setTimeout(init, 0); } }); })();