// assets/scripts/crud.js // Generisches CRUD-Handler für Kunden, Projekte, Leistungen const api = window.CRUD?.apiBase ?? ''; // ── 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() { // Ermittelt den Entitätstyp aus der URL 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() { const fields = ['create-name', 'create-note']; fields.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('Bitte einen Namen eingeben.'); return; } const body = buildCreateBody(); 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 ?? 'Fehler beim Speichern.'); return; } const data = await res.json(); appendRowToList(data); document.getElementById('crud-create')?.classList.remove('crud-create--visible'); resetCreateForm(); } catch (err) { console.error(err); alert('Fehler beim Speichern.'); } } function buildCreateBody() { const body = { name: document.getElementById('create-name')?.value?.trim(), note: document.getElementById('create-note')?.value || null, }; // Kunden-spezifisch const rate = document.getElementById('create-rate'); if (rate) body.hourlyRate = rate.value || null; // Projekt-spezifisch const client = document.getElementById('create-client'); if (client) body.clientId = parseInt(client.value) || null; // Leistungs-spezifisch 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 action = actionEl.dataset.action; const row = e.target.closest('.crud-row'); if (!row) return; switch (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 id = row.dataset.id; const name = row.querySelector('.edit-name')?.value?.trim(); if (!name) { alert('Bitte einen Namen eingeben.'); return; } const body = buildEditBody(row); try { const res = await fetch(`${api}/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { alert('Fehler beim Speichern.'); return; } const data = await res.json(); updateRowDisplay(row, data); closeEdit(row); } catch (err) { console.error(err); alert('Fehler beim Speichern.'); } } function buildEditBody(row) { const body = { name: row.querySelector('.edit-name')?.value?.trim(), note: row.querySelector('.edit-note')?.value || null, }; // Kunden const rate = row.querySelector('.edit-rate'); if (rate) body.hourlyRate = rate.value || null; // Projekt const client = row.querySelector('.edit-client'); if (client) body.clientId = parseInt(client.value) || null; // Leistung 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; // Kunden: Meta-Text unverändert (Projektanzahl ändert sich nicht) // Projekte: Client-Name aktualisieren if (data.clientName && metaEl) metaEl.textContent = data.clientName; // data-Attribute aktualisieren 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 ?? ''; // Edit-Felder aktualisieren 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('Wirklich löschen?')) return; try { const res = await fetch(`${api}/${row.dataset.id}`, { method: 'DELETE' }); if (res.status === 409) { if (confirm('Dieser Eintrag hat abhängige Zeiteinträge und kann nicht gelöscht werden.\nStattdessen archivieren?')) { await archiveRow(row); } return; } if (!res.ok) { alert('Fehler beim Löschen.'); return; } row.classList.add('crud-row--removing'); setTimeout(() => row.remove(), 280); } catch { alert('Fehler beim Löschen.'); } } async function archiveRow(row) { try { const res = await fetch(`${api}/${row.dataset.id}/archive`, { method: 'PATCH' }); if (!res.ok) { alert('Fehler beim Archivieren.'); 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('Fehler beim Archivieren.'); } } async function unarchiveRow(row) { try { const res = await fetch(`${api}/${row.dataset.id}/unarchive`, { method: 'PATCH' }); if (!res.ok) { alert('Fehler beim Wiederherstellen.'); 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('Fehler beim Wiederherstellen.'); } } 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); // Services haben Gruppen → in die richtige Gruppe einfügen if (data.billable !== undefined) { const groupLabel = data.billable ? 'Verrechenbar' : 'Nicht-verrechenbar'; 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 { // Gruppe existiert noch nicht → neu anlegen const groupHtml = `