// panes/items.js — CRUD against /api/items with search, bulk select, export/import.
import { registerPane } from '../app.js';
import { jget, jpost, jdelete, escapeHtml } from '../lib/api.js';
import { ok, err, copyToClipboard, fmtJSON } from '../lib/ui.js';
const TPL = `
Items
Backend-managed records · /api/items.
New item
Bulk actions
0 selected
`;
let all = [];
const selected = new Set();
function row(it, checked) {
return ``;
}
function render(host) {
const q = host.querySelector('#items-search').value.trim().toLowerCase();
const filtered = q ? all.filter(it => (it.name + ' ' + (it.description || '')).toLowerCase().includes(q)) : all;
host.querySelector('#items-count').textContent = `${filtered.length}/${all.length}`;
host.querySelector('#items-list').innerHTML = filtered.length
? filtered.map(it => row(it, selected.has(it.id))).join('')
: 'no items match
';
host.querySelectorAll('[data-del]').forEach(b => b.addEventListener('click', async () => {
if (!confirm(`Delete item #${b.dataset.del}?`)) return;
try { await jdelete('/api/items/' + b.dataset.del); selected.delete(+b.dataset.del); await load(host); ok('deleted'); }
catch (e) { err(e.message); }
}));
host.querySelectorAll('[data-copy]').forEach(b => b.addEventListener('click', () => {
const it = all.find(x => x.id === +b.dataset.copy); if (it) copyToClipboard(fmtJSON(it), 'copied');
}));
host.querySelectorAll('[data-sel]').forEach(c => c.addEventListener('change', () => {
const id = +c.dataset.sel;
if (c.checked) selected.add(id); else selected.delete(id);
updateSel(host);
}));
}
function updateSel(host) {
host.querySelector('#items-sel-count').textContent = `${selected.size} selected`;
host.querySelector('#items-sel-del').disabled = selected.size === 0;
}
async function load(host) {
try {
const j = await jget('/api/items');
all = j.items || [];
// prune stale selections
const ids = new Set(all.map(x => x.id));
for (const id of [...selected]) if (!ids.has(id)) selected.delete(id);
render(host); updateSel(host);
} catch (e) {
host.querySelector('#items-list').innerHTML = `${escapeHtml(e.message)}
`;
}
}
registerPane('items', {
label: 'Items',
init(host) {
host.innerHTML = TPL;
host.querySelector('#items-refresh').addEventListener('click', () => load(host));
host.querySelector('#items-search').addEventListener('input', () => render(host));
host.querySelector('#items-form').addEventListener('submit', async (e) => {
e.preventDefault();
const name = host.querySelector('#items-name').value.trim();
const description = host.querySelector('#items-desc').value.trim();
if (!name) return;
try {
await jpost('/api/items', { name, description });
host.querySelector('#items-form').reset();
ok('added'); await load(host);
} catch (e) { err(e.message); }
});
host.querySelector('#items-sel-all').addEventListener('click', () => { all.forEach(x => selected.add(x.id)); render(host); updateSel(host); });
host.querySelector('#items-sel-none').addEventListener('click', () => { selected.clear(); render(host); updateSel(host); });
host.querySelector('#items-sel-del').addEventListener('click', async () => {
const ids = [...selected];
if (!ids.length) return;
if (!confirm(`Delete ${ids.length} item(s)?`)) return;
for (const id of ids) { try { await jdelete('/api/items/' + id); } catch (e) { err(`#${id}: ${e.message}`); } }
selected.clear(); ok(`deleted ${ids.length}`); await load(host);
});
host.querySelector('#items-export').addEventListener('click', () => {
const blob = new Blob([JSON.stringify(all, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `cxwebapp-items-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
});
host.querySelector('#items-import').addEventListener('click', () => host.querySelector('#items-file').click());
host.querySelector('#items-file').addEventListener('change', async (e) => {
const f = e.target.files?.[0]; if (!f) return;
try {
const text = await f.text();
const arr = JSON.parse(text);
if (!Array.isArray(arr)) throw new Error('expected JSON array');
let n = 0;
for (const it of arr) {
if (!it?.name) continue;
try { await jpost('/api/items', { name: it.name, description: it.description || '' }); n++; } catch {}
}
ok(`imported ${n}/${arr.length}`); await load(host);
} catch (err2) { err('import: ' + err2.message); }
e.target.value = '';
});
load(host);
},
refresh: load,
});