111 lines
3.4 KiB
JavaScript
111 lines
3.4 KiB
JavaScript
/* ── Categories page ── */
|
|
|
|
const CategoriesPage = (() => {
|
|
function escHtml(str) {
|
|
return String(str || '').replace(/&/g, '&').replace(/</g, '<')
|
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
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 };
|
|
})();
|