/** * 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.allowedExtensions = ['pdf', 'docx', 'txt']; this.initialized = false; // Vorschläge für häufige Dateiendungen this.extensionSuggestions = [ 'xlsx', 'pptx', 'doc', 'xls', 'ppt', // Office 'png', 'jpg', 'gif', 'svg', 'webp', // Bilder 'csv', 'json', 'xml', 'md', // Daten 'zip', 'rar', '7z', // Archive 'odt', 'ods', 'rtf' // OpenDocument ]; } 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.extensionTagsContainer = $('#extension-tags'); this.extensionInput = $('#extension-input'); this.addExtensionBtn = $('#btn-add-extension'); this.extensionSuggestionsList = $('#extension-suggestions-list'); 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 - Add Extension this.addExtensionBtn?.addEventListener('click', () => this.addExtensionFromInput()); // Enter-Taste im Input-Feld this.extensionInput?.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.addExtensionFromInput(); } }); // Password-Buttons $('#edit-password-btn')?.addEventListener('click', () => this.togglePasswordEdit()); $('#generate-password-btn')?.addEventListener('click', () => this.generatePassword()); } 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 für Bearbeitung vorbereiten this.passwordInput.closest('.form-group').style.display = 'block'; this.passwordInput.value = ''; this.passwordInput.placeholder = 'Neues Passwort (leer lassen = unverändert)'; this.passwordInput.readOnly = true; this.passwordHint.textContent = '(optional - leer lassen für unverändert)'; 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.allowedExtensions = this.uploadSettings.allowedExtensions || ['pdf', 'docx', 'txt']; 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; } // Extension-Tags rendern this.renderExtensionTags(); // Vorschläge rendern this.renderExtensionSuggestions(); } renderExtensionTags() { if (!this.extensionTagsContainer) return; if (this.allowedExtensions.length === 0) { this.extensionTagsContainer.innerHTML = 'Keine Endungen definiert'; return; } this.extensionTagsContainer.innerHTML = this.allowedExtensions.map(ext => ` .${ext} `).join(''); // Remove-Buttons Event Listener this.extensionTagsContainer.querySelectorAll('.extension-tag-remove').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const ext = btn.dataset.remove; this.removeExtension(ext); }); }); } renderExtensionSuggestions() { if (!this.extensionSuggestionsList) return; // Nur Vorschläge anzeigen, die noch nicht aktiv sind const availableSuggestions = this.extensionSuggestions.filter( ext => !this.allowedExtensions.includes(ext) ); if (availableSuggestions.length === 0) { this.extensionSuggestionsList.innerHTML = 'Alle Vorschläge bereits hinzugefügt'; return; } this.extensionSuggestionsList.innerHTML = availableSuggestions.map(ext => ` `).join(''); // Suggestion-Buttons Event Listener this.extensionSuggestionsList.querySelectorAll('.extension-suggestion').forEach(btn => { btn.addEventListener('click', () => { const ext = btn.dataset.suggestion; this.addExtension(ext); }); }); } addExtensionFromInput() { const input = this.extensionInput?.value?.trim().toLowerCase(); if (!input) return; // Punkt am Anfang entfernen falls vorhanden const ext = input.replace(/^\./, ''); if (this.addExtension(ext)) { this.extensionInput.value = ''; } } addExtension(ext) { // Validierung: nur alphanumerisch, 1-10 Zeichen if (!/^[a-z0-9]{1,10}$/.test(ext)) { this.showToast('Ungültige Dateiendung (nur Buchstaben/Zahlen, max. 10 Zeichen)', 'error'); return false; } // Prüfen ob bereits vorhanden if (this.allowedExtensions.includes(ext)) { this.showToast(`Endung .${ext} bereits vorhanden`, 'error'); return false; } // Hinzufügen this.allowedExtensions.push(ext); this.renderExtensionTags(); this.renderExtensionSuggestions(); return true; } removeExtension(ext) { // Prüfen ob mindestens eine Endung übrig bleibt if (this.allowedExtensions.length <= 1) { this.showToast('Mindestens eine Dateiendung muss erlaubt sein', 'error'); return; } this.allowedExtensions = this.allowedExtensions.filter(e => e !== ext); this.renderExtensionTags(); this.renderExtensionSuggestions(); } 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; } if (this.allowedExtensions.length === 0) { this.showToast('Mindestens eine Dateiendung muss erlaubt sein', 'error'); return; } await api.updateUploadSettings({ maxFileSizeMB, allowedExtensions: this.allowedExtensions }); this.uploadSettings = { maxFileSizeMB, allowedExtensions: this.allowedExtensions }; this.showToast('Upload-Einstellungen gespeichert', 'success'); } catch (error) { console.error('Error saving upload settings:', error); this.showToast(error.message || 'Fehler beim Speichern', 'error'); } } /** * Passwort-Bearbeitung umschalten */ togglePasswordEdit() { const passwordInput = $('#user-password'); const editBtn = $('#edit-password-btn'); const hint = $('#password-hint'); if (!passwordInput || !editBtn) return; if (passwordInput.readOnly) { // Bearbeitung aktivieren passwordInput.readOnly = false; passwordInput.focus(); passwordInput.select(); editBtn.innerHTML = ` `; editBtn.title = "Bearbeitung bestätigen"; hint.textContent = "(bearbeiten)"; } else { // Bearbeitung beenden passwordInput.readOnly = true; editBtn.innerHTML = ` `; editBtn.title = "Passwort bearbeiten"; hint.textContent = this.currentEditUser ? "(geändert)" : "(automatisch generiert)"; } } /** * Neues Passwort generieren */ generatePassword() { const passwordInput = $('#user-password'); const hint = $('#password-hint'); if (!passwordInput) return; // Starkes Passwort generieren (12 Zeichen) const charset = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%&*'; let password = ''; for (let i = 0; i < 12; i++) { password += charset.charAt(Math.floor(Math.random() * charset.length)); } passwordInput.value = password; passwordInput.readOnly = false; if (hint) { hint.textContent = "(neu generiert)"; } // Passwort kurz markieren passwordInput.focus(); passwordInput.select(); this.showToast('Neues Passwort generiert', 'success'); } show() { this.adminScreen?.classList.add('active'); } hide() { this.adminScreen?.classList.remove('active'); } } // Create singleton instance const adminManager = new AdminManager(); export { adminManager }; export default adminManager;