/**
* 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 = '
';
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);
}
});
})();