From bcb867da694f23019ab4449b7215ca03214a5c8d Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 27 Mar 2026 15:32:15 +0100 Subject: [PATCH] refactor: Separate goal tracking - strategic vs tactical MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **UX Improvements:** - Progress modal: full-width inputs, label-as-heading, left-aligned text - Progress button only visible for custom goals (no source_table) - Prevents confusion with automatic tracking (Weight, Activity, etc.) **New Page: Custom Goals (Capture/Eigene Ziele):** - Dedicated page for daily custom goal value entry - Clean goal selection with progress bars - Quick entry form (date, value, note) - Recent progress history (last 5 entries) - Mobile-optimized for daily use **Architecture:** - Analysis/Goals → Strategic (define goals, set priorities) - Capture/Custom Goals → Tactical (daily value entry) - History → Evaluation (goal achievement analysis) Co-Authored-By: Claude Opus 4.6 --- frontend/src/App.jsx | 2 + frontend/src/pages/CaptureHub.jsx | 7 + frontend/src/pages/CustomGoalsPage.jsx | 370 +++++++++++++++++++++++++ frontend/src/pages/GoalsPage.jsx | 54 +++- 4 files changed, 420 insertions(+), 13 deletions(-) create mode 100644 frontend/src/pages/CustomGoalsPage.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 98f260f..bab73a1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -37,6 +37,7 @@ import SleepPage from './pages/SleepPage' import RestDaysPage from './pages/RestDaysPage' import VitalsPage from './pages/VitalsPage' import GoalsPage from './pages/GoalsPage' +import CustomGoalsPage from './pages/CustomGoalsPage' import './app.css' function Nav() { @@ -175,6 +176,7 @@ function AppShell() { }/> }/> }/> + }/> }/> }/> }/> diff --git a/frontend/src/pages/CaptureHub.jsx b/frontend/src/pages/CaptureHub.jsx index 40cadc1..5da1fcb 100644 --- a/frontend/src/pages/CaptureHub.jsx +++ b/frontend/src/pages/CaptureHub.jsx @@ -66,6 +66,13 @@ const ENTRIES = [ to: '/vitals', color: '#E74C3C', }, + { + icon: '🎯', + label: 'Eigene Ziele', + sub: 'Fortschritte für individuelle Ziele erfassen', + to: '/custom-goals', + color: '#1D9E75', + }, { icon: '📖', label: 'Messanleitung', diff --git a/frontend/src/pages/CustomGoalsPage.jsx b/frontend/src/pages/CustomGoalsPage.jsx new file mode 100644 index 0000000..5fdcf7d --- /dev/null +++ b/frontend/src/pages/CustomGoalsPage.jsx @@ -0,0 +1,370 @@ +import { useState, useEffect } from 'react' +import { api } from '../utils/api' +import { Target, TrendingUp, Calendar, CheckCircle2, AlertCircle } from 'lucide-react' +import dayjs from 'dayjs' + +export default function CustomGoalsPage() { + const [customGoals, setCustomGoals] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [selectedGoal, setSelectedGoal] = useState(null) + const [formData, setFormData] = useState({ + date: new Date().toISOString().split('T')[0], + value: '', + note: '' + }) + const [recentProgress, setRecentProgress] = useState([]) + + useEffect(() => { + loadCustomGoals() + }, []) + + const loadCustomGoals = async () => { + try { + setLoading(true) + const grouped = await api.listGoalsGrouped() + + // Extract all goals and filter for custom only (no source_table) + const allGoals = Object.values(grouped).flat() + const custom = allGoals.filter(g => !g.source_table) + + setCustomGoals(custom) + setError(null) + } catch (err) { + console.error('Failed to load custom goals:', err) + setError(err.message || 'Fehler beim Laden') + } finally { + setLoading(false) + } + } + + const loadRecentProgress = async (goalId) => { + try { + const entries = await api.listGoalProgress(goalId) + setRecentProgress(entries.slice(0, 5)) // Last 5 entries + } catch (err) { + console.error('Failed to load progress:', err) + setRecentProgress([]) + } + } + + const handleSelectGoal = async (goal) => { + setSelectedGoal(goal) + setFormData({ + date: new Date().toISOString().split('T')[0], + value: goal.current_value || '', + note: '' + }) + await loadRecentProgress(goal.id) + } + + const handleSaveProgress = async () => { + if (!formData.value || !formData.date) { + setError('Bitte Datum und Wert eingeben') + return + } + + try { + const data = { + date: formData.date, + value: parseFloat(formData.value), + note: formData.note || null + } + + await api.createGoalProgress(selectedGoal.id, data) + + // Reset form and reload + setFormData({ + date: new Date().toISOString().split('T')[0], + value: '', + note: '' + }) + + await loadCustomGoals() + await loadRecentProgress(selectedGoal.id) + + // Update selected goal with new current_value + const updated = customGoals.find(g => g.id === selectedGoal.id) + if (updated) setSelectedGoal(updated) + + setError(null) + } catch (err) { + console.error('Failed to save progress:', err) + setError(err.message || 'Fehler beim Speichern') + } + } + + const getProgressPercentage = (goal) => { + if (!goal.current_value || !goal.target_value) return 0 + + const current = parseFloat(goal.current_value) + const target = parseFloat(goal.target_value) + const start = parseFloat(goal.start_value) || 0 + + if (goal.direction === 'decrease') { + return Math.min(100, Math.max(0, ((start - current) / (start - target)) * 100)) + } else { + return Math.min(100, Math.max(0, ((current - start) / (target - start)) * 100)) + } + } + + if (loading) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+ +

Eigene Ziele

+
+
+ Erfasse Fortschritte für deine individuellen Ziele +
+
+ + {error && ( +
+ {error} +
+ )} + + {customGoals.length === 0 ? ( +
+ +
+ Keine eigenen Ziele vorhanden +
+
+ Erstelle eigene Ziele über die Ziele-Seite in der Analyse +
+
+ ) : ( +
+ {/* Goal Selection */} +
+

+ Ziel auswählen ({customGoals.length}) +

+ +
+ {customGoals.map(goal => { + const progress = getProgressPercentage(goal) + const isSelected = selectedGoal?.id === goal.id + + return ( + + ) + })} +
+
+ + {/* Progress Entry Form */} + {selectedGoal && ( +
+

+ + Fortschritt erfassen +

+ +
+
+
+ Datum +
+ setFormData({ ...formData, date: e.target.value })} + max={new Date().toISOString().split('T')[0]} + style={{ width: '100%', textAlign: 'left' }} + /> +
+ +
+
+ Wert ({selectedGoal.unit}) +
+ setFormData({ ...formData, value: e.target.value })} + placeholder={`Aktueller Wert in ${selectedGoal.unit}`} + style={{ width: '100%', textAlign: 'left' }} + /> +
+ +
+
+ Notiz (optional) +
+