// assets/scripts/report.js import { parseAndValidate, initDurationBlurHandler } from './duration.js'; import { esc, createTranslator } from './utils.js'; const t = createTranslator('Report'); // ── Hilfsfunktionen ────────────────────────────────────────────────────────── 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; 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, 10) || null; const serviceId = parseInt(row.dataset.serviceId, 10) || 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 saveBtn = row.querySelector('[data-action="save"]'); if (saveBtn?.disabled) return; const id = row.dataset.entryId; 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 dur = parseAndValidate(row.querySelector('.edit-duration')?.value ?? '0:00'); if (dur.error) { alert(t(dur.error)); return; } if (dur.warn && !confirm(t(dur.warn))) return; if (saveBtn) saveBtn.disabled = true; try { const res = await fetch(`/api/entries/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ duration: dur.formatted, projectId: parseInt(projectId, 10), serviceId: serviceId ? parseInt(serviceId, 10) : null, note: note || null, }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error ?? t('errorSave')); return; } window.location.reload(); } catch { alert(t('errorSave')); } finally { if (saveBtn) saveBtn.disabled = false; } } // ── Löschen ────────────────────────────────────────────────────────────────── async function deleteEntry(row) { if (!confirm(t('confirmDelete'))) return; try { const res = await fetch(`/api/entries/${row.dataset.entryId}`, { 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('toggleInvoiced error:', err); } } // ── Event-Delegation ───────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { initDurationBlurHandler(); const table = document.querySelector('.report-table'); if (table) { 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; } }); } new ReportFilter().init(); initExportButtons(); initPrintButton(); }); // ── ReportFilter ───────────────────────────────────────────────────────────── class ReportFilter { constructor() { this.panel = document.getElementById('report-filter'); this.toggleBtn = document.getElementById('btn-filter-toggle'); this.applyBtn = document.getElementById('btn-filter-apply'); this.hideBtn = document.getElementById('btn-filter-hide'); this.periodSel = document.querySelector('.filter-period-select'); this.customDates = document.querySelector('.filter-custom-dates'); } init() { if (!this.panel) return; this.toggleBtn?.addEventListener('click', () => this.togglePanel()); this.hideBtn?.addEventListener('click', () => this.hidePanel()); this.applyBtn?.addEventListener('click', () => this.applyFilters()); this.panel.querySelectorAll('.filter-row__checkbox').forEach(cb => { cb.addEventListener('change', () => { this.syncRowState(cb.closest('.filter-row'), cb.checked); }); }); this.panel.querySelectorAll('.filter-select, .filter-note-input').forEach(el => { el.addEventListener('mousedown', () => this.activateRowByControl(el)); }); this.periodSel?.addEventListener('change', () => { this.activateRowByControl(this.periodSel); this.toggleCustomDates(this.periodSel.value === 'custom'); }); this.panel.querySelectorAll('.filter-row__add').forEach(btn => { btn.addEventListener('click', () => this.addControl(btn)); }); this.panel.addEventListener('click', e => { const removeBtn = e.target.closest('.filter-row__remove'); if (removeBtn) this.removeControl(removeBtn); }); this.panel.addEventListener('change', e => { const sel = e.target.closest('.filter-select'); if (!sel) return; const container = sel.closest('.filter-row__controls'); if (container) this.refreshGroupSelects(container); }); this.panel.querySelectorAll('.filter-row').forEach(row => { const cb = row.querySelector('.filter-row__checkbox'); this.syncRowState(row, cb?.checked ?? false); }); this.panel.querySelectorAll('.filter-row__controls').forEach(container => { this.refreshGroupSelects(container); }); } togglePanel() { const isHidden = this.panel.hasAttribute('hidden'); if (isHidden) { this.panel.removeAttribute('hidden'); this.toggleBtn?.classList.add('report-toolbar__action--active'); } else { this.hidePanel(); } } hidePanel() { this.panel.setAttribute('hidden', ''); this.toggleBtn?.classList.remove('report-toolbar__action--active'); } syncRowState(row, active) { row.classList.toggle('filter-row--inactive', !active); } activateRowByControl(el) { const row = el.closest('.filter-row'); if (!row) return; const cb = row.querySelector('.filter-row__checkbox'); if (cb && !cb.checked) { cb.checked = true; this.syncRowState(row, true); } } toggleCustomDates(show) { if (!this.customDates) return; this.customDates.toggleAttribute('hidden', !show); } addControl(btn) { const targetId = btn.dataset.target; const container = document.getElementById(targetId); if (!container) return; const template = container.querySelector('.filter-row__control-group'); if (!template) return; const clone = template.cloneNode(true); const clonedSelect = clone.querySelector('.filter-select'); if (clonedSelect) clonedSelect.value = ''; if (!clone.querySelector('.filter-row__remove')) { const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'filter-row__remove'; removeBtn.textContent = '×'; clone.appendChild(removeBtn); } clone.querySelector('.filter-select')?.addEventListener('mousedown', () => { this.activateRowByControl(clone.querySelector('.filter-select')); }); container.appendChild(clone); this.refreshGroupSelects(container); const row = btn.closest('.filter-row'); const cb = row?.querySelector('.filter-row__checkbox'); if (cb && !cb.checked) { cb.checked = true; this.syncRowState(row, true); } clonedSelect?.focus(); } removeControl(removeBtn) { const group = removeBtn.closest('.filter-row__control-group'); const container = group?.parentElement; group?.remove(); if (container && !container.querySelector('.filter-row__control-group')) { const row = container.closest('.filter-row'); const cb = row?.querySelector('.filter-row__checkbox'); if (cb) { cb.checked = false; this.syncRowState(row, false); } } if (container) this.refreshGroupSelects(container); } refreshGroupSelects(container) { const selects = [...container.querySelectorAll('.filter-select')]; if (selects.length < 2) return; const selectedValues = new Set( selects.map(s => s.value).filter(v => v !== '') ); selects.forEach(sel => { const ownValue = sel.value; sel.querySelectorAll('option').forEach(opt => { if (!opt.value) return; opt.hidden = selectedValues.has(opt.value) && opt.value !== ownValue; }); }); } applyFilters() { const params = new URLSearchParams(); params.set('limit', String(window.Report?.limit ?? 50)); this.panel.querySelectorAll('.filter-row').forEach(row => { const cb = row.querySelector('.filter-row__checkbox'); if (!cb?.checked) return; const key = row.dataset.filterKey; if (['clients', 'projects', 'services', 'users'].includes(key)) { row.querySelectorAll('.filter-select').forEach(sel => { if (sel.value) params.append(`filter[${key}][]`, sel.value); }); if (row.querySelector('.filter-neg-checkbox')?.checked) { params.set(`filter[${key}_neg]`, '1'); } } else if (key === 'period') { const val = this.periodSel?.value; if (!val) return; params.set('filter[period]', val); if (val === 'custom' && this.customDates) { const get = field => this.customDates.querySelector(`[data-date-field="${field}"]`)?.value ?? ''; const fromDay = get('from-day').padStart(2, '0'); const fromMonth = get('from-month').padStart(2, '0'); const fromYear = get('from-year'); const toDay = get('to-day').padStart(2, '0'); const toMonth = get('to-month').padStart(2, '0'); const toYear = get('to-year'); if (fromYear && fromMonth && fromDay) { params.set('filter[date_from]', `${fromYear}-${fromMonth}-${fromDay}`); } if (toYear && toMonth && toDay) { params.set('filter[date_to]', `${toYear}-${toMonth}-${toDay}`); } } if (row.querySelector('.filter-neg-checkbox')?.checked) { params.set('filter[period_neg]', '1'); } } else if (key === 'note') { const val = row.querySelector('.filter-note-input')?.value?.trim(); if (val) params.set('filter[note]', val); } else if (key === 'invoiced') { const checked = row.querySelector('.filter-invoiced-radio:checked'); if (checked) params.set('filter[invoiced]', checked.value); } }); window.location.href = `/reports/times?${params}`; } } // ── Export ──────────────────────────────────────────────────────────────────── function initExportButtons() { ['excel', 'csv', 'pdf'].forEach(format => { document.getElementById(`btn-export-${format}`)?.addEventListener('click', () => { const params = new URLSearchParams(window.location.search); params.delete('limit'); window.location.href = `/reports/export/${format}?${params}`; }); }); } function initPrintButton() { document.getElementById('btn-print')?.addEventListener('click', () => { window.print(); }); }