// assets/scripts/account.js import { esc, createTranslator } from './utils.js'; const TOAST_DURATION = 3000; const t = createTranslator('ACCOUNT'); document.addEventListener('DOMContentLoaded', () => { const toast = document.getElementById('account-toast'); function showToast(msg, isError = false) { toast.textContent = msg; toast.classList.toggle('account-toast--error', isError); toast.classList.add('account-toast--visible'); setTimeout(() => toast.classList.remove('account-toast--visible'), TOAST_DURATION); } async function patchJson(url, data) { const res = await fetch(url, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const json = await res.json(); if (!res.ok) throw new Error(json.error ?? t('errorGeneric')); return json; } // ── Farbfeld: Picker <-> Hex-Input synchron + Live-Kontrast ─────────────── const colorPicker = document.getElementById('account-color-picker'); const colorHex = document.getElementById('account-color'); function applyHeaderContrast(hex) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); const brightness = (r * 299 + g * 587 + b * 114) / 1000; const isLight = brightness > 128; const root = document.documentElement; root.style.setProperty('--header-text', isLight ? '#1a2a3a' : '#ffffff'); root.style.setProperty('--header-text-muted', isLight ? 'rgba(26, 42, 58, 0.65)' : 'rgba(255, 255, 255, 0.75)'); root.style.setProperty('--header-overlay', isLight ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.18)'); } if (colorPicker && colorHex) { colorPicker.addEventListener('input', () => { colorHex.value = colorPicker.value; applyHeaderContrast(colorPicker.value); }); colorHex.addEventListener('input', () => { if (/^#[0-9a-fA-F]{6}$/.test(colorHex.value)) { colorPicker.value = colorHex.value; applyHeaderContrast(colorHex.value); } }); } // ── Account-Formular ────────────────────────────────────────────────────── const btnAccountSave = document.getElementById('btn-account-save'); if (btnAccountSave) { btnAccountSave.addEventListener('click', async () => { const payload = { name: document.getElementById('account-name').value.trim(), trackingInterval: parseInt(document.getElementById('account-interval').value, 10), }; if (colorHex) { const hex = colorHex.value.trim(); if (hex && !/^#[0-9a-fA-F]{6}$/.test(hex)) { showToast(t('invalidHex'), true); return; } payload.primaryColor = hex || ''; } const lexofficeKey = document.getElementById('account-lexoffice-key'); if (lexofficeKey && !lexofficeKey.hidden) { const keyVal = lexofficeKey.value.trim(); if (keyVal) payload.lexofficeApiKey = keyVal; } btnAccountSave.disabled = true; try { await patchJson('/api/account', payload); showToast(t('savedReloading')); setTimeout(() => window.location.reload(), 1200); } catch (e) { showToast(e.message, true); } finally { btnAccountSave.disabled = false; } }); } // ── Besitzer des Accounts ───────────────────────────────────────────────── const superadminSelect = document.getElementById('superadmin-select'); if (superadminSelect && !superadminSelect.disabled) { superadminSelect.dataset.original = superadminSelect.value; superadminSelect.addEventListener('change', async () => { const selectedName = superadminSelect.options[superadminSelect.selectedIndex].text; if (!confirm(t('ownerConfirm').replace('%name%', selectedName))) { superadminSelect.value = superadminSelect.dataset.original; return; } try { await patchJson('/api/account/superadmin', { userId: parseInt(superadminSelect.value, 10), }); showToast(t('ownerChanged')); setTimeout(() => window.location.reload(), 1500); } catch (e) { showToast(e.message, true); superadminSelect.value = superadminSelect.dataset.original; } }); } // ── Lexoffice-Key-Toggle ─────────────────────────────────────────────────── const btnLexKeyChange = document.getElementById('btn-lexoffice-key-change'); const lexKeyInput = document.getElementById('account-lexoffice-key'); const lexKeyStatus = document.getElementById('lexoffice-key-status'); if (btnLexKeyChange && lexKeyInput && lexKeyStatus) { btnLexKeyChange.addEventListener('click', (e) => { e.preventDefault(); const showing = !lexKeyInput.hidden; lexKeyInput.hidden = showing; lexKeyStatus.querySelector('.account-form__key-mask').hidden = !showing; btnLexKeyChange.textContent = showing ? t('changeLabel') : t('cancelLabel'); if (!showing) lexKeyInput.focus(); }); } // ── Passwort-Toggle ─────────────────────────────────────────────────────── const btnPwToggle = document.getElementById('btn-pw-toggle'); const pwSection = document.getElementById('pw-section'); if (btnPwToggle && pwSection) { btnPwToggle.addEventListener('click', (e) => { e.preventDefault(); const open = !pwSection.hidden; pwSection.hidden = open; btnPwToggle.textContent = open ? t('changeLabel') : t('cancelLabel'); }); } // ── Theme-Picker ────────────────────────────────────────────────────────── const themePicker = document.getElementById('theme-picker'); if (themePicker) { themePicker.querySelectorAll('input[name="theme"]').forEach(radio => { radio.addEventListener('change', async () => { const theme = radio.value; try { await patchJson('/api/account/user', { theme }); themePicker.querySelectorAll('.theme-option').forEach(opt => { opt.classList.toggle('theme-option--active', opt.dataset.theme === theme); }); document.body.dataset.theme = theme; showToast(t('themeChanged')); } catch (e) { showToast(e.message, true); } }); }); } // ── Benutzer-Formular ───────────────────────────────────────────────────── const btnUserSave = document.getElementById('btn-user-save'); if (btnUserSave) { btnUserSave.addEventListener('click', async () => { const data = { firstName: document.getElementById('user-firstname').value.trim(), lastName: document.getElementById('user-lastname').value.trim(), email: document.getElementById('user-email').value.trim(), }; if (pwSection && !pwSection.hidden) { const pwNew = document.getElementById('user-pw-new').value; const pwRepeat = document.getElementById('user-pw-repeat').value; if (pwNew !== pwRepeat) { showToast(t('passwordMismatch'), true); return; } data.currentPassword = document.getElementById('user-pw-current').value; data.newPassword = pwNew; } btnUserSave.disabled = true; try { await patchJson('/api/account/user', data); showToast(t('saved')); if (pwSection) { pwSection.hidden = true; document.getElementById('btn-pw-toggle').textContent = t('changeLabel'); ['user-pw-current', 'user-pw-new', 'user-pw-repeat'].forEach(id => { document.getElementById(id).value = ''; }); } } catch (e) { showToast(e.message, true); } finally { btnUserSave.disabled = false; } }); } // ── mite-Import ───────────────────────────────────────────────────────────── const importFile = document.getElementById('import-file'); const importFileInfo = document.getElementById('import-file-info'); const importFileName = document.getElementById('import-file-name'); const importFileRemove = document.getElementById('import-file-remove'); const btnAnalyze = document.getElementById('btn-import-analyze'); const btnExecute = document.getElementById('btn-import-execute'); const btnReset = document.getElementById('btn-import-reset'); const previewCard = document.getElementById('import-preview'); const previewContent = document.getElementById('import-preview-content'); const resultCard = document.getElementById('import-result'); const resultContent = document.getElementById('import-result-content'); const dropArea = document.getElementById('import-drop-area'); if (importFile) { let selectedFile = null; function setFile(file) { if (!file || !file.name.toLowerCase().endsWith('.xml')) { showToast(t('importErrorNoFile'), true); return; } selectedFile = file; importFileName.textContent = file.name + ' (' + formatFileSize(file.size) + ')'; importFileInfo.hidden = false; btnAnalyze.disabled = false; previewCard.hidden = true; resultCard.hidden = true; } function clearFile() { selectedFile = null; importFile.value = ''; importFileInfo.hidden = true; btnAnalyze.disabled = true; previewCard.hidden = true; resultCard.hidden = true; } function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } importFile.addEventListener('change', () => { if (importFile.files.length > 0) setFile(importFile.files[0]); }); importFileRemove.addEventListener('click', clearFile); // Drag & Drop ['dragenter', 'dragover'].forEach(evt => { dropArea.addEventListener(evt, (e) => { e.preventDefault(); dropArea.classList.add('import-upload__area--dragover'); }); }); ['dragleave', 'drop'].forEach(evt => { dropArea.addEventListener(evt, () => { dropArea.classList.remove('import-upload__area--dragover'); }); }); dropArea.addEventListener('drop', (e) => { e.preventDefault(); if (e.dataTransfer.files.length > 0) setFile(e.dataTransfer.files[0]); }); // Analyse btnAnalyze.addEventListener('click', async () => { if (!selectedFile) return; btnAnalyze.disabled = true; btnAnalyze.textContent = t('importAnalyzing'); const form = new FormData(); form.append('file', selectedFile); try { const res = await fetch('/api/import/mite/preview', { method: 'POST', body: form }); const json = await res.json(); if (!res.ok) throw new Error(json.error ?? t('errorGeneric')); renderPreview(json); previewCard.hidden = false; resultCard.hidden = true; } catch (e) { showToast(e.message, true); } finally { btnAnalyze.textContent = t('importAnalyze'); btnAnalyze.disabled = false; } }); // Import ausführen btnExecute.addEventListener('click', async () => { if (!selectedFile) return; if (!confirm(t('importConfirm'))) return; btnExecute.disabled = true; btnExecute.textContent = t('importExecuting'); const form = new FormData(); form.append('file', selectedFile); const createUserIds = []; previewContent.querySelectorAll('input[name^="user-mode-"]:checked').forEach(radio => { if (radio.value === 'create') { createUserIds.push(parseInt(radio.name.replace('user-mode-', ''), 10)); } }); form.append('createUsers', JSON.stringify(createUserIds)); try { const res = await fetch('/api/import/mite/execute', { method: 'POST', body: form }); const json = await res.json(); if (!res.ok) throw new Error(json.error ?? t('errorGeneric')); renderResult(json); previewCard.hidden = true; resultCard.hidden = false; showToast(t('importSuccess')); } catch (e) { showToast(e.message, true); } finally { btnExecute.textContent = t('importExecute'); btnExecute.disabled = false; } }); // Reset btnReset.addEventListener('click', () => { previewCard.hidden = true; resultCard.hidden = true; }); function renderPreview(data) { const rows = [ { label: t('importLabelClients'), value: data.customers }, { label: t('importLabelProjects'), value: data.projects }, { label: t('importLabelServices'), value: data.services }, { label: t('importLabelEntries'), value: data.timeEntries }, ]; let html = '
'; for (const row of rows) { html += `
${esc(row.label)}
${esc(String(row.value))}
`; } if (data.dateRange.from && data.dateRange.to) { html += `
${esc(t('importLabelDateRange'))}
${esc(formatDate(data.dateRange.from))} – ${esc(formatDate(data.dateRange.to))}
`; } html += '
'; // Benutzer-Zuordnung if (data.users.length > 0) { html += `

${esc(t('importLabelUsers'))}

`; html += ''; } // Warnungen if (data.warnings.length > 0) { html += `

${esc(t('importLabelWarnings'))}

`; html += ''; } previewContent.innerHTML = html; } function renderResult(data) { function statLine(label, total, created) { const existing = total - created; let detail = `${created} ${esc(t('importNewLabel'))}`; if (existing > 0) { detail += `, ${existing} ${esc(t('importExistingLabel'))}`; } return `
${esc(label)}
${detail}
`; } let html = '
'; html += statLine(t('importLabelClients'), data.clients, data.clientsCreated); html += statLine(t('importLabelProjects'), data.projects, data.projectsCreated); html += statLine(t('importLabelServices'), data.services, data.servicesCreated); html += `
${esc(t('importResultEntries'))}
${data.timeEntries}
`; if (data.usersCreated > 0) { html += `
${esc(t('importResultUsers'))}
${data.usersCreated}
`; } html += '
'; resultContent.innerHTML = html; } function formatDate(dateStr) { const d = new Date(dateStr + 'T00:00:00'); return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } } });