|
- // 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 = '<dl class="import-preview__stats">';
- for (const row of rows) {
- html += `<dt>${esc(row.label)}</dt><dd>${esc(String(row.value))}</dd>`;
- }
-
- if (data.dateRange.from && data.dateRange.to) {
- html += `<dt>${esc(t('importLabelDateRange'))}</dt><dd>${esc(formatDate(data.dateRange.from))} – ${esc(formatDate(data.dateRange.to))}</dd>`;
- }
- html += '</dl>';
-
- // Benutzer-Zuordnung
- if (data.users.length > 0) {
- html += `<h3 class="import-preview__subtitle">${esc(t('importLabelUsers'))}</h3>`;
- html += '<ul class="import-preview__users">';
- for (const u of data.users) {
- html += `<li><strong>${esc(u.name)}</strong> <span class="import-preview__email">(${esc(u.email)})</span> — ${u.entryCount} ${esc(t('importLabelEntries'))}`;
- if (u.matched) {
- html += ` <span class="import-badge import-badge--matched">${esc(t('importUserMatched'))} ${esc(u.matchedUserName)}</span>`;
- } else {
- const radioName = `user-mode-${u.miteId}`;
- html += `<div class="import-user-options">`;
- html += `<label class="import-user-option"><input type="radio" name="${radioName}" value="me" checked> ${esc(t('importUserAssignMe'))}</label>`;
- html += `<label class="import-user-option"><input type="radio" name="${radioName}" value="create"> ${esc(t('importUserCreate'))}</label>`;
- html += `</div>`;
- }
- html += `</li>`;
- }
- html += '</ul>';
- }
-
- // Warnungen
- if (data.warnings.length > 0) {
- html += `<h3 class="import-preview__subtitle">${esc(t('importLabelWarnings'))}</h3>`;
- html += '<ul class="import-preview__warnings">';
- for (const w of data.warnings) {
- html += `<li>${esc(w)}</li>`;
- }
- html += '</ul>';
- }
-
- previewContent.innerHTML = html;
- }
-
- function renderResult(data) {
- function statLine(label, total, created) {
- const existing = total - created;
- let detail = `<strong>${created}</strong> ${esc(t('importNewLabel'))}`;
- if (existing > 0) {
- detail += `, ${existing} ${esc(t('importExistingLabel'))}`;
- }
- return `<dt>${esc(label)}</dt><dd>${detail}</dd>`;
- }
-
- let html = '<dl class="import-preview__stats import-preview__stats--result">';
- 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 += `<dt>${esc(t('importResultEntries'))}</dt><dd><strong>${data.timeEntries}</strong></dd>`;
- if (data.usersCreated > 0) {
- html += `<dt>${esc(t('importResultUsers'))}</dt><dd><strong>${data.usersCreated}</strong></dd>`;
- }
- html += '</dl>';
- 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' });
- }
- }
- });
|