// assets/scripts/report.js import { parseDuration, roundToQuarter, formatMinutes, validateDuration, initDurationBlurHandler, } from './duration.js'; // ── Hilfsfunktionen ─────────────────────────────────────────────────────────── function t(key) { return window.Report?.i18n?.[key] ?? key; } function populateProjectSelect(select, selectedId) { const projects = window.Report?.projects ?? []; select.innerHTML = ''; projects.forEach(p => { const opt = document.createElement('option'); opt.value = p.id; opt.textContent = `${p.clientName} / ${p.name}`; if (p.id === selectedId) opt.selected = true; select.appendChild(opt); }); } function populateServiceSelect(select, selectedId) { const services = window.Report?.services ?? []; const billable = services.filter(s => s.billable); const notBillable = services.filter(s => !s.billable); select.innerHTML = ``; function addGroup(label, list) { if (!list.length) return; const group = document.createElement('optgroup'); group.label = label; list.forEach(s => { const opt = document.createElement('option'); opt.value = s.id; opt.textContent = s.name; if (s.id === selectedId) opt.selected = true; group.appendChild(opt); }); select.appendChild(group); } addGroup(t('billable'), billable); addGroup(t('notBillable'), notBillable); } // ── Edit öffnen ─────────────────────────────────────────────────────────────── function openEdit(row) { document.querySelectorAll('.report-table__row--editing').forEach(r => { if (r !== row) closeEdit(r); }); const editForm = row.querySelector('.report-row__edit'); if (!editForm) return; // Selects klonen um akkumulierte Listener zu vermeiden const oldProjectSel = row.querySelector('.edit-project'); const oldServiceSel = row.querySelector('.edit-service'); const projectSel = oldProjectSel.cloneNode(false); const serviceSel = oldServiceSel.cloneNode(false); oldProjectSel.replaceWith(projectSel); oldServiceSel.replaceWith(serviceSel); const projectId = parseInt(row.dataset.projectId) || null; const serviceId = parseInt(row.dataset.serviceId) || null; populateProjectSelect(projectSel, projectId); populateServiceSelect(serviceSel, serviceId); projectSel.addEventListener('change', () => { populateServiceSelect(row.querySelector('.edit-service'), null); }); editForm.hidden = false; row.classList.add('report-table__row--editing'); row.querySelector('.edit-duration')?.focus(); } function closeEdit(row) { const editForm = row.querySelector('.report-row__edit'); if (!editForm) return; editForm.hidden = true; row.classList.remove('report-table__row--editing'); } // ── Speichern ───────────────────────────────────────────────────────────────── async function saveEdit(row) { const id = row.dataset.entryId; const durationRaw = row.querySelector('.edit-duration')?.value ?? '0:00'; const projectId = row.querySelector('.edit-project')?.value; const serviceId = row.querySelector('.edit-service')?.value; const note = row.querySelector('.edit-note')?.value ?? ''; if (!projectId) { alert(t('errorNoProject')); return; } const rawMinutes = roundToQuarter(parseDuration(durationRaw)); if (rawMinutes === 0) { alert(t('errorZeroDuration')); return; } const validation = validateDuration(rawMinutes); if (validation.status === 'error') { alert(t('errorDurationTooLong')); return; } if (validation.status === 'warn' && !confirm(t('warnDurationLong'))) return; try { const res = await fetch(`/api/entries/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ duration: formatMinutes(rawMinutes), projectId: parseInt(projectId), serviceId: serviceId ? parseInt(serviceId) : null, note: note || null, }), }); if (!res.ok) { alert(t('errorSave')); return; } window.location.reload(); } catch { alert(t('errorSave')); } } // ── Löschen ─────────────────────────────────────────────────────────────────── async function deleteEntry(row) { if (!confirm(t('confirmDelete'))) return; const id = row.dataset.entryId; try { const res = await fetch(`/api/entries/${id}`, { method: 'DELETE' }); if (!res.ok) { alert(t('errorDelete')); return; } window.location.reload(); } catch { alert(t('errorDelete')); } } // ── Abgerechnet toggeln ─────────────────────────────────────────────────────── async function toggleInvoiced(row) { const id = row.dataset.entryId; const btn = row.querySelector('[data-action="toggle-invoiced"]'); try { const res = await fetch(`/api/entries/${id}/invoiced`, { method: 'PATCH' }); if (!res.ok) return; const data = await res.json(); const invoiced = data.invoiced; row.dataset.invoiced = invoiced ? 'true' : 'false'; row.classList.toggle('report-table__row--invoiced', invoiced); if (btn) { btn.classList.toggle('report-lock--invoiced', invoiced); btn.title = invoiced ? t('btnUnlock') : t('btnLock'); } } catch (err) { console.error('Fehler beim Toggeln des Abrechnungsstatus:', err); } } // ── Event-Delegation ────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { initDurationBlurHandler(); const table = document.querySelector('.report-table'); if (!table) return; table.addEventListener('click', e => { const btn = e.target.closest('[data-action]'); if (!btn) return; const row = btn.closest('.report-table__row'); if (!row) return; switch (btn.dataset.action) { case 'edit': openEdit(row); break; case 'cancel': closeEdit(row); break; case 'save': saveEdit(row); break; case 'delete': deleteEntry(row); break; case 'toggle-invoiced': toggleInvoiced(row); break; } }); });