/** * TASKMATE - Admin Dashboard * ========================== * Benutzerverwaltung fuer Administratoren */ import api from './api.js'; import { $, $$ } from './utils.js'; import authManager from './auth.js'; import store from './store.js'; class AdminManager { constructor() { this.users = []; this.currentEditUser = null; this.uploadSettings = null; this.initialized = false; } async init() { console.log('[Admin] init() called, initialized:', this.initialized); if (this.initialized) { await this.loadUsers(); await this.loadUploadSettings(); return; } // DOM Elements - erst bei init() laden this.adminScreen = $('#admin-screen'); console.log('[Admin] adminScreen found:', !!this.adminScreen); this.usersList = $('#admin-users-list'); this.logoutBtn = $('#admin-logout-btn'); this.newUserBtn = $('#btn-new-user'); // Modal Elements this.userModal = $('#user-modal'); this.userModalTitle = $('#user-modal-title'); this.userForm = $('#user-form'); this.editUserId = $('#edit-user-id'); this.usernameInput = $('#user-username'); this.displayNameInput = $('#user-displayname'); this.emailInput = $('#user-email'); this.passwordInput = $('#user-password'); this.passwordHint = $('#password-hint'); this.roleSelect = $('#user-role'); this.permissionsGroup = $('#permissions-group'); this.permGenehmigung = $('#perm-genehmigung'); this.deleteUserBtn = $('#btn-delete-user'); this.unlockUserBtn = $('#btn-unlock-user'); // Upload Settings Elements this.uploadMaxSizeInput = $('#upload-max-size'); this.saveUploadSettingsBtn = $('#btn-save-upload-settings'); this.uploadCategories = $$('.upload-category'); this.bindEvents(); this.initialized = true; await this.loadUsers(); await this.loadUploadSettings(); } bindEvents() { // Logout this.logoutBtn?.addEventListener('click', () => this.handleLogout()); // New User Button this.newUserBtn?.addEventListener('click', () => this.openNewUserModal()); // User Form Submit this.userForm?.addEventListener('submit', (e) => this.handleUserSubmit(e)); // Role Change - hide permissions for admin this.roleSelect?.addEventListener('change', () => this.togglePermissionsVisibility()); // Delete User Button this.deleteUserBtn?.addEventListener('click', () => this.handleDeleteUser()); // Unlock User Button this.unlockUserBtn?.addEventListener('click', () => this.handleUnlockUser()); // Modal close buttons this.userModal?.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', () => this.closeModal()); }); // Upload Settings - Save Button this.saveUploadSettingsBtn?.addEventListener('click', () => this.saveUploadSettings()); // Upload Settings - Category Toggles this.uploadCategories?.forEach(category => { const checkbox = category.querySelector('input[type="checkbox"]'); checkbox?.addEventListener('change', () => { this.toggleUploadCategory(category, checkbox.checked); }); }); } async loadUsers() { try { this.users = await api.getAdminUsers(); this.renderUsers(); } catch (error) { console.error('Error loading users:', error); this.showToast('Fehler beim Laden der Benutzer', 'error'); } } renderUsers() { if (!this.usersList) return; if (this.users.length === 0) { this.usersList.innerHTML = `

Keine Benutzer vorhanden

`; return; } this.usersList.innerHTML = this.users.map(user => this.renderUserCard(user)).join(''); // Bind edit buttons this.usersList.querySelectorAll('.admin-user-card').forEach(card => { const userId = parseInt(card.dataset.userId); const editBtn = card.querySelector('.btn-edit-user'); editBtn?.addEventListener('click', () => this.openEditUserModal(userId)); }); } renderUserCard(user) { const initial = (user.display_name || user.username).charAt(0).toUpperCase(); const isLocked = user.locked_until && new Date(user.locked_until) > new Date(); const permissions = user.permissions || []; return `
${initial}
${this.escapeHtml(user.display_name)}
@${this.escapeHtml(user.username)}${user.email ? ` · ${this.escapeHtml(user.email)}` : ''}
${user.role === 'admin' ? 'Admin' : 'Benutzer'} ${permissions.map(p => `${this.escapeHtml(p)}`).join('')}
${isLocked ? 'Gesperrt' : 'Aktiv'}
`; } generateRandomPassword(length = 10) { const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; const lower = 'abcdefghjkmnpqrstuvwxyz'; const numbers = '23456789'; const special = '!@#$%&*?'; // Mindestens ein Zeichen aus jeder Kategorie let password = ''; password += upper.charAt(Math.floor(Math.random() * upper.length)); password += lower.charAt(Math.floor(Math.random() * lower.length)); password += numbers.charAt(Math.floor(Math.random() * numbers.length)); password += special.charAt(Math.floor(Math.random() * special.length)); // Rest mit gemischten Zeichen auffüllen const allChars = upper + lower + numbers + special; for (let i = password.length; i < length; i++) { password += allChars.charAt(Math.floor(Math.random() * allChars.length)); } // Passwort mischen return password.split('').sort(() => Math.random() - 0.5).join(''); } openNewUserModal() { this.currentEditUser = null; this.userModalTitle.textContent = 'Neuer Benutzer'; this.userForm.reset(); this.editUserId.value = ''; // Passwort-Feld anzeigen (falls bei Bearbeitung versteckt) this.passwordInput.closest('.form-group').style.display = ''; // Zufälliges Passwort generieren und anzeigen (10 Zeichen mit Sonderzeichen) const randomPassword = this.generateRandomPassword(10); this.passwordInput.value = randomPassword; this.passwordInput.readOnly = true; this.passwordInput.type = 'text'; this.passwordHint.textContent = '(automatisch generiert)'; this.usernameInput.disabled = false; this.emailInput.disabled = false; this.deleteUserBtn.classList.add('hidden'); this.unlockUserBtn.classList.add('hidden'); this.togglePermissionsVisibility(); this.openModal(); } openEditUserModal(userId) { const user = this.users.find(u => u.id === userId); if (!user) return; this.currentEditUser = user; this.userModalTitle.textContent = 'Benutzer bearbeiten'; this.editUserId.value = user.id; this.usernameInput.value = user.username; this.usernameInput.disabled = true; // Username cannot be changed this.displayNameInput.value = user.display_name; this.emailInput.value = user.email || ''; this.emailInput.disabled = false; // Passwort-Feld bei Bearbeitung ausblenden this.passwordInput.closest('.form-group').style.display = 'none'; this.roleSelect.value = user.role || 'user'; // Set permissions const permissions = user.permissions || []; this.permGenehmigung.checked = permissions.includes('genehmigung'); // Show/hide delete button (cannot delete self or last admin) const canDelete = user.id !== authManager.getUser()?.id; this.deleteUserBtn.classList.toggle('hidden', !canDelete); // Show unlock button if user is locked const isLocked = user.locked_until && new Date(user.locked_until) > new Date(); this.unlockUserBtn.classList.toggle('hidden', !isLocked); this.togglePermissionsVisibility(); this.openModal(); } togglePermissionsVisibility() { const isAdmin = this.roleSelect.value === 'admin'; this.permissionsGroup.style.display = isAdmin ? 'none' : 'block'; } async handleUserSubmit(e) { e.preventDefault(); const userId = this.editUserId.value; const isEdit = !!userId; const data = { displayName: this.displayNameInput.value.trim(), email: this.emailInput.value.trim(), role: this.roleSelect.value, permissions: this.roleSelect.value === 'admin' ? [] : this.getSelectedPermissions() }; if (!isEdit) { data.username = this.usernameInput.value.trim().toUpperCase(); } if (this.passwordInput.value) { data.password = this.passwordInput.value; } try { if (isEdit) { await api.updateAdminUser(userId, data); this.showToast('Benutzer aktualisiert', 'success'); } else { await api.createAdminUser(data); this.showToast('Benutzer erstellt', 'success'); } this.closeModal(); await this.loadUsers(); } catch (error) { this.showToast(error.message || 'Fehler beim Speichern', 'error'); } } async handleDeleteUser() { if (!this.currentEditUser) return; const confirmDelete = confirm(`Benutzer "${this.currentEditUser.display_name}" wirklich löschen?`); if (!confirmDelete) return; try { await api.deleteAdminUser(this.currentEditUser.id); this.showToast('Benutzer gelöscht', 'success'); this.closeModal(); await this.loadUsers(); } catch (error) { this.showToast(error.message || 'Fehler beim Löschen', 'error'); } } async handleUnlockUser() { if (!this.currentEditUser) return; try { await api.updateAdminUser(this.currentEditUser.id, { unlockAccount: true }); this.showToast('Benutzer entsperrt', 'success'); this.closeModal(); await this.loadUsers(); } catch (error) { this.showToast(error.message || 'Fehler beim Entsperren', 'error'); } } getSelectedPermissions() { const permissions = []; if (this.permGenehmigung?.checked) { permissions.push('genehmigung'); } return permissions; } async handleLogout() { try { await authManager.logout(); window.location.reload(); } catch (error) { console.error('Logout error:', error); } } openModal() { if (this.userModal) { this.userModal.classList.remove('hidden'); this.userModal.classList.add('visible'); } const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('hidden'); overlay.classList.add('visible'); } store.openModal('user-modal'); } closeModal() { if (this.userModal) { this.userModal.classList.remove('visible'); this.userModal.classList.add('hidden'); } // Only hide overlay if no other modals are open const openModals = store.get('openModals').filter(id => id !== 'user-modal'); if (openModals.length === 0) { const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('visible'); overlay.classList.add('hidden'); } } store.closeModal('user-modal'); } showToast(message, type = 'info') { window.dispatchEvent(new CustomEvent('toast:show', { detail: { message, type } })); } escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // ===================== // UPLOAD SETTINGS // ===================== async loadUploadSettings() { try { this.uploadSettings = await api.getUploadSettings(); this.renderUploadSettings(); } catch (error) { console.error('Error loading upload settings:', error); } } renderUploadSettings() { if (!this.uploadSettings) return; // Maximale Dateigröße setzen if (this.uploadMaxSizeInput) { this.uploadMaxSizeInput.value = this.uploadSettings.maxFileSizeMB || 15; } // Kategorien setzen const categoryMap = { 'images': 'upload-cat-images', 'documents': 'upload-cat-documents', 'office': 'upload-cat-office', 'text': 'upload-cat-text', 'archives': 'upload-cat-archives', 'data': 'upload-cat-data' }; Object.entries(categoryMap).forEach(([category, checkboxId]) => { const checkbox = $(`#${checkboxId}`); const categoryEl = $(`.upload-category[data-category="${category}"]`); if (checkbox && this.uploadSettings.allowedTypes?.[category]) { const isEnabled = this.uploadSettings.allowedTypes[category].enabled; checkbox.checked = isEnabled; this.toggleUploadCategory(categoryEl, isEnabled); } }); } toggleUploadCategory(categoryEl, enabled) { if (!categoryEl) return; if (enabled) { categoryEl.classList.remove('disabled'); } else { categoryEl.classList.add('disabled'); } } async saveUploadSettings() { try { const maxFileSizeMB = parseInt(this.uploadMaxSizeInput?.value) || 15; // Validierung if (maxFileSizeMB < 1 || maxFileSizeMB > 100) { this.showToast('Dateigröße muss zwischen 1 und 100 MB liegen', 'error'); return; } // Kategorien sammeln const allowedTypes = { images: { enabled: $('#upload-cat-images')?.checked ?? true, types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'] }, documents: { enabled: $('#upload-cat-documents')?.checked ?? true, types: ['application/pdf'] }, office: { enabled: $('#upload-cat-office')?.checked ?? true, types: [ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ] }, text: { enabled: $('#upload-cat-text')?.checked ?? true, types: ['text/plain', 'text/csv', 'text/markdown'] }, archives: { enabled: $('#upload-cat-archives')?.checked ?? true, types: ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed'] }, data: { enabled: $('#upload-cat-data')?.checked ?? true, types: ['application/json'] } }; // Prüfen ob mindestens eine Kategorie aktiviert ist const hasEnabledCategory = Object.values(allowedTypes).some(cat => cat.enabled); if (!hasEnabledCategory) { this.showToast('Mindestens eine Dateikategorie muss aktiviert sein', 'error'); return; } await api.updateUploadSettings({ maxFileSizeMB, allowedTypes }); this.uploadSettings = { maxFileSizeMB, allowedTypes }; this.showToast('Upload-Einstellungen gespeichert', 'success'); } catch (error) { console.error('Error saving upload settings:', error); this.showToast(error.message || 'Fehler beim Speichern', 'error'); } } show() { this.adminScreen?.classList.add('active'); } hide() { this.adminScreen?.classList.remove('active'); } } // Create singleton instance const adminManager = new AdminManager(); export { adminManager }; export default adminManager;