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ů.
 
 
 
 
 

216 řádky
8.3 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. const lexofficeKey = document.getElementById('account-lexoffice-key');
  66. if (lexofficeKey && !lexofficeKey.hidden) {
  67. const keyVal = lexofficeKey.value.trim();
  68. if (keyVal) payload.lexofficeApiKey = keyVal;
  69. }
  70. btnAccountSave.disabled = true;
  71. try {
  72. await patchJson('/api/account', payload);
  73. showToast(t('savedReloading'));
  74. setTimeout(() => window.location.reload(), 1200);
  75. } catch (e) {
  76. showToast(e.message, true);
  77. } finally {
  78. btnAccountSave.disabled = false;
  79. }
  80. });
  81. }
  82. // ── Besitzer des Accounts ─────────────────────────────────────────────────
  83. const superadminSelect = document.getElementById('superadmin-select');
  84. if (superadminSelect && !superadminSelect.disabled) {
  85. superadminSelect.dataset.original = superadminSelect.value;
  86. superadminSelect.addEventListener('change', async () => {
  87. const selectedName = superadminSelect.options[superadminSelect.selectedIndex].text;
  88. if (!confirm(t('ownerConfirm').replace('%name%', selectedName))) {
  89. superadminSelect.value = superadminSelect.dataset.original;
  90. return;
  91. }
  92. try {
  93. await patchJson('/api/account/superadmin', {
  94. userId: parseInt(superadminSelect.value, 10),
  95. });
  96. showToast(t('ownerChanged'));
  97. setTimeout(() => window.location.reload(), 1500);
  98. } catch (e) {
  99. showToast(e.message, true);
  100. superadminSelect.value = superadminSelect.dataset.original;
  101. }
  102. });
  103. }
  104. // ── Lexoffice-Key-Toggle ───────────────────────────────────────────────────
  105. const btnLexKeyChange = document.getElementById('btn-lexoffice-key-change');
  106. const lexKeyInput = document.getElementById('account-lexoffice-key');
  107. const lexKeyStatus = document.getElementById('lexoffice-key-status');
  108. if (btnLexKeyChange && lexKeyInput && lexKeyStatus) {
  109. btnLexKeyChange.addEventListener('click', (e) => {
  110. e.preventDefault();
  111. const showing = !lexKeyInput.hidden;
  112. lexKeyInput.hidden = showing;
  113. lexKeyStatus.querySelector('.account-form__key-mask').hidden = !showing;
  114. btnLexKeyChange.textContent = showing ? t('changeLabel') : t('cancelLabel');
  115. if (!showing) lexKeyInput.focus();
  116. });
  117. }
  118. // ── Passwort-Toggle ───────────────────────────────────────────────────────
  119. const btnPwToggle = document.getElementById('btn-pw-toggle');
  120. const pwSection = document.getElementById('pw-section');
  121. if (btnPwToggle && pwSection) {
  122. btnPwToggle.addEventListener('click', (e) => {
  123. e.preventDefault();
  124. const open = !pwSection.hidden;
  125. pwSection.hidden = open;
  126. btnPwToggle.textContent = open ? t('changeLabel') : t('cancelLabel');
  127. });
  128. }
  129. // ── Theme-Picker ──────────────────────────────────────────────────────────
  130. const themePicker = document.getElementById('theme-picker');
  131. if (themePicker) {
  132. themePicker.querySelectorAll('input[name="theme"]').forEach(radio => {
  133. radio.addEventListener('change', async () => {
  134. const theme = radio.value;
  135. try {
  136. await patchJson('/api/account/user', { theme });
  137. themePicker.querySelectorAll('.theme-option').forEach(opt => {
  138. opt.classList.toggle('theme-option--active', opt.dataset.theme === theme);
  139. });
  140. document.body.dataset.theme = theme;
  141. showToast(t('themeChanged'));
  142. } catch (e) {
  143. showToast(e.message, true);
  144. }
  145. });
  146. });
  147. }
  148. // ── Benutzer-Formular ─────────────────────────────────────────────────────
  149. const btnUserSave = document.getElementById('btn-user-save');
  150. if (btnUserSave) {
  151. btnUserSave.addEventListener('click', async () => {
  152. const data = {
  153. firstName: document.getElementById('user-firstname').value.trim(),
  154. lastName: document.getElementById('user-lastname').value.trim(),
  155. email: document.getElementById('user-email').value.trim(),
  156. };
  157. if (pwSection && !pwSection.hidden) {
  158. const pwNew = document.getElementById('user-pw-new').value;
  159. const pwRepeat = document.getElementById('user-pw-repeat').value;
  160. if (pwNew !== pwRepeat) {
  161. showToast(t('passwordMismatch'), true);
  162. return;
  163. }
  164. data.currentPassword = document.getElementById('user-pw-current').value;
  165. data.newPassword = pwNew;
  166. }
  167. btnUserSave.disabled = true;
  168. try {
  169. await patchJson('/api/account/user', data);
  170. showToast(t('saved'));
  171. if (pwSection) {
  172. pwSection.hidden = true;
  173. document.getElementById('btn-pw-toggle').textContent = t('changeLabel');
  174. ['user-pw-current', 'user-pw-new', 'user-pw-repeat'].forEach(id => {
  175. document.getElementById(id).value = '';
  176. });
  177. }
  178. } catch (e) {
  179. showToast(e.message, true);
  180. } finally {
  181. btnUserSave.disabled = false;
  182. }
  183. });
  184. }
  185. });