import React, { useState, useEffect, useRef, useCallback } from 'react'; import UserList from '../users/UserList'; import ExportButton from './ExportButton'; import Trash from './Trash'; import AuditLogs from './AuditLogs'; import { updateAvailability, updateGPS, createUser, updateUser, deleteUser, uploadUserPhoto, deleteUserPhoto, generateInviteToken } from '../../services/users'; import { getFullConfig, updateConfig, uploadLogo, deleteLogo } from '../../services/config'; import ErrorMessage from '../common/ErrorMessage'; import './AdminPanel.css'; const AdminPanel = ({ users, loading, error, onRefetch }) => { const [notification, setNotification] = useState(null); const [activeTab, setActiveTab] = useState('users'); const [config, setConfig] = useState(null); const [configOriginal, setConfigOriginal] = useState(null); const [configLoading, setConfigLoading] = useState(false); const [configError, setConfigError] = useState(false); const [logoUploading, setLogoUploading] = useState(false); const [collapsed, setCollapsed] = useState({ logo: false, sections: false, rules: false, userTypes: false }); const logoInputRef = useRef(null); const importInputRef = useRef(null); const hasUnsavedChanges = config && configOriginal && JSON.stringify(config) !== JSON.stringify(configOriginal); const loadConfig = useCallback(async () => { setConfigLoading(true); setConfigError(false); try { const response = await getFullConfig(); setConfig(response.data); setConfigOriginal(response.data); } catch (err) { console.error('Config load error:', err); setConfigError(true); } finally { setConfigLoading(false); } }, []); useEffect(() => { if (activeTab === 'settings' && !config) { loadConfig(); } }, [activeTab, config, loadConfig]); const showNotification = (type, message) => { setNotification({ message, type }); setTimeout(() => setNotification(null), 3000); }; const handleAvailabilityToggle = async (id, available) => { const result = await updateAvailability(id, available); if (result.success) { showNotification('success', `Verfügbarkeit erfolgreich aktualisiert`); onRefetch(); } else { showNotification('error', result.message || 'Fehler beim Aktualisieren'); } }; const handleGPSUpdate = async (id, lat, lng) => { const result = await updateGPS(id, lat, lng); if (result.success) { showNotification('success', `GPS-Koordinaten erfolgreich aktualisiert`); onRefetch(); } else { showNotification('error', result.message || 'Fehler beim Aktualisieren'); } }; const handleUserCreate = async (userData) => { const result = await createUser(userData); if (result.success) { if (result.warning) { showNotification('warning', `Erstellt – GPS konnte nicht automatisch ermittelt werden. Bitte manuell setzen.`); } else { showNotification('success', 'Nachsuchenführer erfolgreich erstellt (GPS gesetzt)'); } onRefetch(); } else { showNotification('error', result.message || 'Fehler beim Erstellen'); } }; const handleUserUpdate = async (id, userData) => { const result = await updateUser(id, userData); if (result.success) { if (result.warning) { showNotification('warning', `Gespeichert – GPS konnte nicht automatisch ermittelt werden. Bitte manuell setzen.`); } else { showNotification('success', 'Nachsuchenführer erfolgreich aktualisiert'); } onRefetch(); } else { showNotification('error', result.message || 'Fehler beim Aktualisieren'); } }; const handleUserDelete = async (id) => { const result = await deleteUser(id); if (result.success) { showNotification('success', 'Nachsuchenführer erfolgreich gelöscht'); onRefetch(); } else { showNotification('error', result.message || 'Fehler beim Löschen'); } }; const handleUserPhotoUpload = async (id, dataUrl) => { const result = await uploadUserPhoto(id, dataUrl); if (result.success) { showNotification('success', 'Foto erfolgreich hochgeladen'); onRefetch(); } else { showNotification('error', result.message || 'Fehler beim Foto-Upload'); } }; const handleUserPhotoDelete = async (id) => { const result = await deleteUserPhoto(id); if (result.success) { showNotification('success', 'Foto entfernt'); onRefetch(); } else { showNotification('error', result.message || 'Fehler beim Löschen des Fotos'); } }; const handleGenerateInvite = async (id) => { const result = await generateInviteToken(id); if (result.success) { showNotification('success', 'Einladungs-Token erzeugt (7 Tage gueltig)'); } else { showNotification('error', result.message || 'Fehler beim Erzeugen des Einladungs-Tokens'); } return result; }; const handleConfigUpdate = async (updatedConfig) => { try { const response = await updateConfig(updatedConfig); setConfig(response.data); setConfigOriginal(response.data); showNotification('success', 'Konfiguration erfolgreich gespeichert'); } catch (err) { showNotification('error', err.message || 'Fehler beim Aktualisieren der Konfiguration'); } }; const handleConfigExport = () => { const exportData = { exportedAt: new Date().toISOString(), userTypes: config.userTypes.map(({ code, label }) => ({ code, label })), rules: config.rules, sections: config.sections ? config.sections.map(({ key, title, content }) => ({ key, title, content })) : [] }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `nachsuche-einstellungen-${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url); showNotification('success', 'Einstellungen exportiert'); }; const handleConfigImport = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const imported = JSON.parse(ev.target.result); if (!imported.userTypes || !imported.rules) { showNotification('error', 'Ungültiges Export-Format'); return; } setConfig(prev => ({ ...prev, userTypes: imported.userTypes, rules: imported.rules, sections: imported.sections || prev.sections || [] })); showNotification('success', 'Einstellungen importiert – bitte prüfen und speichern'); } catch { showNotification('error', 'Fehler beim Lesen der Datei'); } }; reader.readAsText(file); e.target.value = ''; }; const toggleSection = (key) => setCollapsed(prev => ({ ...prev, [key]: !prev[key] })); const handleLogoUpload = async (e) => { const file = e.target.files[0]; if (!file) return; if (!file.type.startsWith('image/')) { showNotification('error', 'Nur Bilddateien erlaubt (JPG, PNG, WebP)'); return; } if (file.size > 500 * 1024) { showNotification('error', 'Logo darf max. 500 KB groß sein'); return; } setLogoUploading(true); const reader = new FileReader(); reader.onload = async (ev) => { try { await uploadLogo(ev.target.result); showNotification('success', 'Logo erfolgreich hochgeladen'); // Reload config to get new logo const response = await getFullConfig(); setConfig(response.data); setConfigOriginal(response.data); window.location.reload(); // refresh ConfigContext logo } catch (err) { const msg = err?.response?.data?.message || err?.message || 'Fehler beim Logo-Upload'; showNotification('error', msg); } finally { setLogoUploading(false); } }; reader.readAsDataURL(file); }; const handleLogoDelete = async () => { if (!window.confirm('Logo wirklich entfernen?')) return; try { await deleteLogo(); showNotification('success', 'Logo entfernt'); window.location.reload(); } catch (err) { showNotification('error', 'Fehler beim Entfernen des Logos'); } }; const updateSection = (key, content) => { const sections = config.sections ? [...config.sections] : []; const idx = sections.findIndex(s => s.key === key); if (idx >= 0) { sections[idx] = { ...sections[idx], content }; } else { sections.push({ key, title: key, content }); } setConfig({ ...config, sections }); }; const getSectionContent = (key) => { if (!config || !config.sections) return ''; const s = config.sections.find(s => s.key === key); return s ? s.content : ''; }; return (
Die Konfiguration konnte nicht geladen werden.
Erlaubt: PNG, JPG oder WebP, max. 500 KB (SVG wird aus Sicherheitsgruenden abgelehnt)