You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

223 regels
9.4 KiB

  1. // team.js
  2. document.addEventListener('DOMContentLoaded', () => {
  3. // ── Tabs ─────────────────────────────────────────────────────────────────────
  4. document.querySelectorAll('.crud-tab').forEach(tab => {
  5. tab.addEventListener('click', () => {
  6. document.querySelectorAll('.crud-tab').forEach(t =>
  7. t.classList.toggle('crud-tab--active', t === tab)
  8. );
  9. document.querySelectorAll('[data-tab-panel]').forEach(panel => {
  10. panel.hidden = panel.dataset.tabPanel !== tab.dataset.tab;
  11. });
  12. });
  13. });
  14. // ── Einlade-Modal ─────────────────────────────────────────────────────────────
  15. const modal = document.getElementById('team-modal');
  16. const errorsBox = document.getElementById('team-modal-errors');
  17. const openModal = () => { modal.hidden = false; };
  18. const closeModal = () => {
  19. modal.hidden = true;
  20. errorsBox.hidden = true;
  21. ['inv-firstName', 'inv-lastName', 'inv-email'].forEach(id => {
  22. document.getElementById(id).value = '';
  23. });
  24. const defaultRole = modal.querySelector('input[name="inv-role"][value="member"]');
  25. if (defaultRole) defaultRole.checked = true;
  26. };
  27. document.getElementById('team-invite-btn').addEventListener('click', openModal);
  28. document.getElementById('team-modal-close').addEventListener('click', closeModal);
  29. document.getElementById('team-modal-cancel').addEventListener('click', closeModal);
  30. modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
  31. document.getElementById('team-modal-submit').addEventListener('click', async () => {
  32. const payload = {
  33. firstName: document.getElementById('inv-firstName').value.trim(),
  34. lastName: document.getElementById('inv-lastName').value.trim(),
  35. email: document.getElementById('inv-email').value.trim(),
  36. role: modal.querySelector('input[name="inv-role"]:checked')?.value ?? 'member',
  37. };
  38. const res = await fetch('/api/team/invite', {
  39. method: 'POST',
  40. headers: { 'Content-Type': 'application/json' },
  41. body: JSON.stringify(payload),
  42. });
  43. const data = await res.json();
  44. if (!res.ok) {
  45. errorsBox.hidden = false;
  46. errorsBox.innerHTML = '<ul>' + (data.errors ?? [data.error]).map(e => `<li>${e}</li>`).join('') + '</ul>';
  47. return;
  48. }
  49. closeModal();
  50. window.location.reload();
  51. });
  52. // ── Listen-Delegation: aktive User + Einladungen ───────────────────────────
  53. const list = document.getElementById('team-list');
  54. if (list) {
  55. list.addEventListener('click', e => {
  56. const actionEl = e.target.closest('[data-action]');
  57. if (!actionEl) return;
  58. const action = actionEl.dataset.action;
  59. const row = e.target.closest('.crud-row');
  60. if (!row) return;
  61. switch (action) {
  62. case 'edit': openEdit(row); break;
  63. case 'save': saveEdit(row); break;
  64. case 'cancel': closeEdit(row); break;
  65. case 'delete': deleteMember(row); break;
  66. case 'delete-invite': deleteInvite(actionEl.dataset.id, row); break;
  67. }
  68. });
  69. }
  70. // ── Listen-Delegation: archivierte User ───────────────────────────────────
  71. const archivedList = document.getElementById('team-list-archived');
  72. if (archivedList) {
  73. archivedList.addEventListener('click', e => {
  74. const actionEl = e.target.closest('[data-action]');
  75. if (!actionEl) return;
  76. const row = e.target.closest('.crud-row');
  77. if (!row) return;
  78. if (actionEl.dataset.action === 'unarchive') {
  79. unarchiveMember(row);
  80. }
  81. });
  82. }
  83. // ── Inline Edit ───────────────────────────────────────────────────────────
  84. function openEdit(row) {
  85. row.querySelector('.crud-row__display').hidden = true;
  86. row.querySelector('.crud-row__edit').hidden = false;
  87. row.querySelector('.edit-first-name')?.focus();
  88. }
  89. function closeEdit(row) {
  90. row.querySelector('.crud-row__display').hidden = false;
  91. row.querySelector('.crud-row__edit').hidden = true;
  92. // Felder auf ursprüngliche Werte zurücksetzen
  93. row.querySelector('.edit-first-name').value = row.dataset.firstName ?? '';
  94. row.querySelector('.edit-last-name').value = row.dataset.lastName ?? '';
  95. row.querySelector('.edit-email').value = row.dataset.email ?? '';
  96. row.querySelector('.edit-note').value = row.dataset.note ?? '';
  97. const currentRole = row.dataset.role;
  98. row.querySelectorAll('.edit-role').forEach(radio => {
  99. radio.checked = radio.value === currentRole;
  100. });
  101. }
  102. async function saveEdit(row) {
  103. const id = row.dataset.id;
  104. const firstName = row.querySelector('.edit-first-name').value.trim();
  105. const lastName = row.querySelector('.edit-last-name').value.trim();
  106. const email = row.querySelector('.edit-email').value.trim();
  107. const note = row.querySelector('.edit-note').value || null;
  108. const role = row.querySelector('.edit-role:checked')?.value ?? row.dataset.role;
  109. const res = await fetch(`/api/team/${id}`, {
  110. method: 'PATCH',
  111. headers: { 'Content-Type': 'application/json' },
  112. body: JSON.stringify({ firstName, lastName, email, note, role }),
  113. });
  114. if (!res.ok) {
  115. const data = await res.json();
  116. alert((data.errors ?? [data.error]).join('\n'));
  117. return;
  118. }
  119. const data = await res.json();
  120. updateDisplay(row, data);
  121. closeEdit(row);
  122. }
  123. function updateDisplay(row, data) {
  124. row.querySelector('.crud-row__name').textContent = data.fullName;
  125. row.querySelector('.crud-row__meta').textContent = `(${data.roleLabel})`;
  126. row.dataset.firstName = data.firstName;
  127. row.dataset.lastName = data.lastName;
  128. row.dataset.email = data.email;
  129. row.dataset.note = data.note ?? '';
  130. row.dataset.role = data.role;
  131. // Edit-Felder aktualisieren
  132. row.querySelector('.edit-first-name').value = data.firstName;
  133. row.querySelector('.edit-last-name').value = data.lastName;
  134. row.querySelector('.edit-email').value = data.email;
  135. row.querySelector('.edit-note').value = data.note ?? '';
  136. row.querySelectorAll('.edit-role').forEach(radio => {
  137. radio.checked = radio.value === data.role;
  138. });
  139. }
  140. // ── Delete ────────────────────────────────────────────────────────────────
  141. async function deleteMember(row) {
  142. if (!confirm('Wirklich entfernen?')) return;
  143. const id = row.dataset.id;
  144. const res = await fetch(`/api/team/${id}`, { method: 'DELETE' });
  145. if (res.status === 409) {
  146. if (confirm('Dieser Benutzer hat Zeiteinträge und kann nicht gelöscht werden.\nStattdessen archivieren?')) {
  147. await archiveMember(row);
  148. }
  149. return;
  150. }
  151. if (!res.ok) {
  152. const data = await res.json();
  153. alert(data.error ?? 'Fehler beim Löschen.');
  154. return;
  155. }
  156. row.classList.add('crud-row--removing');
  157. setTimeout(() => row.remove(), 280);
  158. }
  159. async function deleteInvite(id, row) {
  160. if (!confirm('Einladung zurückziehen?')) return;
  161. const res = await fetch(`/api/team/invite/${id}`, { method: 'DELETE' });
  162. if (!res.ok) {
  163. const data = await res.json();
  164. alert(data.error ?? 'Fehler');
  165. return;
  166. }
  167. row.classList.add('crud-row--removing');
  168. setTimeout(() => row.remove(), 280);
  169. }
  170. // ── Archive / Unarchive ───────────────────────────────────────────────────
  171. async function archiveMember(row) {
  172. const id = row.dataset.id;
  173. const res = await fetch(`/api/team/${id}/archive`, { method: 'PATCH' });
  174. if (!res.ok) { alert('Fehler beim Archivieren.'); return; }
  175. row.classList.add('crud-row--removing');
  176. setTimeout(() => window.location.reload(), 280);
  177. }
  178. async function unarchiveMember(row) {
  179. const id = row.dataset.id;
  180. const res = await fetch(`/api/team/${id}/unarchive`, { method: 'PATCH' });
  181. if (!res.ok) { alert('Fehler beim Wiederherstellen.'); return; }
  182. row.classList.add('crud-row--removing');
  183. setTimeout(() => window.location.reload(), 280);
  184. }
  185. });