Você não pode selecionar mais de 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.
 
 
 
 
 

194 linhas
7.2 KiB

  1. // assets/scripts/account.js
  2. import { esc, createTranslator } from './utils.js';
  3. const TOAST_DURATION = 3000;
  4. const t = createTranslator('ACCOUNT');
  5. document.addEventListener('DOMContentLoaded', () => {
  6. const toast = document.getElementById('account-toast');
  7. function showToast(msg, isError = false) {
  8. toast.textContent = msg;
  9. toast.classList.toggle('account-toast--error', isError);
  10. toast.classList.add('account-toast--visible');
  11. setTimeout(() => toast.classList.remove('account-toast--visible'), TOAST_DURATION);
  12. }
  13. async function patchJson(url, data) {
  14. const res = await fetch(url, {
  15. method: 'PATCH',
  16. headers: { 'Content-Type': 'application/json' },
  17. body: JSON.stringify(data),
  18. });
  19. const json = await res.json();
  20. if (!res.ok) throw new Error(json.error ?? t('errorGeneric'));
  21. return json;
  22. }
  23. // ── Farbfeld: Picker <-> Hex-Input synchron + Live-Kontrast ───────────────
  24. const colorPicker = document.getElementById('account-color-picker');
  25. const colorHex = document.getElementById('account-color');
  26. function applyHeaderContrast(hex) {
  27. const r = parseInt(hex.slice(1, 3), 16);
  28. const g = parseInt(hex.slice(3, 5), 16);
  29. const b = parseInt(hex.slice(5, 7), 16);
  30. const brightness = (r * 299 + g * 587 + b * 114) / 1000;
  31. const isLight = brightness > 128;
  32. const root = document.documentElement;
  33. root.style.setProperty('--header-text', isLight ? '#1a2a3a' : '#ffffff');
  34. root.style.setProperty('--header-text-muted', isLight ? 'rgba(26, 42, 58, 0.65)' : 'rgba(255, 255, 255, 0.75)');
  35. root.style.setProperty('--header-overlay', isLight ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.18)');
  36. }
  37. if (colorPicker && colorHex) {
  38. colorPicker.addEventListener('input', () => {
  39. colorHex.value = colorPicker.value;
  40. applyHeaderContrast(colorPicker.value);
  41. });
  42. colorHex.addEventListener('input', () => {
  43. if (/^#[0-9a-fA-F]{6}$/.test(colorHex.value)) {
  44. colorPicker.value = colorHex.value;
  45. applyHeaderContrast(colorHex.value);
  46. }
  47. });
  48. }
  49. // ── Account-Formular ──────────────────────────────────────────────────────
  50. const btnAccountSave = document.getElementById('btn-account-save');
  51. if (btnAccountSave) {
  52. btnAccountSave.addEventListener('click', async () => {
  53. const payload = {
  54. name: document.getElementById('account-name').value.trim(),
  55. trackingInterval: parseInt(document.getElementById('account-interval').value, 10),
  56. };
  57. if (colorHex) {
  58. const hex = colorHex.value.trim();
  59. if (hex && !/^#[0-9a-fA-F]{6}$/.test(hex)) {
  60. showToast(t('invalidHex'), true);
  61. return;
  62. }
  63. payload.primaryColor = hex || '';
  64. }
  65. btnAccountSave.disabled = true;
  66. try {
  67. await patchJson('/api/account', payload);
  68. showToast(t('savedReloading'));
  69. setTimeout(() => window.location.reload(), 1200);
  70. } catch (e) {
  71. showToast(e.message, true);
  72. } finally {
  73. btnAccountSave.disabled = false;
  74. }
  75. });
  76. }
  77. // ── Besitzer des Accounts ─────────────────────────────────────────────────
  78. const superadminSelect = document.getElementById('superadmin-select');
  79. if (superadminSelect && !superadminSelect.disabled) {
  80. superadminSelect.dataset.original = superadminSelect.value;
  81. superadminSelect.addEventListener('change', async () => {
  82. const selectedName = superadminSelect.options[superadminSelect.selectedIndex].text;
  83. if (!confirm(t('ownerConfirm').replace('%name%', selectedName))) {
  84. superadminSelect.value = superadminSelect.dataset.original;
  85. return;
  86. }
  87. try {
  88. await patchJson('/api/account/superadmin', {
  89. userId: parseInt(superadminSelect.value, 10),
  90. });
  91. showToast(t('ownerChanged'));
  92. setTimeout(() => window.location.reload(), 1500);
  93. } catch (e) {
  94. showToast(e.message, true);
  95. superadminSelect.value = superadminSelect.dataset.original;
  96. }
  97. });
  98. }
  99. // ── Passwort-Toggle ───────────────────────────────────────────────────────
  100. const btnPwToggle = document.getElementById('btn-pw-toggle');
  101. const pwSection = document.getElementById('pw-section');
  102. if (btnPwToggle && pwSection) {
  103. btnPwToggle.addEventListener('click', (e) => {
  104. e.preventDefault();
  105. const open = !pwSection.hidden;
  106. pwSection.hidden = open;
  107. btnPwToggle.textContent = open ? t('changeLabel') : t('cancelLabel');
  108. });
  109. }
  110. // ── Theme-Picker ──────────────────────────────────────────────────────────
  111. const themePicker = document.getElementById('theme-picker');
  112. if (themePicker) {
  113. themePicker.querySelectorAll('input[name="theme"]').forEach(radio => {
  114. radio.addEventListener('change', async () => {
  115. const theme = radio.value;
  116. try {
  117. await patchJson('/api/account/user', { theme });
  118. themePicker.querySelectorAll('.theme-option').forEach(opt => {
  119. opt.classList.toggle('theme-option--active', opt.dataset.theme === theme);
  120. });
  121. document.body.dataset.theme = theme;
  122. showToast(t('themeChanged'));
  123. } catch (e) {
  124. showToast(e.message, true);
  125. }
  126. });
  127. });
  128. }
  129. // ── Benutzer-Formular ─────────────────────────────────────────────────────
  130. const btnUserSave = document.getElementById('btn-user-save');
  131. if (btnUserSave) {
  132. btnUserSave.addEventListener('click', async () => {
  133. const data = {
  134. firstName: document.getElementById('user-firstname').value.trim(),
  135. lastName: document.getElementById('user-lastname').value.trim(),
  136. email: document.getElementById('user-email').value.trim(),
  137. };
  138. if (pwSection && !pwSection.hidden) {
  139. const pwNew = document.getElementById('user-pw-new').value;
  140. const pwRepeat = document.getElementById('user-pw-repeat').value;
  141. if (pwNew !== pwRepeat) {
  142. showToast(t('passwordMismatch'), true);
  143. return;
  144. }
  145. data.currentPassword = document.getElementById('user-pw-current').value;
  146. data.newPassword = pwNew;
  147. }
  148. btnUserSave.disabled = true;
  149. try {
  150. await patchJson('/api/account/user', data);
  151. showToast(t('saved'));
  152. if (pwSection) {
  153. pwSection.hidden = true;
  154. document.getElementById('btn-pw-toggle').textContent = t('changeLabel');
  155. ['user-pw-current', 'user-pw-new', 'user-pw-repeat'].forEach(id => {
  156. document.getElementById(id).value = '';
  157. });
  158. }
  159. } catch (e) {
  160. showToast(e.message, true);
  161. } finally {
  162. btnUserSave.disabled = false;
  163. }
  164. });
  165. }
  166. });