Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 

262 linhas
9.3 KiB

  1. // assets/scripts/team.js
  2. import { esc, createTranslator, ANIMATION_MS, removeWithAnimation } from './utils.js';
  3. const t = createTranslator('Team');
  4. document.addEventListener('DOMContentLoaded', () => {
  5. // ── Tabs ──────────────────────────────────────────────────────────────────
  6. document.querySelectorAll('.crud-tab').forEach(tab => {
  7. tab.addEventListener('click', () => {
  8. document.querySelectorAll('.crud-tab').forEach(t =>
  9. t.classList.toggle('crud-tab--active', t === tab)
  10. );
  11. document.querySelectorAll('[data-tab-panel]').forEach(panel => {
  12. panel.hidden = panel.dataset.tabPanel !== tab.dataset.tab;
  13. });
  14. });
  15. });
  16. // ── Einlade-Modal ─────────────────────────────────────────────────────────
  17. const modal = document.getElementById('team-modal');
  18. const errorsBox = document.getElementById('team-modal-errors');
  19. const openModal = () => { modal.hidden = false; };
  20. const closeModal = () => {
  21. modal.hidden = true;
  22. errorsBox.hidden = true;
  23. ['inv-firstName', 'inv-lastName', 'inv-email'].forEach(id => {
  24. document.getElementById(id).value = '';
  25. });
  26. const defaultRole = modal.querySelector('input[name="inv-role"][value="member"]');
  27. if (defaultRole) defaultRole.checked = true;
  28. };
  29. document.getElementById('team-invite-btn').addEventListener('click', openModal);
  30. document.getElementById('team-modal-close').addEventListener('click', closeModal);
  31. document.getElementById('team-modal-cancel').addEventListener('click', closeModal);
  32. modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
  33. const submitBtn = document.getElementById('team-modal-submit');
  34. submitBtn.addEventListener('click', async () => {
  35. const payload = {
  36. firstName: document.getElementById('inv-firstName').value.trim(),
  37. lastName: document.getElementById('inv-lastName').value.trim(),
  38. email: document.getElementById('inv-email').value.trim(),
  39. role: modal.querySelector('input[name="inv-role"]:checked')?.value ?? 'member',
  40. };
  41. submitBtn.disabled = true;
  42. try {
  43. const res = await fetch('/api/team/invite', {
  44. method: 'POST',
  45. headers: { 'Content-Type': 'application/json' },
  46. body: JSON.stringify(payload),
  47. });
  48. const data = await res.json();
  49. if (!res.ok) {
  50. errorsBox.hidden = false;
  51. const errors = data.errors ?? [data.error];
  52. errorsBox.innerHTML = '<ul>' + errors.map(e => `<li>${esc(e)}</li>`).join('') + '</ul>';
  53. return;
  54. }
  55. closeModal();
  56. window.location.reload();
  57. } catch {
  58. errorsBox.hidden = false;
  59. errorsBox.innerHTML = `<ul><li>${esc(t('errorSave'))}</li></ul>`;
  60. } finally {
  61. submitBtn.disabled = false;
  62. }
  63. });
  64. // ── Listen-Delegation: aktive User + Einladungen ──────────────────────────
  65. const list = document.getElementById('team-list');
  66. if (list) {
  67. list.addEventListener('click', e => {
  68. const actionEl = e.target.closest('[data-action]');
  69. if (!actionEl) return;
  70. const row = e.target.closest('.crud-row');
  71. if (!row) return;
  72. switch (actionEl.dataset.action) {
  73. case 'edit': openEdit(row); break;
  74. case 'save': saveEdit(row); break;
  75. case 'cancel': closeEdit(row); break;
  76. case 'delete': deleteMember(row); break;
  77. case 'delete-invite': deleteInvite(actionEl.dataset.id, row); break;
  78. }
  79. });
  80. }
  81. // ── Listen-Delegation: archivierte User ───────────────────────────────────
  82. const archivedList = document.getElementById('team-list-archived');
  83. if (archivedList) {
  84. archivedList.addEventListener('click', e => {
  85. const actionEl = e.target.closest('[data-action]');
  86. if (!actionEl) return;
  87. const row = e.target.closest('.crud-row');
  88. if (!row) return;
  89. if (actionEl.dataset.action === 'unarchive') {
  90. unarchiveMember(row);
  91. }
  92. });
  93. }
  94. // ── Inline Edit ───────────────────────────────────────────────────────────
  95. function openEdit(row) {
  96. row.querySelector('.crud-row__display').hidden = true;
  97. row.querySelector('.crud-row__edit').hidden = false;
  98. row.querySelector('.edit-first-name')?.focus();
  99. }
  100. function closeEdit(row) {
  101. row.querySelector('.crud-row__display').hidden = false;
  102. row.querySelector('.crud-row__edit').hidden = true;
  103. row.querySelector('.edit-first-name').value = row.dataset.firstName ?? '';
  104. row.querySelector('.edit-last-name').value = row.dataset.lastName ?? '';
  105. row.querySelector('.edit-email').value = row.dataset.email ?? '';
  106. row.querySelector('.edit-note').value = row.dataset.note ?? '';
  107. const currentRole = row.dataset.role;
  108. row.querySelectorAll('.edit-role').forEach(radio => {
  109. radio.checked = radio.value === currentRole;
  110. });
  111. }
  112. async function saveEdit(row) {
  113. const saveBtn = row.querySelector('[data-action="save"]');
  114. if (saveBtn?.disabled) return;
  115. const id = row.dataset.id;
  116. const firstName = row.querySelector('.edit-first-name').value.trim();
  117. const lastName = row.querySelector('.edit-last-name').value.trim();
  118. const email = row.querySelector('.edit-email').value.trim();
  119. const note = row.querySelector('.edit-note').value || null;
  120. const role = row.querySelector('.edit-role:checked')?.value ?? row.dataset.role;
  121. if (saveBtn) saveBtn.disabled = true;
  122. try {
  123. const res = await fetch(`/api/team/${id}`, {
  124. method: 'PATCH',
  125. headers: { 'Content-Type': 'application/json' },
  126. body: JSON.stringify({ firstName, lastName, email, note, role }),
  127. });
  128. if (!res.ok) {
  129. const data = await res.json();
  130. alert((data.errors ?? [data.error]).join('\n'));
  131. return;
  132. }
  133. const data = await res.json();
  134. updateDisplay(row, data);
  135. closeEdit(row);
  136. } catch {
  137. alert(t('errorSave'));
  138. } finally {
  139. if (saveBtn) saveBtn.disabled = false;
  140. }
  141. }
  142. function updateDisplay(row, data) {
  143. row.querySelector('.crud-row__name').textContent = data.fullName;
  144. row.querySelector('.crud-row__meta').textContent = `(${data.roleLabel})`;
  145. row.dataset.firstName = data.firstName;
  146. row.dataset.lastName = data.lastName;
  147. row.dataset.email = data.email;
  148. row.dataset.note = data.note ?? '';
  149. row.dataset.role = data.role;
  150. row.querySelector('.edit-first-name').value = data.firstName;
  151. row.querySelector('.edit-last-name').value = data.lastName;
  152. row.querySelector('.edit-email').value = data.email;
  153. row.querySelector('.edit-note').value = data.note ?? '';
  154. row.querySelectorAll('.edit-role').forEach(radio => {
  155. radio.checked = radio.value === data.role;
  156. });
  157. }
  158. // ── Delete ────────────────────────────────────────────────────────────────
  159. async function deleteMember(row) {
  160. if (!confirm(t('confirmDelete'))) return;
  161. try {
  162. const res = await fetch(`/api/team/${row.dataset.id}`, { method: 'DELETE' });
  163. if (res.status === 409) {
  164. if (confirm(t('confirmArchive'))) {
  165. await archiveMember(row);
  166. }
  167. return;
  168. }
  169. if (!res.ok) {
  170. const data = await res.json().catch(() => ({}));
  171. alert(data.error ?? t('errorDelete'));
  172. return;
  173. }
  174. removeWithAnimation(row, 'crud-row--removing');
  175. } catch {
  176. alert(t('errorDelete'));
  177. }
  178. }
  179. async function deleteInvite(id, row) {
  180. if (!confirm(t('confirmRevokeInvite'))) return;
  181. try {
  182. const res = await fetch(`/api/team/invite/${id}`, { method: 'DELETE' });
  183. if (!res.ok) {
  184. const data = await res.json().catch(() => ({}));
  185. alert(data.error ?? t('errorGeneric'));
  186. return;
  187. }
  188. removeWithAnimation(row, 'crud-row--removing');
  189. } catch {
  190. alert(t('errorGeneric'));
  191. }
  192. }
  193. // ── Archive / Unarchive ───────────────────────────────────────────────────
  194. async function archiveMember(row) {
  195. try {
  196. const res = await fetch(`/api/team/${row.dataset.id}/archive`, { method: 'PATCH' });
  197. if (!res.ok) { alert(t('errorArchive')); return; }
  198. removeWithAnimation(row, 'crud-row--removing');
  199. setTimeout(() => window.location.reload(), ANIMATION_MS);
  200. } catch {
  201. alert(t('errorArchive'));
  202. }
  203. }
  204. async function unarchiveMember(row) {
  205. try {
  206. const res = await fetch(`/api/team/${row.dataset.id}/unarchive`, { method: 'PATCH' });
  207. if (!res.ok) { alert(t('errorRestore')); return; }
  208. removeWithAnimation(row, 'crud-row--removing');
  209. setTimeout(() => window.location.reload(), ANIMATION_MS);
  210. } catch {
  211. alert(t('errorRestore'));
  212. }
  213. }
  214. });