From d14157f7ad5af2f2cc59637d026fff21e7f2b4d0 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 27 Mar 2026 19:51:18 +0100 Subject: [PATCH] feat: Frontend Phase 3.1 - Focus Areas Admin UI - AdminFocusAreasPage: Full CRUD for focus area definitions - Route: /admin/focus-areas - AdminPanel: Link zu Focus Areas (neben Goal Types) - api.js: 7 neue Focus Area Endpoints Features: - Category-grouped display (7 categories) - Inline editing - Active/Inactive toggle - Create form with validation - Show/Hide inactive areas Next: Goal Form Multi-Select --- frontend/src/App.jsx | 2 + frontend/src/pages/AdminFocusAreasPage.jsx | 475 +++++++++++++++++++++ frontend/src/pages/AdminPanel.jsx | 17 + frontend/src/utils/api.js | 9 + 4 files changed, 503 insertions(+) create mode 100644 frontend/src/pages/AdminFocusAreasPage.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index bab73a1..c75f6cf 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -32,6 +32,7 @@ import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage' import AdminTrainingProfiles from './pages/AdminTrainingProfiles' import AdminPromptsPage from './pages/AdminPromptsPage' import AdminGoalTypesPage from './pages/AdminGoalTypesPage' +import AdminFocusAreasPage from './pages/AdminFocusAreasPage' import SubscriptionPage from './pages/SubscriptionPage' import SleepPage from './pages/SleepPage' import RestDaysPage from './pages/RestDaysPage' @@ -192,6 +193,7 @@ function AppShell() { }/> }/> }/> + }/> }/> diff --git a/frontend/src/pages/AdminFocusAreasPage.jsx b/frontend/src/pages/AdminFocusAreasPage.jsx new file mode 100644 index 0000000..0369314 --- /dev/null +++ b/frontend/src/pages/AdminFocusAreasPage.jsx @@ -0,0 +1,475 @@ +import { useState, useEffect } from 'react' +import { Plus, Pencil, Trash2, Save, X, Eye, EyeOff } from 'lucide-react' +import { api } from '../utils/api' + +const CATEGORIES = [ + { value: 'body_composition', label: 'Körperzusammensetzung' }, + { value: 'training', label: 'Training' }, + { value: 'endurance', label: 'Ausdauer' }, + { value: 'coordination', label: 'Koordination' }, + { value: 'mental', label: 'Mental' }, + { value: 'recovery', label: 'Erholung' }, + { value: 'health', label: 'Gesundheit' }, + { value: 'custom', label: 'Eigene' } +] + +export default function AdminFocusAreasPage() { + const [data, setData] = useState({ areas: [], grouped: {}, total: 0 }) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [showInactive, setShowInactive] = useState(false) + const [editingId, setEditingId] = useState(null) + const [creating, setCreating] = useState(false) + const [formData, setFormData] = useState({ + key: '', + name_de: '', + name_en: '', + icon: '', + description: '', + category: 'custom' + }) + + useEffect(() => { + loadData() + }, [showInactive]) + + const loadData = async () => { + try { + setLoading(true) + const result = await api.listFocusAreaDefinitions(showInactive) + setData(result) + setError(null) + } catch (err) { + console.error('Failed to load focus areas:', err) + setError(err.message) + } finally { + setLoading(false) + } + } + + const handleCreate = async () => { + if (!formData.key || !formData.name_de) { + setError('Key und Name (DE) sind erforderlich') + return + } + + try { + await api.createFocusAreaDefinition(formData) + setCreating(false) + setFormData({ + key: '', + name_de: '', + name_en: '', + icon: '', + description: '', + category: 'custom' + }) + await loadData() + } catch (err) { + setError(err.message) + } + } + + const handleUpdate = async (id) => { + try { + const area = data.areas.find(a => a.id === id) + await api.updateFocusAreaDefinition(id, { + name_de: area.name_de, + name_en: area.name_en, + icon: area.icon, + description: area.description, + category: area.category, + is_active: area.is_active + }) + setEditingId(null) + await loadData() + } catch (err) { + setError(err.message) + } + } + + const handleDelete = async (id) => { + if (!confirm('Focus Area wirklich löschen?')) return + + try { + await api.deleteFocusAreaDefinition(id) + await loadData() + } catch (err) { + setError(err.message) + } + } + + const handleToggleActive = async (id) => { + const area = data.areas.find(a => a.id === id) + try { + await api.updateFocusAreaDefinition(id, { + is_active: !area.is_active + }) + await loadData() + } catch (err) { + setError(err.message) + } + } + + const updateField = (id, field, value) => { + setData(prev => ({ + ...prev, + areas: prev.areas.map(a => + a.id === id ? { ...a, [field]: value } : a + ) + })) + } + + if (loading) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+

+ 🎯 Focus Areas ({data.total}) +

+
+ + +
+
+ + {error && ( +
+ {error} +
+ )} + + {/* Create Form */} + {creating && ( +
+

+ Neue Focus Area +

+ +
+
+ + setFormData({ ...formData, key: e.target.value })} + placeholder="explosive_power" + style={{ width: '100%' }} + /> +
+ +
+ + setFormData({ ...formData, name_de: e.target.value })} + placeholder="Explosivkraft" + style={{ width: '100%' }} + /> +
+ +
+ + setFormData({ ...formData, name_en: e.target.value })} + placeholder="Explosive Power" + style={{ width: '100%' }} + /> +
+ +
+ + setFormData({ ...formData, icon: e.target.value })} + placeholder="💥" + style={{ width: '100%' }} + /> +
+ +
+ + +
+ +
+ +