diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 01d5139..98f260f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -31,6 +31,7 @@ import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage' import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage' import AdminTrainingProfiles from './pages/AdminTrainingProfiles' import AdminPromptsPage from './pages/AdminPromptsPage' +import AdminGoalTypesPage from './pages/AdminGoalTypesPage' import SubscriptionPage from './pages/SubscriptionPage' import SleepPage from './pages/SleepPage' import RestDaysPage from './pages/RestDaysPage' @@ -188,6 +189,7 @@ function AppShell() { }/> }/> }/> + }/> }/> diff --git a/frontend/src/pages/AdminGoalTypesPage.jsx b/frontend/src/pages/AdminGoalTypesPage.jsx new file mode 100644 index 0000000..9ff50c8 --- /dev/null +++ b/frontend/src/pages/AdminGoalTypesPage.jsx @@ -0,0 +1,446 @@ +import { useState, useEffect } from 'react' +import { Settings, Plus, Pencil, Trash2, Database } from 'lucide-react' +import { api } from '../utils/api' + +export default function AdminGoalTypesPage() { + const [goalTypes, setGoalTypes] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [showForm, setShowForm] = useState(false) + const [editingType, setEditingType] = useState(null) + const [toast, setToast] = useState(null) + + const [formData, setFormData] = useState({ + type_key: '', + label_de: '', + unit: '', + icon: '', + category: 'custom', + source_table: '', + source_column: '', + aggregation_method: 'latest', + description: '' + }) + + const CATEGORIES = ['body', 'mind', 'activity', 'nutrition', 'recovery', 'custom'] + const AGGREGATION_METHODS = [ + { value: 'latest', label: 'Letzter Wert' }, + { value: 'avg_7d', label: 'Durchschnitt 7 Tage' }, + { value: 'avg_30d', label: 'Durchschnitt 30 Tage' }, + { value: 'sum_30d', label: 'Summe 30 Tage' }, + { value: 'count_7d', label: 'Anzahl 7 Tage' }, + { value: 'count_30d', label: 'Anzahl 30 Tage' }, + { value: 'min_30d', label: 'Minimum 30 Tage' }, + { value: 'max_30d', label: 'Maximum 30 Tage' } + ] + + useEffect(() => { + loadGoalTypes() + }, []) + + const loadGoalTypes = async () => { + setLoading(true) + try { + const data = await api.listGoalTypeDefinitions() + setGoalTypes(data) + } catch (err) { + setError('Fehler beim Laden der Goal Types') + } finally { + setLoading(false) + } + } + + const showToast = (message) => { + setToast(message) + setTimeout(() => setToast(null), 2000) + } + + const handleCreate = () => { + setEditingType(null) + setFormData({ + type_key: '', + label_de: '', + unit: '', + icon: '', + category: 'custom', + source_table: '', + source_column: '', + aggregation_method: 'latest', + description: '' + }) + setShowForm(true) + } + + const handleEdit = (type) => { + setEditingType(type.id) + setFormData({ + type_key: type.type_key, + label_de: type.label_de, + unit: type.unit, + icon: type.icon || '', + category: type.category || 'custom', + source_table: type.source_table || '', + source_column: type.source_column || '', + aggregation_method: type.aggregation_method || 'latest', + description: type.description || '' + }) + setShowForm(true) + } + + const handleSave = async () => { + if (!formData.label_de || !formData.unit) { + setError('Bitte Label und Einheit ausfüllen') + return + } + + try { + if (editingType) { + await api.updateGoalType(editingType, formData) + showToast('✓ Goal Type aktualisiert') + } else { + if (!formData.type_key) { + setError('Bitte eindeutigen Key angeben (z.B. meditation_minutes)') + return + } + await api.createGoalType(formData) + showToast('✓ Goal Type erstellt') + } + + await loadGoalTypes() + setShowForm(false) + setError(null) + } catch (err) { + setError(err.message || 'Fehler beim Speichern') + } + } + + const handleDelete = async (typeId, typeName, isSystem) => { + if (isSystem) { + if (!confirm(`System Goal Type "${typeName}" deaktivieren? (Nicht löschbar)`)) return + } else { + if (!confirm(`Goal Type "${typeName}" wirklich löschen?`)) return + } + + try { + await api.deleteGoalType(typeId) + showToast('✓ Goal Type gelöscht/deaktiviert') + await loadGoalTypes() + } catch (err) { + setError(err.message || 'Fehler beim Löschen') + } + } + + if (loading) { + return ( +
+
+
+
+
+ ) + } + + return ( +
+
+

Goal Type Verwaltung

+
+ + {error && ( +
+

{error}

+
+ )} + + {toast && ( +
+ {toast} +
+ )} + +
+
+
+

Verfügbare Goal Types

+

+ {goalTypes.length} Types registriert ({goalTypes.filter(t => t.is_system).length} System, {goalTypes.filter(t => !t.is_system).length} Custom) +

+
+ +
+ +
+ {goalTypes.map(type => ( +
+
+
+
+ {type.icon || '📊'} + {type.label_de} + + {type.unit} + + {type.is_system && ( + + SYSTEM + + )} + {!type.is_active && ( + + INAKTIV + + )} +
+ +
+ Key: {type.type_key} + {type.source_table && ( + <> + {' | '}Quelle: {type.source_table}.{type.source_column} + {' | '}Methode: {type.aggregation_method} + + )} + {type.calculation_formula && ( + <> + {' | '}Formel: Komplex (JSON) + + )} +
+ + {type.description && ( +
+ {type.description} +
+ )} +
+ +
+ + +
+
+
+ ))} +
+
+ + {/* Form Modal */} + {showForm && ( +
+
+
+ {editingType ? 'Goal Type bearbeiten' : 'Neuer Goal Type'} +
+ +
+ {/* Type Key (nur bei Create) */} + {!editingType && ( +
+ + setFormData(f => ({ ...f, type_key: e.target.value }))} + placeholder="snake_case verwenden" + /> +
+ )} + + {/* Label */} +
+ + setFormData(f => ({ ...f, label_de: e.target.value }))} + placeholder="z.B. Meditation" + /> +
+ + {/* Unit & Icon */} +
+
+ + setFormData(f => ({ ...f, unit: e.target.value }))} + placeholder="z.B. min/Tag" + /> +
+
+ + setFormData(f => ({ ...f, icon: e.target.value }))} + placeholder="🧘" + /> +
+
+ + {/* Category */} +
+ + +
+ + {/* Data Source */} +
+
+ + setFormData(f => ({ ...f, source_table: e.target.value }))} + placeholder="z.B. meditation_log" + /> +
+
+ + setFormData(f => ({ ...f, source_column: e.target.value }))} + placeholder="z.B. duration_minutes" + /> +
+
+ + {/* Aggregation Method */} +
+ + +
+ + {/* Description */} +
+ +