Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 

200 řádky
7.1 KiB

  1. // assets/scripts/report.js
  2. import {
  3. parseDuration,
  4. roundToQuarter,
  5. formatMinutes,
  6. validateDuration,
  7. initDurationBlurHandler,
  8. } from './duration.js';
  9. // ── Hilfsfunktionen ───────────────────────────────────────────────────────────
  10. function t(key) {
  11. return window.Report?.i18n?.[key] ?? key;
  12. }
  13. function populateProjectSelect(select, selectedId) {
  14. const projects = window.Report?.projects ?? [];
  15. select.innerHTML = '';
  16. projects.forEach(p => {
  17. const opt = document.createElement('option');
  18. opt.value = p.id;
  19. opt.textContent = `${p.clientName} / ${p.name}`;
  20. if (p.id === selectedId) opt.selected = true;
  21. select.appendChild(opt);
  22. });
  23. }
  24. function populateServiceSelect(select, selectedId) {
  25. const services = window.Report?.services ?? [];
  26. const billable = services.filter(s => s.billable);
  27. const notBillable = services.filter(s => !s.billable);
  28. select.innerHTML = `<option value="">${t('selectPh')}</option>`;
  29. function addGroup(label, list) {
  30. if (!list.length) return;
  31. const group = document.createElement('optgroup');
  32. group.label = label;
  33. list.forEach(s => {
  34. const opt = document.createElement('option');
  35. opt.value = s.id;
  36. opt.textContent = s.name;
  37. if (s.id === selectedId) opt.selected = true;
  38. group.appendChild(opt);
  39. });
  40. select.appendChild(group);
  41. }
  42. addGroup(t('billable'), billable);
  43. addGroup(t('notBillable'), notBillable);
  44. }
  45. // ── Edit öffnen ───────────────────────────────────────────────────────────────
  46. function openEdit(row) {
  47. document.querySelectorAll('.report-table__row--editing').forEach(r => {
  48. if (r !== row) closeEdit(r);
  49. });
  50. const editForm = row.querySelector('.report-row__edit');
  51. if (!editForm) return;
  52. // Selects klonen um akkumulierte Listener zu vermeiden
  53. const oldProjectSel = row.querySelector('.edit-project');
  54. const oldServiceSel = row.querySelector('.edit-service');
  55. const projectSel = oldProjectSel.cloneNode(false);
  56. const serviceSel = oldServiceSel.cloneNode(false);
  57. oldProjectSel.replaceWith(projectSel);
  58. oldServiceSel.replaceWith(serviceSel);
  59. const projectId = parseInt(row.dataset.projectId) || null;
  60. const serviceId = parseInt(row.dataset.serviceId) || null;
  61. populateProjectSelect(projectSel, projectId);
  62. populateServiceSelect(serviceSel, serviceId);
  63. projectSel.addEventListener('change', () => {
  64. populateServiceSelect(row.querySelector('.edit-service'), null);
  65. });
  66. editForm.hidden = false;
  67. row.classList.add('report-table__row--editing');
  68. row.querySelector('.edit-duration')?.focus();
  69. }
  70. function closeEdit(row) {
  71. const editForm = row.querySelector('.report-row__edit');
  72. if (!editForm) return;
  73. editForm.hidden = true;
  74. row.classList.remove('report-table__row--editing');
  75. }
  76. // ── Speichern ─────────────────────────────────────────────────────────────────
  77. async function saveEdit(row) {
  78. const id = row.dataset.entryId;
  79. const durationRaw = row.querySelector('.edit-duration')?.value ?? '0:00';
  80. const projectId = row.querySelector('.edit-project')?.value;
  81. const serviceId = row.querySelector('.edit-service')?.value;
  82. const note = row.querySelector('.edit-note')?.value ?? '';
  83. if (!projectId) { alert(t('errorNoProject')); return; }
  84. const rawMinutes = roundToQuarter(parseDuration(durationRaw));
  85. if (rawMinutes === 0) { alert(t('errorZeroDuration')); return; }
  86. const validation = validateDuration(rawMinutes);
  87. if (validation.status === 'error') { alert(t('errorDurationTooLong')); return; }
  88. if (validation.status === 'warn' && !confirm(t('warnDurationLong'))) return;
  89. try {
  90. const res = await fetch(`/api/entries/${id}`, {
  91. method: 'PATCH',
  92. headers: { 'Content-Type': 'application/json' },
  93. body: JSON.stringify({
  94. duration: formatMinutes(rawMinutes),
  95. projectId: parseInt(projectId),
  96. serviceId: serviceId ? parseInt(serviceId) : null,
  97. note: note || null,
  98. }),
  99. });
  100. if (!res.ok) { alert(t('errorSave')); return; }
  101. window.location.reload();
  102. } catch {
  103. alert(t('errorSave'));
  104. }
  105. }
  106. // ── Löschen ───────────────────────────────────────────────────────────────────
  107. async function deleteEntry(row) {
  108. if (!confirm(t('confirmDelete'))) return;
  109. const id = row.dataset.entryId;
  110. try {
  111. const res = await fetch(`/api/entries/${id}`, { method: 'DELETE' });
  112. if (!res.ok) { alert(t('errorDelete')); return; }
  113. window.location.reload();
  114. } catch {
  115. alert(t('errorDelete'));
  116. }
  117. }
  118. // ── Abgerechnet toggeln ───────────────────────────────────────────────────────
  119. async function toggleInvoiced(row) {
  120. const id = row.dataset.entryId;
  121. const btn = row.querySelector('[data-action="toggle-invoiced"]');
  122. try {
  123. const res = await fetch(`/api/entries/${id}/invoiced`, { method: 'PATCH' });
  124. if (!res.ok) return;
  125. const data = await res.json();
  126. const invoiced = data.invoiced;
  127. row.dataset.invoiced = invoiced ? 'true' : 'false';
  128. row.classList.toggle('report-table__row--invoiced', invoiced);
  129. if (btn) {
  130. btn.classList.toggle('report-lock--invoiced', invoiced);
  131. btn.title = invoiced ? t('btnUnlock') : t('btnLock');
  132. }
  133. } catch (err) {
  134. console.error('Fehler beim Toggeln des Abrechnungsstatus:', err);
  135. }
  136. }
  137. // ── Event-Delegation ──────────────────────────────────────────────────────────
  138. document.addEventListener('DOMContentLoaded', () => {
  139. initDurationBlurHandler();
  140. const table = document.querySelector('.report-table');
  141. if (!table) return;
  142. table.addEventListener('click', e => {
  143. const btn = e.target.closest('[data-action]');
  144. if (!btn) return;
  145. const row = btn.closest('.report-table__row');
  146. if (!row) return;
  147. switch (btn.dataset.action) {
  148. case 'edit': openEdit(row); break;
  149. case 'cancel': closeEdit(row); break;
  150. case 'save': saveEdit(row); break;
  151. case 'delete': deleteEntry(row); break;
  152. case 'toggle-invoiced': toggleInvoiced(row); break;
  153. }
  154. });
  155. });