Dieser Commit ist enthalten in:
Claude Project Manager
2025-12-28 21:36:45 +00:00
Commit ab1e5be9a9
146 geänderte Dateien mit 65525 neuen und 0 gelöschten Zeilen

505
frontend/js/admin.js Normale Datei
Datei anzeigen

@ -0,0 +1,505 @@
/**
* 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 = `
<div class="admin-empty-state">
<svg viewBox="0 0 24 24"><path d="M12 2a5 5 0 0 1 5 5v2a5 5 0 0 1-10 0V7a5 5 0 0 1 5-5zm-7 18a7 7 0 0 1 14 0" stroke="currentColor" stroke-width="2" fill="none"/></svg>
<p>Keine Benutzer vorhanden</p>
</div>
`;
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 `
<div class="admin-user-card" data-user-id="${user.id}">
<div class="admin-user-avatar" style="background-color: ${user.color || '#808080'}">
${initial}
</div>
<div class="admin-user-info">
<div class="admin-user-name">${this.escapeHtml(user.display_name)}</div>
<div class="admin-user-username">@${this.escapeHtml(user.username)}${user.email ? ` · ${this.escapeHtml(user.email)}` : ''}</div>
</div>
<div class="admin-user-badges">
<span class="admin-badge role-${user.role || 'user'}">
${user.role === 'admin' ? 'Admin' : 'Benutzer'}
</span>
${permissions.map(p => `<span class="admin-badge permission">${this.escapeHtml(p)}</span>`).join('')}
</div>
<div class="admin-user-status">
${isLocked ? '<span class="status-locked">Gesperrt</span>' : '<span class="status-active">Aktiv</span>'}
</div>
<div class="admin-user-actions">
<button class="btn-edit-user" title="Bearbeiten">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" fill="none"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
</div>
</div>
`;
}
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;