import React, { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { adminApi, authApi } from '../api/client' import { useAuthStore } from '../store/authStore' import { useSettingsStore } from '../store/settingsStore' import { useTranslation } from '../i18n' import Navbar from '../components/Layout/Navbar' import Modal from '../components/shared/Modal' import { useToast } from '../components/shared/Toast' import CategoryManager from '../components/Admin/CategoryManager' import BackupPanel from '../components/Admin/BackupPanel' import AddonManager from '../components/Admin/AddonManager' import { Users, Map, Briefcase, Shield, Trash2, Edit2, Camera, FileText, Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, UserPlus } from 'lucide-react' import CustomSelect from '../components/shared/CustomSelect' export default function AdminPage() { const { demoMode } = useAuthStore() const { t, locale } = useTranslation() const hour12 = useSettingsStore(s => s.settings.time_format) === '12h' const TABS = [ { id: 'users', label: t('admin.tabs.users') }, { id: 'categories', label: t('admin.tabs.categories') }, { id: 'addons', label: t('admin.tabs.addons') }, { id: 'settings', label: t('admin.tabs.settings') }, { id: 'backup', label: t('admin.tabs.backup') }, ] const [activeTab, setActiveTab] = useState('users') const [users, setUsers] = useState([]) const [stats, setStats] = useState(null) const [isLoading, setIsLoading] = useState(true) const [editingUser, setEditingUser] = useState(null) const [editForm, setEditForm] = useState({ username: '', email: '', role: 'user', password: '' }) const [showCreateUser, setShowCreateUser] = useState(false) const [createForm, setCreateForm] = useState({ username: '', email: '', password: '', role: 'user' }) // OIDC config const [oidcConfig, setOidcConfig] = useState({ issuer: '', client_id: '', client_secret: '', display_name: '' }) const [savingOidc, setSavingOidc] = useState(false) // Registration toggle const [allowRegistration, setAllowRegistration] = useState(true) // API Keys const [mapsKey, setMapsKey] = useState('') const [weatherKey, setWeatherKey] = useState('') const [showKeys, setShowKeys] = useState({}) const [savingKeys, setSavingKeys] = useState(false) const [validating, setValidating] = useState({}) const [validation, setValidation] = useState({}) const { user: currentUser, updateApiKeys } = useAuthStore() const navigate = useNavigate() const toast = useToast() useEffect(() => { loadData() loadAppConfig() loadApiKeys() adminApi.getOidc().then(setOidcConfig).catch(() => {}) }, []) const loadData = async () => { setIsLoading(true) try { const [usersData, statsData] = await Promise.all([ adminApi.users(), adminApi.stats(), ]) setUsers(usersData.users) setStats(statsData) } catch (err) { toast.error(t('admin.toast.loadError')) } finally { setIsLoading(false) } } const loadAppConfig = async () => { try { const config = await authApi.getAppConfig() setAllowRegistration(config.allow_registration) } catch (err) { // ignore } } const loadApiKeys = async () => { try { const data = await authApi.getSettings() setMapsKey(data.settings?.maps_api_key || '') setWeatherKey(data.settings?.openweather_api_key || '') } catch (err) { // ignore } } const handleToggleRegistration = async (value) => { setAllowRegistration(value) try { await authApi.updateAppSettings({ allow_registration: value }) } catch (err) { setAllowRegistration(!value) toast.error(err.response?.data?.error || t('common.error')) } } const toggleKey = (key) => { setShowKeys(prev => ({ ...prev, [key]: !prev[key] })) } const handleSaveApiKeys = async () => { setSavingKeys(true) try { await updateApiKeys({ maps_api_key: mapsKey, openweather_api_key: weatherKey, }) toast.success(t('admin.keySaved')) } catch (err) { toast.error(err.message) } finally { setSavingKeys(false) } } const handleValidateKeys = async () => { setValidating({ maps: true, weather: true }) try { // Save first so validation uses the current values await updateApiKeys({ maps_api_key: mapsKey, openweather_api_key: weatherKey }) const result = await authApi.validateKeys() setValidation(result) } catch (err) { toast.error(t('common.error')) } finally { setValidating({}) } } const handleValidateKey = async (keyType) => { setValidating(prev => ({ ...prev, [keyType]: true })) try { // Save first so validation uses the current values await updateApiKeys({ maps_api_key: mapsKey, openweather_api_key: weatherKey }) const result = await authApi.validateKeys() setValidation(prev => ({ ...prev, [keyType]: result[keyType] })) } catch (err) { toast.error(t('common.error')) } finally { setValidating(prev => ({ ...prev, [keyType]: false })) } } const handleCreateUser = async () => { if (!createForm.username.trim() || !createForm.email.trim() || !createForm.password.trim()) { toast.error(t('admin.toast.fieldsRequired')) return } try { const data = await adminApi.createUser(createForm) setUsers(prev => [data.user, ...prev]) setShowCreateUser(false) setCreateForm({ username: '', email: '', password: '', role: 'user' }) toast.success(t('admin.toast.userCreated')) } catch (err) { toast.error(err.response?.data?.error || t('admin.toast.createError')) } } const handleEditUser = (user) => { setEditingUser(user) setEditForm({ username: user.username, email: user.email, role: user.role, password: '' }) } const handleSaveUser = async () => { try { const payload = { username: editForm.username.trim() || undefined, email: editForm.email.trim() || undefined, role: editForm.role, } if (editForm.password.trim()) payload.password = editForm.password.trim() const data = await adminApi.updateUser(editingUser.id, payload) setUsers(prev => prev.map(u => u.id === editingUser.id ? data.user : u)) setEditingUser(null) toast.success(t('admin.toast.userUpdated')) } catch (err) { toast.error(err.response?.data?.error || t('admin.toast.updateError')) } } const handleDeleteUser = async (user) => { if (user.id === currentUser?.id) { toast.error(t('admin.toast.cannotDeleteSelf')) return } if (!confirm(t('admin.deleteUser', { name: user.username }))) return try { await adminApi.deleteUser(user.id) setUsers(prev => prev.filter(u => u.id !== user.id)) toast.success(t('admin.toast.userDeleted')) } catch (err) { toast.error(err.response?.data?.error || t('admin.toast.deleteError')) } } return (
{/* Header */}

Administration

{t('admin.subtitle')}

{/* Demo Baseline Button */} {demoMode && (

Demo Baseline

Save current state as the hourly reset point. All admin trips and settings will be preserved.

)} {/* Stats */} {stats && (
{[ { label: t('admin.stats.users'), value: stats.totalUsers, icon: Users }, { label: t('admin.stats.trips'), value: stats.totalTrips, icon: Briefcase }, { label: t('admin.stats.places'), value: stats.totalPlaces, icon: Map }, { label: t('admin.stats.files'), value: stats.totalFiles || 0, icon: FileText }, ].map(({ label, value, icon: Icon }) => (

{value}

{label}

))}
)} {/* Tabs */}
{TABS.map(tab => ( ))}
{/* Tab content */} {activeTab === 'users' && (

{t('admin.tabs.users')}

{users.length} {t('admin.stats.users')}

{isLoading ? (
) : (
{users.map(u => ( ))}
{t('admin.table.user')} {t('admin.table.email')} {t('admin.table.role')} {t('admin.table.created')} {t('admin.table.lastLogin')} {t('admin.table.actions')}
{u.username.charAt(0).toUpperCase()}

{u.username}

{u.id === currentUser?.id && ( {t('admin.you')} )}
{u.email} {u.role === 'admin' && } {u.role === 'admin' ? t('settings.roleAdmin') : t('settings.roleUser')} {new Date(u.created_at).toLocaleDateString(locale)} {u.last_login ? new Date(u.last_login).toLocaleDateString(locale, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', hour12 }) : '—'}
)}
)} {activeTab === 'categories' && } {activeTab === 'addons' && } {activeTab === 'settings' && (
{/* Registration Toggle */}

{t('admin.allowRegistration')}

{t('admin.allowRegistration')}

{t('admin.allowRegistrationHint')}

{/* API Keys */}

{t('admin.apiKeys')}

{t('admin.apiKeysHint')}

{/* Google Maps Key */}
setMapsKey(e.target.value)} placeholder={t('settings.keyPlaceholder')} className="w-full pr-10 px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />

{t('admin.mapsKeyHintLong')}

{validation.maps === true && (

{t('admin.keyValid')}

)} {validation.maps === false && (

{t('admin.keyInvalid')}

)}
{/* OpenWeatherMap Key */}
setWeatherKey(e.target.value)} placeholder={t('settings.keyPlaceholder')} className="w-full pr-10 px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />

{t('admin.weatherKeyHint')}

{validation.weather === true && (

{t('admin.keyValid')}

)} {validation.weather === false && (

{t('admin.keyInvalid')}

)}
{/* OIDC / SSO Configuration */}

{t('admin.oidcTitle')}

{t('admin.oidcSubtitle')}

setOidcConfig(c => ({ ...c, display_name: e.target.value }))} placeholder='z.B. Google, Authentik, Keycloak' className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
setOidcConfig(c => ({ ...c, issuer: e.target.value }))} placeholder='https://accounts.google.com' className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />

{t('admin.oidcIssuerHint')}

setOidcConfig(c => ({ ...c, client_id: e.target.value }))} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
setOidcConfig(c => ({ ...c, client_secret: e.target.value }))} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
)} {activeTab === 'backup' && }
{/* Create user modal */} setShowCreateUser(false)} title={t('admin.createUser')} size="sm" footer={
} >
setCreateForm(f => ({ ...f, username: e.target.value }))} placeholder={t('settings.username')} className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-slate-400 focus:border-transparent text-sm" />
setCreateForm(f => ({ ...f, email: e.target.value }))} placeholder={t('common.email')} className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-slate-400 focus:border-transparent text-sm" />
setCreateForm(f => ({ ...f, password: e.target.value }))} placeholder={t('common.password')} className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-slate-400 focus:border-transparent text-sm" />
setCreateForm(f => ({ ...f, role: value }))} options={[ { value: 'user', label: t('settings.roleUser') }, { value: 'admin', label: t('settings.roleAdmin') }, ]} />
{/* Edit user modal */} setEditingUser(null)} title={t('admin.editUser')} size="sm" footer={
} > {editingUser && (
setEditForm(f => ({ ...f, username: e.target.value }))} className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-slate-400 focus:border-transparent text-sm" />
setEditForm(f => ({ ...f, email: e.target.value }))} className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-slate-400 focus:border-transparent text-sm" />
setEditForm(f => ({ ...f, password: e.target.value }))} placeholder={t('admin.newPasswordPlaceholder')} className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-slate-900 focus:ring-2 focus:ring-slate-400 focus:border-transparent text-sm" />
setEditForm(f => ({ ...f, role: value }))} options={[ { value: 'user', label: t('settings.roleUser') }, { value: 'admin', label: t('settings.roleAdmin') }, ]} />
)}
) }