BillTracker/public/js/categories.js

111 lines
3.4 KiB
JavaScript

/* ── Categories page ── */
const CategoriesPage = (() => {
function escHtml(str) {
return String(str || '').replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
async function init(container) {
await render(container);
}
async function render(container) {
let cats;
try {
cats = await API.categories();
} catch (e) {
container.innerHTML = `<div class="empty-state"><p>Failed to load: ${e.message}</p></div>`;
return;
}
container.innerHTML = `
<div class="page-header">
<h1 class="page-title">Categories</h1>
</div>
<form class="cat-add-form" id="cat-add-form">
<input type="text" id="cat-new-name" placeholder="New category name" style="max-width:280px">
<button type="submit" class="btn btn-primary">Add</button>
</form>
<div class="cat-list" id="cat-list">
${cats.map(c => renderItem(c)).join('')}
${cats.length === 0 ? `<div class="empty-state"><p>No categories yet.</p></div>` : ''}
</div>
`;
document.getElementById('cat-add-form').onsubmit = async (e) => {
e.preventDefault();
const name = document.getElementById('cat-new-name').value.trim();
if (!name) return;
try {
await API.createCategory(name);
document.getElementById('cat-new-name').value = '';
showToast('Category added', 'success');
render(container);
} catch (err) {
showToast('Error: ' + err.message, 'error');
}
};
container.querySelectorAll('.btn-delete-cat').forEach(btn => {
btn.onclick = async () => {
if (!confirm('Delete this category? Bills using it will be uncategorized.')) return;
try {
await API.deleteCategory(btn.dataset.id);
showToast('Category deleted', 'success');
render(container);
} catch (err) {
showToast('Error: ' + err.message, 'error');
}
};
});
container.querySelectorAll('.cat-name-span').forEach(span => {
span.ondblclick = () => startRename(span, cats.find(c => c.id == span.dataset.id), container);
});
}
function renderItem(cat) {
return `
<div class="cat-item" data-cat-id="${cat.id}">
<span class="cat-name cat-name-span" data-id="${cat.id}" title="Double-click to rename">
${escHtml(cat.name)}
</span>
<button class="btn btn-ghost btn-sm btn-delete-cat" data-id="${cat.id}">Delete</button>
</div>
`;
}
function startRename(span, cat, container) {
const input = document.createElement('input');
input.type = 'text';
input.value = cat.name;
input.className = 'cat-name';
input.style.flex = '1';
span.replaceWith(input);
input.focus();
input.select();
async function commit() {
const name = input.value.trim();
if (!name || name === cat.name) { render(container); return; }
try {
await API.updateCategory(cat.id, name);
showToast('Renamed', 'success');
render(container);
} catch (err) {
showToast('Error: ' + err.message, 'error');
render(container);
}
}
input.addEventListener('blur', commit);
input.addEventListener('keydown', e => {
if (e.key === 'Enter') input.blur();
if (e.key === 'Escape') render(container);
});
}
return { init };
})();