// assets/scripts/crud.js import { esc, createTranslator, ANIMATION_MS, removeWithAnimation, animateIn } from './utils.js'; const api = window.CRUD?.apiBase ?? ''; const t = createTranslator('CRUD'); // ── Hilfsfunktionen ────────────────────────────────────────────────────────── function buildClientOptions(selectedId = null) { const clients = window.CRUD?.clients ?? []; let html = ``; clients.forEach(c => { const sel = String(c.id) === String(selectedId) ? ' selected' : ''; html += ``; }); return html; } function rowPrefix() { if (location.pathname.includes('/clients')) return 'client'; if (location.pathname.includes('/projects')) return 'project'; if (location.pathname.includes('/services')) return 'service'; return 'row'; } // ── Create-Formular ────────────────────────────────────────────────────────── function initCreateForm() { const btnNew = document.getElementById('btn-new'); const form = document.getElementById('crud-create'); const btnSave = document.getElementById('btn-create-save'); const btnCancel = document.getElementById('btn-create-cancel'); if (!btnNew || !form) return; btnNew.addEventListener('click', () => { form.classList.toggle('crud-create--visible'); document.getElementById('create-name')?.focus(); }); btnCancel?.addEventListener('click', () => { form.classList.remove('crud-create--visible'); resetCreateForm(); }); btnSave?.addEventListener('click', () => createEntity()); } function resetCreateForm() { ['create-name', 'create-note'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); const rate = document.getElementById('create-rate'); if (rate) rate.value = ''; const billable = document.getElementById('create-billable'); if (billable) billable.checked = true; const client = document.getElementById('create-client'); if (client) client.value = ''; } async function createEntity() { const name = document.getElementById('create-name')?.value?.trim(); if (!name) { alert(t('errorNoName')); return; } const btn = document.getElementById('btn-create-save'); const body = buildCreateBody(); if (btn) btn.disabled = true; try { const res = await fetch(api, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error ?? t('errorSave')); return; } const data = await res.json(); appendRowToList(data); document.getElementById('crud-create')?.classList.remove('crud-create--visible'); resetCreateForm(); } catch { alert(t('errorSave')); } finally { if (btn) btn.disabled = false; } } function buildCreateBody() { const body = { name: document.getElementById('create-name')?.value?.trim(), note: document.getElementById('create-note')?.value || null, }; const rate = document.getElementById('create-rate'); if (rate) body.hourlyRate = rate.value || null; const client = document.getElementById('create-client'); if (client) body.clientId = parseInt(client.value, 10) || null; const billable = document.getElementById('create-billable'); if (billable) body.billable = billable.checked; return body; } // ── Liste: Event Delegation ────────────────────────────────────────────────── function initList() { const list = document.getElementById('crud-list'); if (!list) return; list.addEventListener('click', e => { const actionEl = e.target.closest('[data-action]'); if (!actionEl) return; const row = e.target.closest('.crud-row'); if (!row) return; switch (actionEl.dataset.action) { case 'edit': openEdit(row); break; case 'delete': deleteRow(row); break; case 'save': saveEdit(row); break; case 'cancel': closeEdit(row); break; case 'unarchive': unarchiveRow(row); break; } }); } // ── Inline Edit ────────────────────────────────────────────────────────────── function openEdit(row) { row.querySelector('.crud-row__display').hidden = true; row.querySelector('.crud-row__edit').hidden = false; row.querySelector('.edit-name')?.focus(); } function closeEdit(row) { row.querySelector('.crud-row__display').hidden = false; row.querySelector('.crud-row__edit').hidden = true; } async function saveEdit(row) { const saveBtn = row.querySelector('[data-action="save"]'); if (saveBtn?.disabled) return; const name = row.querySelector('.edit-name')?.value?.trim(); if (!name) { alert(t('errorNoName')); return; } const body = buildEditBody(row); if (saveBtn) saveBtn.disabled = true; try { const res = await fetch(`${api}/${row.dataset.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { alert(t('errorSave')); return; } const data = await res.json(); updateRowDisplay(row, data); closeEdit(row); } catch { alert(t('errorSave')); } finally { if (saveBtn) saveBtn.disabled = false; } } function buildEditBody(row) { const body = { name: row.querySelector('.edit-name')?.value?.trim(), note: row.querySelector('.edit-note')?.value || null, }; const rate = row.querySelector('.edit-rate'); if (rate) body.hourlyRate = rate.value || null; const client = row.querySelector('.edit-client'); if (client) body.clientId = parseInt(client.value, 10) || null; const billable = row.querySelector('.edit-billable'); if (billable) body.billable = billable.checked; return body; } function updateRowDisplay(row, data) { const nameEl = row.querySelector('.crud-row__name'); const metaEl = row.querySelector('.crud-row__meta'); if (nameEl) nameEl.textContent = data.name; if (data.clientName && metaEl) metaEl.textContent = data.clientName; row.dataset.name = data.name; if (data.hourlyRate !== undefined) row.dataset.rate = data.hourlyRate ?? ''; if (data.clientId !== undefined) row.dataset.clientId = data.clientId; if (data.billable !== undefined) row.dataset.billable = data.billable ? '1' : '0'; if (data.note !== undefined) row.dataset.note = data.note ?? ''; const editName = row.querySelector('.edit-name'); if (editName) editName.value = data.name; const editNote = row.querySelector('.edit-note'); if (editNote) editNote.value = data.note ?? ''; const editRate = row.querySelector('.edit-rate'); if (editRate) editRate.value = data.hourlyRate ?? ''; const editBillable = row.querySelector('.edit-billable'); if (editBillable) editBillable.checked = !!data.billable; } // ── Delete ─────────────────────────────────────────────────────────────────── async function deleteRow(row) { if (!confirm(t('confirmDelete'))) return; try { const res = await fetch(`${api}/${row.dataset.id}`, { method: 'DELETE' }); if (res.status === 409) { if (confirm(t('confirmArchive'))) { await archiveRow(row); } return; } if (!res.ok) { alert(t('errorDelete')); return; } removeWithAnimation(row, 'crud-row--removing'); } catch { alert(t('errorDelete')); } } async function archiveRow(row) { try { const res = await fetch(`${api}/${row.dataset.id}/archive`, { method: 'PATCH' }); if (!res.ok) { alert(t('errorArchive')); return; } row.dataset.archived = '1'; row.classList.add('crud-row--archived'); updateRowArchivedState(row, true); filterByTab(document.querySelector('.crud-tab--active')?.dataset.tab ?? 'active'); } catch { alert(t('errorArchive')); } } async function unarchiveRow(row) { try { const res = await fetch(`${api}/${row.dataset.id}/unarchive`, { method: 'PATCH' }); if (!res.ok) { alert(t('errorRestore')); return; } row.dataset.archived = '0'; row.classList.remove('crud-row--archived'); updateRowArchivedState(row, false); filterByTab(document.querySelector('.crud-tab--active')?.dataset.tab ?? 'active'); } catch { alert(t('errorRestore')); } } function updateRowArchivedState(row, archived) { const actions = row.querySelector('.crud-row__actions'); if (!actions) return; if (archived) { actions.innerHTML = ` `; row.querySelector('.crud-row__edit')?.remove(); } else { actions.innerHTML = ` `; } } function filterByTab(tab) { document.querySelectorAll('#crud-list .crud-row').forEach(row => { row.hidden = tab === 'active' ? row.dataset.archived === '1' : row.dataset.archived === '0'; }); } function initTabs() { const tabs = document.querySelectorAll('.crud-tab'); if (!tabs.length) return; filterByTab('active'); tabs.forEach(tab => { tab.addEventListener('click', () => { tabs.forEach(t => t.classList.remove('crud-tab--active')); tab.classList.add('crud-tab--active'); filterByTab(tab.dataset.tab); const btnNew = document.getElementById('btn-new'); if (btnNew) btnNew.hidden = tab.dataset.tab === 'archived'; }); }); } // ── Neue Zeile einfügen ────────────────────────────────────────────────────── function appendRowToList(data) { const list = document.getElementById('crud-list'); if (!list) return; const html = buildRowHTML(data); if (data.billable !== undefined) { const groupLabel = data.billable ? t('groupBillable') : t('groupNotBillable'); let targetGroup = null; list.querySelectorAll('.crud-list__group').forEach(g => { if (g.querySelector('.crud-list__group-label')?.textContent === groupLabel) { targetGroup = g; } }); if (targetGroup) { targetGroup.insertAdjacentHTML('beforeend', html); } else { const groupHtml = `