|
- // 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 = `<option value="">${t('selectPh')}</option>`;
-
- 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;
- }
- });
- });
|