mitai-jinkendo/frontend/src/pages/GoalsPage.jsx
Lars bcb867da69
Some checks failed
Build Test / lint-backend (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Deploy Development / deploy (push) Has been cancelled
refactor: Separate goal tracking - strategic vs tactical
**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 <noreply@anthropic.com>
2026-03-27 15:32:15 +01:00

1231 lines
45 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react'
import { Target, Plus, Pencil, Trash2, TrendingUp, Calendar } from 'lucide-react'
import { api } from '../utils/api'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
// Goal Categories
const GOAL_CATEGORIES = {
body: { label: 'Körper', icon: '📉', color: '#D85A30', description: 'Gewicht, Körperfett, Muskelmasse' },
training: { label: 'Training', icon: '🏋️', color: '#378ADD', description: 'Kraft, Frequenz, Performance' },
nutrition: { label: 'Ernährung', icon: '🍎', color: '#1D9E75', description: 'Kalorien, Makros, Essgewohnheiten' },
recovery: { label: 'Erholung', icon: '😴', color: '#7B68EE', description: 'Schlaf, Regeneration, Ruhetage' },
health: { label: 'Gesundheit', icon: '❤️', color: '#E67E22', description: 'Vitalwerte, Blutdruck, HRV' },
other: { label: 'Sonstiges', icon: '📌', color: '#94A3B8', description: 'Weitere Ziele' }
}
// Priority Levels
const PRIORITY_LEVELS = {
1: { label: 'Hoch', stars: '⭐⭐⭐', color: 'var(--accent)' },
2: { label: 'Mittel', stars: '⭐⭐', color: '#94A3B8' },
3: { label: 'Niedrig', stars: '⭐', color: '#CBD5E1' }
}
// Auto-assign category based on goal_type
const getCategoryForGoalType = (goalType) => {
const mapping = {
// Body composition
'weight': 'body',
'body_fat': 'body',
'lean_mass': 'body',
// Training
'strength': 'training',
'flexibility': 'training',
'training_frequency': 'training',
// Health/Vitals
'vo2max': 'health',
'rhr': 'health',
'bp': 'health',
'hrv': 'health',
// Recovery
'sleep_quality': 'recovery',
'sleep_duration': 'recovery',
'rest_days': 'recovery',
// Nutrition
'calories': 'nutrition',
'protein': 'nutrition',
'healthy_eating': 'nutrition'
}
return mapping[goalType] || 'other'
}
export default function GoalsPage() {
const [goalMode, setGoalMode] = useState(null)
const [focusAreas, setFocusAreas] = useState(null)
const [focusEditing, setFocusEditing] = useState(false)
const [focusTemp, setFocusTemp] = useState({
weight_loss_pct: 0,
muscle_gain_pct: 0,
strength_pct: 0,
endurance_pct: 0,
flexibility_pct: 0,
health_pct: 0
})
const [goals, setGoals] = useState([]) // Kept for backward compat
const [groupedGoals, setGroupedGoals] = useState({}) // Category-grouped goals
const [goalTypes, setGoalTypes] = useState([]) // Dynamic from DB (Phase 1.5)
const [goalTypesMap, setGoalTypesMap] = useState({}) // For quick lookup
const [showGoalForm, setShowGoalForm] = useState(false)
const [editingGoal, setEditingGoal] = useState(null)
const [showProgressModal, setShowProgressModal] = useState(false)
const [progressGoal, setProgressGoal] = useState(null)
const [progressEntries, setProgressEntries] = useState([])
const [progressFormData, setProgressFormData] = useState({
date: new Date().toISOString().split('T')[0],
value: '',
note: ''
})
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [toast, setToast] = useState(null)
// Form state
const [formData, setFormData] = useState({
goal_type: 'weight',
is_primary: false,
category: 'body',
priority: 2,
target_value: '',
unit: 'kg',
target_date: '',
name: '',
description: ''
})
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
setLoading(true)
setError(null)
try {
const [modeData, goalsData, groupedData, typesData, focusData] = await Promise.all([
api.getGoalMode(),
api.listGoals(),
api.listGoalsGrouped(), // v2.1: Load grouped by category
api.listGoalTypeDefinitions(), // Phase 1.5: Load from DB
api.getFocusAreas() // v2.0: Load focus areas
])
setGoalMode(modeData.goal_mode)
setGoals(goalsData)
setGroupedGoals(groupedData)
// Ensure all focus fields are present and numeric
const sanitizedFocus = {
weight_loss_pct: focusData?.weight_loss_pct ?? 0,
muscle_gain_pct: focusData?.muscle_gain_pct ?? 0,
strength_pct: focusData?.strength_pct ?? 0,
endurance_pct: focusData?.endurance_pct ?? 0,
flexibility_pct: focusData?.flexibility_pct ?? 0,
health_pct: focusData?.health_pct ?? 0,
custom: focusData?.custom,
updated_at: focusData?.updated_at
}
setFocusAreas(sanitizedFocus)
setFocusTemp(sanitizedFocus)
// Convert types array to map for quick lookup
const typesMap = {}
if (typesData && Array.isArray(typesData)) {
typesData.forEach(type => {
typesMap[type.type_key] = {
label: type.label_de,
unit: type.unit,
icon: type.icon || '📊',
category: type.category,
is_system: type.is_system
}
})
}
setGoalTypes(typesData || [])
setGoalTypesMap(typesMap)
} catch (err) {
console.error('Failed to load goals:', err)
setError(`Fehler beim Laden: ${err.message || err.toString()}`)
} finally {
setLoading(false)
}
}
const showToast = (message, duration = 2000) => {
setToast(message)
setTimeout(() => setToast(null), duration)
}
const handleGoalModeChange = async (newMode) => {
try {
await api.updateGoalMode(newMode)
setGoalMode(newMode)
showToast('✓ Trainingsmodus aktualisiert')
} catch (err) {
console.error('Failed to update goal mode:', err)
setError('Fehler beim Aktualisieren des Trainingsmodus')
}
}
const handleCreateGoal = () => {
if (goalTypes.length === 0) {
setError('Keine Goal Types verfügbar. Bitte Admin kontaktieren.')
return
}
setEditingGoal(null)
const firstType = goalTypes[0].type_key
setFormData({
goal_type: firstType,
is_primary: goals.length === 0, // First goal is primary by default
category: getCategoryForGoalType(firstType), // Auto-assign based on type
priority: 2,
target_value: '',
unit: goalTypesMap[firstType]?.unit || 'kg',
target_date: '',
name: '',
description: ''
})
setShowGoalForm(true)
}
const handleEditGoal = (goal) => {
setEditingGoal(goal.id)
setFormData({
goal_type: goal.goal_type,
is_primary: goal.is_primary,
category: goal.category || 'other',
priority: goal.priority || 2,
target_value: goal.target_value,
unit: goal.unit,
target_date: goal.target_date || '',
name: goal.name || '',
description: goal.description || ''
})
setShowGoalForm(true)
}
const handleGoalTypeChange = (type) => {
setFormData(f => ({
...f,
goal_type: type,
unit: goalTypesMap[type]?.unit || 'unit',
category: getCategoryForGoalType(type) // Auto-assign category
}))
}
const handleSaveGoal = async () => {
if (!formData.target_value) {
setError('Bitte Zielwert eingeben')
return
}
try {
const data = {
goal_type: formData.goal_type,
is_primary: formData.is_primary,
category: formData.category,
priority: formData.priority,
target_value: parseFloat(formData.target_value),
unit: formData.unit,
target_date: formData.target_date || null,
name: formData.name || null,
description: formData.description || null
}
console.log('[DEBUG] Saving goal:', { editingGoal, data })
if (editingGoal) {
await api.updateGoal(editingGoal, data)
showToast('✓ Ziel aktualisiert')
} else {
await api.createGoal(data)
showToast('✓ Ziel erstellt')
}
await loadData()
setShowGoalForm(false)
setEditingGoal(null)
} catch (err) {
console.error('Failed to save goal:', err)
setError(err.message || 'Fehler beim Speichern')
}
}
const handleDeleteGoal = async (goalId) => {
if (!confirm('Ziel wirklich löschen?')) return
try {
await api.deleteGoal(goalId)
showToast('✓ Ziel gelöscht')
await loadData()
} catch (err) {
console.error('Failed to delete goal:', err)
setError('Fehler beim Löschen')
}
}
const handleOpenProgressModal = async (goal) => {
setProgressGoal(goal)
setProgressFormData({
date: new Date().toISOString().split('T')[0],
value: goal.current_value || '',
note: ''
})
// Load progress history
try {
const entries = await api.listGoalProgress(goal.id)
setProgressEntries(entries)
} catch (err) {
console.error('Failed to load progress:', err)
setProgressEntries([])
}
setShowProgressModal(true)
setError(null)
}
const handleSaveProgress = async () => {
if (!progressFormData.value || !progressFormData.date) {
setError('Bitte Datum und Wert eingeben')
return
}
try {
const data = {
date: progressFormData.date,
value: parseFloat(progressFormData.value),
note: progressFormData.note || null
}
await api.createGoalProgress(progressGoal.id, data)
showToast('✓ Fortschritt erfasst')
// Reload progress history
const entries = await api.listGoalProgress(progressGoal.id)
setProgressEntries(entries)
// Reset form
setProgressFormData({
date: new Date().toISOString().split('T')[0],
value: '',
note: ''
})
// Reload goals to update current_value
await loadData()
setError(null)
} catch (err) {
console.error('Failed to save progress:', err)
setError(err.message || 'Fehler beim Speichern')
}
}
const handleDeleteProgress = async (progressId) => {
if (!confirm('Eintrag wirklich löschen?')) return
try {
await api.deleteGoalProgress(progressGoal.id, progressId)
showToast('✓ Eintrag gelöscht')
// Reload progress history
const entries = await api.listGoalProgress(progressGoal.id)
setProgressEntries(entries)
// Reload goals to update current_value
await loadData()
} catch (err) {
console.error('Failed to delete progress:', err)
setError('Fehler beim Löschen')
}
}
const getProgressColor = (progress) => {
if (progress >= 100) return 'var(--accent)'
if (progress >= 75) return '#1D9E75'
if (progress >= 50) return '#378ADD'
if (progress >= 25) return '#E67E22'
return '#D85A30'
}
if (loading) {
return (
<div className="page">
<div style={{ textAlign: 'center', padding: '40px' }}>
<div className="spinner"></div>
</div>
</div>
)
}
return (
<div className="page">
<div className="page-header">
<h1><Target size={24} /> Ziele</h1>
</div>
{error && (
<div className="card" style={{ background: '#FEF2F2', border: '1px solid #FCA5A5', marginBottom: 16 }}>
<p style={{ color: '#DC2626', margin: 0 }}>{error}</p>
</div>
)}
{toast && (
<div style={{
position: 'fixed',
top: 16,
right: 16,
background: 'var(--accent)',
color: 'white',
padding: '12px 20px',
borderRadius: 8,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
zIndex: 1000
}}>
{toast}
</div>
)}
{/* Focus Areas (v2.0) */}
<div className="card" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<h2 style={{ margin: 0 }}>🎯 Fokus-Bereiche</h2>
{!focusEditing && focusAreas && (
<button
className="btn-secondary"
onClick={() => {
setFocusTemp(focusAreas) // Sync temp state before editing
setFocusEditing(true)
}}
style={{ padding: '6px 12px' }}
>
<Pencil size={14} /> Anpassen
</button>
)}
</div>
<p style={{ color: 'var(--text2)', fontSize: 14, marginBottom: 16 }}>
Setze relative Gewichte für deine Trainingsziele. Das System berechnet automatisch die Prozentanteile.
{focusAreas && !focusAreas.custom && (
<span style={{ display: 'block', marginTop: 4, fontStyle: 'italic' }}>
Aktuell abgeleitet aus Trainingsmodus "{goalMode}" - klicke "Anpassen" für individuelle Gewichtung
</span>
)}
</p>
{focusEditing ? (
<>
{/* Sliders */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 20, marginBottom: 20 }}>
{[
{ key: 'weight_loss_pct', label: 'Fettabbau', icon: '📉', color: '#D85A30' },
{ key: 'muscle_gain_pct', label: 'Muskelaufbau', icon: '💪', color: '#378ADD' },
{ key: 'strength_pct', label: 'Kraftsteigerung', icon: '🏋️', color: '#7B68EE' },
{ key: 'endurance_pct', label: 'Ausdauer', icon: '🏃', color: '#1D9E75' },
{ key: 'flexibility_pct', label: 'Beweglichkeit', icon: '🤸', color: '#E67E22' },
{ key: 'health_pct', label: 'Gesundheit', icon: '❤️', color: '#F59E0B' }
].map(area => {
const rawValue = Number(focusTemp[area.key]) || 0
const weight = Math.round(rawValue / 10)
const sum = Object.entries(focusTemp)
.filter(([k]) => k.endsWith('_pct'))
.reduce((acc, [k, v]) => acc + (Number(v) || 0), 0)
const actualPercent = sum > 0 ? Math.round(rawValue / sum * 100) : 0
return (
<div key={area.key}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 20 }}>{area.icon}</span>
<span style={{ fontWeight: 500 }}>{area.label}</span>
</div>
<span style={{
fontSize: 16,
fontWeight: 600,
color: area.color,
minWidth: 80,
textAlign: 'right'
}}>
{weight} {actualPercent}%
</span>
</div>
<input
type="range"
min="0"
max="10"
step="1"
value={weight}
onChange={e => setFocusTemp(f => ({ ...f, [area.key]: parseInt(e.target.value) * 10 }))}
style={{
width: '100%',
height: 8,
borderRadius: 4,
background: `linear-gradient(to right, ${area.color} 0%, ${area.color} ${weight * 10}%, var(--border) ${weight * 10}%, var(--border) 100%)`,
outline: 'none',
cursor: 'pointer'
}}
/>
</div>
)
})}
</div>
{/* Action Buttons */}
<div style={{ display: 'flex', gap: 12 }}>
<button
className="btn-primary"
onClick={async () => {
// Calculate sum (filter out NaN/undefined)
const sum = Object.entries(focusTemp)
.filter(([k]) => k.endsWith('_pct'))
.reduce((acc, [k, v]) => acc + (Number(v) || 0), 0)
if (sum === 0 || isNaN(sum)) {
setError('Mindestens ein Bereich muss gewichtet sein')
return
}
// Normalize to percentages (ensure no NaN values)
const normalized = {
weight_loss_pct: Math.round((Number(focusTemp.weight_loss_pct) || 0) / sum * 100),
muscle_gain_pct: Math.round((Number(focusTemp.muscle_gain_pct) || 0) / sum * 100),
strength_pct: Math.round((Number(focusTemp.strength_pct) || 0) / sum * 100),
endurance_pct: Math.round((Number(focusTemp.endurance_pct) || 0) / sum * 100),
flexibility_pct: Math.round((Number(focusTemp.flexibility_pct) || 0) / sum * 100),
health_pct: Math.round((Number(focusTemp.health_pct) || 0) / sum * 100)
}
// Ensure sum is exactly 100 (adjust largest value if needed due to rounding)
const normalizedSum = Object.values(normalized).reduce((a, b) => a + b, 0)
if (normalizedSum !== 100) {
const largest = Object.entries(normalized).reduce((max, [k, v]) => v > max[1] ? [k, v] : max, ['', 0])
normalized[largest[0]] += (100 - normalizedSum)
}
try {
await api.updateFocusAreas(normalized)
showToast('✓ Fokus-Bereiche aktualisiert')
await loadData()
setFocusEditing(false)
setError(null)
} catch (err) {
setError(err.message || 'Fehler beim Speichern')
}
}}
style={{ flex: 1 }}
>
Speichern
</button>
<button
className="btn-secondary"
onClick={() => {
setFocusTemp(focusAreas)
setFocusEditing(false)
setError(null)
}}
style={{ flex: 1 }}
>
Abbrechen
</button>
</div>
</>
) : focusAreas && (
/* Display Mode */
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 12 }}>
{[
{ key: 'weight_loss_pct', label: 'Fettabbau', icon: '📉', color: '#D85A30' },
{ key: 'muscle_gain_pct', label: 'Muskelaufbau', icon: '💪', color: '#378ADD' },
{ key: 'strength_pct', label: 'Kraftsteigerung', icon: '🏋️', color: '#7B68EE' },
{ key: 'endurance_pct', label: 'Ausdauer', icon: '🏃', color: '#1D9E75' },
{ key: 'flexibility_pct', label: 'Beweglichkeit', icon: '🤸', color: '#E67E22' },
{ key: 'health_pct', label: 'Gesundheit', icon: '❤️', color: '#F59E0B' }
].filter(area => focusAreas[area.key] > 0).map(area => (
<div
key={area.key}
style={{
padding: 12,
background: 'var(--surface2)',
border: '1px solid var(--border)',
borderRadius: 8,
textAlign: 'center'
}}
>
<div style={{ fontSize: 24, marginBottom: 4 }}>{area.icon}</div>
<div style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 4 }}>{area.label}</div>
<div style={{ fontSize: 20, fontWeight: 700, color: area.color }}>{focusAreas[area.key]}%</div>
</div>
))}
</div>
)}
</div>
{/* Tactical Goals List - Category Grouped */}
<div className="card">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<h2 style={{ margin: 0 }}>🎯 Konkrete Ziele</h2>
<button className="btn-primary" onClick={handleCreateGoal}>
<Plus size={16} /> Ziel hinzufügen
</button>
</div>
{Object.keys(groupedGoals).length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px 20px', color: 'var(--text2)' }}>
<Target size={48} style={{ opacity: 0.3, marginBottom: 16 }} />
<p>Noch keine Ziele definiert</p>
<button className="btn-primary" onClick={handleCreateGoal} style={{ marginTop: 16 }}>
Erstes Ziel erstellen
</button>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
{Object.entries(GOAL_CATEGORIES).map(([catKey, catInfo]) => {
const categoryGoals = groupedGoals[catKey] || []
if (categoryGoals.length === 0) return null
return (
<div key={catKey}>
{/* Category Header */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 16,
paddingBottom: 12,
borderBottom: `3px solid ${catInfo.color}`
}}>
<span style={{ fontSize: 28 }}>{catInfo.icon}</span>
<div style={{ flex: 1 }}>
<h3 style={{ margin: 0, color: catInfo.color, fontSize: 18 }}>{catInfo.label}</h3>
<p style={{ margin: 0, fontSize: 13, color: 'var(--text3)' }}>{catInfo.description}</p>
</div>
<span style={{
background: catInfo.color,
color: 'white',
padding: '4px 12px',
borderRadius: 16,
fontSize: 13,
fontWeight: 600
}}>
{categoryGoals.length} {categoryGoals.length === 1 ? 'Ziel' : 'Ziele'}
</span>
</div>
{/* Goals in Category */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{categoryGoals.map(goal => {
const typeInfo = goalTypesMap[goal.goal_type] || { label_de: goal.goal_type, unit: '', icon: '📊' }
const priorityInfo = PRIORITY_LEVELS[goal.priority] || PRIORITY_LEVELS[2]
return (
<div
key={goal.id}
className="card"
style={{
background: 'var(--surface2)',
border: `1px solid var(--border)`,
borderLeft: `5px solid ${catInfo.color}`
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16 }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, flexWrap: 'wrap' }}>
<span style={{ fontSize: 20 }}>{goal.icon || typeInfo.icon}</span>
<span style={{ fontWeight: 600, fontSize: 16 }}>
{goal.name || goal.label_de || typeInfo.label_de || goal.goal_type}
</span>
<span style={{ fontSize: 16 }} title={priorityInfo.label}>
{priorityInfo.stars}
</span>
<span style={{
background: goal.status === 'active' ? '#E6F4F1' : '#F3F4F6',
color: goal.status === 'active' ? 'var(--accent)' : 'var(--text2)',
fontSize: 11,
padding: '3px 8px',
borderRadius: 4,
fontWeight: 500
}}>
{goal.status === 'active' ? 'AKTIV' : goal.status?.toUpperCase() || 'AKTIV'}
</span>
</div>
<div style={{ display: 'flex', gap: 20, marginBottom: 12, fontSize: 14, flexWrap: 'wrap' }}>
<div>
<span style={{ color: 'var(--text2)' }}>Start:</span>{' '}
<strong>{goal.start_value} {goal.unit}</strong>
</div>
<div>
<span style={{ color: 'var(--text2)' }}>Aktuell:</span>{' '}
<strong>{goal.current_value || '—'} {goal.unit}</strong>
</div>
<div>
<span style={{ color: 'var(--text2)' }}>Ziel:</span>{' '}
<strong style={{ color: catInfo.color }}>{goal.target_value} {goal.unit}</strong>
</div>
{goal.target_date && (
<div style={{ color: 'var(--text2)' }}>
<Calendar size={14} style={{ verticalAlign: 'middle', marginRight: 4 }} />
{dayjs(goal.target_date).format('DD.MM.YYYY')}
</div>
)}
</div>
{goal.progress_pct !== null && (
<div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
fontSize: 12,
marginBottom: 4,
color: 'var(--text2)'
}}>
<span>Fortschritt</span>
<span style={{ fontWeight: 600, color: 'var(--text1)' }}>{goal.progress_pct}%</span>
</div>
<div style={{
width: '100%',
height: 8,
background: 'var(--surface)',
borderRadius: 4,
overflow: 'hidden'
}}>
<div style={{
width: `${Math.min(100, Math.max(0, goal.progress_pct))}%`,
height: '100%',
background: catInfo.color,
transition: 'width 0.3s ease'
}} />
</div>
</div>
)}
</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
{/* Progress button only for custom goals (no automatic data source) */}
{!goal.source_table && (
<button
className="btn-secondary"
onClick={() => handleOpenProgressModal(goal)}
style={{ padding: '6px 12px' }}
title="Fortschritt erfassen"
>
<TrendingUp size={14} />
</button>
)}
<button
className="btn-secondary"
onClick={() => handleEditGoal(goal)}
style={{ padding: '6px 12px' }}
title="Bearbeiten"
>
<Pencil size={14} />
</button>
<button
className="btn-secondary"
onClick={() => handleDeleteGoal(goal.id)}
style={{ padding: '6px 12px', color: '#DC2626' }}
title="Löschen"
>
<Trash2 size={14} />
</button>
</div>
</div>
</div>
)
})}
</div>
</div>
)
})}
</div>
)}
</div>
{/* Goal Form Modal */}
{showGoalForm && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
zIndex: 1000,
padding: 16,
paddingTop: 40,
overflowY: 'auto'
}}>
<div className="card" style={{
maxWidth: 500,
width: '100%',
marginBottom: 40
}}>
<div className="card-title">
{editingGoal ? 'Ziel bearbeiten' : 'Neues Ziel'}
</div>
{error && (
<div style={{
padding: 10,
background: '#FCEBEB',
border: '1px solid #D85A30',
borderRadius: 8,
fontSize: 13,
color: '#D85A30',
marginBottom: 16
}}>
{error}
</div>
)}
{/* Zieltyp */}
<div style={{ marginBottom: 16 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 600,
marginBottom: 4,
color: 'var(--text2)'
}}>
Zieltyp
</label>
<select
className="form-input"
style={{ width: '100%' }}
value={formData.goal_type}
onChange={e => handleGoalTypeChange(e.target.value)}
>
{goalTypes.map(type => (
<option key={type.type_key} value={type.type_key}>
{type.icon} {type.label_de}
</option>
))}
</select>
{/* Warning for incomplete goal types */}
{['bp', 'strength', 'flexibility'].includes(formData.goal_type) && (
<div style={{
marginTop: 8,
padding: 8,
background: '#FEF3C7',
border: '1px solid #F59E0B',
borderRadius: 6,
fontSize: 12,
color: '#92400E'
}}>
Dieser Zieltyp ist aktuell eingeschränkt:
{formData.goal_type === 'bp' && ' Blutdruck benötigt 2 Werte (geplant für v2.0)'}
{formData.goal_type === 'strength' && ' Keine Datenquelle vorhanden (geplant für v2.0)'}
{formData.goal_type === 'flexibility' && ' Keine Datenquelle vorhanden (geplant für v2.0)'}
</div>
)}
</div>
{/* Name */}
<div style={{ marginBottom: 16 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 500,
marginBottom: 4,
color: 'var(--text2)'
}}>
Name (optional)
</label>
<input
type="text"
className="form-input"
style={{ width: '100%', textAlign: 'left' }}
value={formData.name}
onChange={e => setFormData(f => ({ ...f, name: e.target.value }))}
placeholder="z.B. Sommerfigur 2026"
/>
</div>
{/* Zielwert */}
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8, marginTop: 20, color: 'var(--text1)' }}>
🎯 Zielwert
</div>
<div style={{ marginBottom: 16 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 500,
marginBottom: 4,
color: 'var(--text2)'
}}>
Wert *
</label>
<div style={{ display: 'flex', gap: 8 }}>
<input
type="number"
step="0.01"
className="form-input"
style={{ flex: 1 }}
value={formData.target_value}
onChange={e => setFormData(f => ({ ...f, target_value: e.target.value }))}
placeholder="Zielwert eingeben"
/>
<div style={{
padding: '8px 16px',
background: 'var(--surface2)',
border: '1px solid var(--border)',
borderRadius: 8,
fontSize: 14,
fontWeight: 600,
color: 'var(--text2)',
minWidth: 70,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
{formData.unit}
</div>
</div>
</div>
{/* Zieldatum */}
<div style={{ marginBottom: 16 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 500,
marginBottom: 4,
color: 'var(--text2)'
}}>
Zieldatum (optional)
</label>
<input
type="date"
className="form-input"
style={{ width: '100%', textAlign: 'left' }}
value={formData.target_date}
onChange={e => setFormData(f => ({ ...f, target_date: e.target.value }))}
/>
</div>
{/* Beschreibung */}
<div style={{ marginBottom: 16 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 500,
marginBottom: 4,
color: 'var(--text2)'
}}>
Beschreibung (optional)
</label>
<textarea
className="form-input"
style={{ width: '100%', minHeight: 80, textAlign: 'left' }}
value={formData.description}
onChange={e => setFormData(f => ({ ...f, description: e.target.value }))}
rows={3}
placeholder="Warum ist dir dieses Ziel wichtig?"
/>
</div>
{/* Category & Priority */}
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8, marginTop: 20, color: 'var(--text1)' }}>
📂 Kategorisierung
</div>
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
{/* Category */}
<div style={{ flex: 1 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 500,
marginBottom: 4,
color: 'var(--text2)'
}}>
Kategorie *
</label>
<select
className="form-input"
style={{ width: '100%' }}
value={formData.category}
onChange={e => setFormData(f => ({ ...f, category: e.target.value }))}
>
{Object.entries(GOAL_CATEGORIES).map(([key, info]) => (
<option key={key} value={key}>
{info.icon} {info.label}
</option>
))}
</select>
</div>
{/* Priority */}
<div style={{ flex: 1 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 500,
marginBottom: 4,
color: 'var(--text2)'
}}>
Priorität *
</label>
<select
className="form-input"
style={{ width: '100%' }}
value={formData.priority}
onChange={e => setFormData(f => ({ ...f, priority: parseInt(e.target.value) }))}
>
{Object.entries(PRIORITY_LEVELS).map(([level, info]) => (
<option key={level} value={level}>
{info.stars} {info.label}
</option>
))}
</select>
</div>
</div>
{/* Buttons */}
<button
className="btn btn-primary btn-full"
onClick={handleSaveGoal}
style={{ marginBottom: 8 }}
>
{editingGoal ? 'Aktualisieren' : 'Ziel erstellen'}
</button>
<button
className="btn btn-secondary btn-full"
onClick={() => {
setShowGoalForm(false)
setEditingGoal(null)
setError(null)
}}
>
Abbrechen
</button>
</div>
</div>
)}
{/* Progress Modal */}
{showProgressModal && progressGoal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
zIndex: 1000,
padding: 16,
overflowY: 'auto'
}}>
<div className="card" style={{
width: '100%',
maxWidth: 600,
marginTop: 40,
marginBottom: 40
}}>
<h2 style={{ marginBottom: 20, display: 'flex', alignItems: 'center', gap: 8 }}>
<TrendingUp size={24} style={{ color: 'var(--accent)' }} />
Fortschritt erfassen
</h2>
<div style={{
padding: 12,
background: 'var(--surface2)',
borderRadius: 8,
marginBottom: 24,
borderLeft: `4px solid ${GOAL_CATEGORIES[progressGoal.category]?.color || 'var(--accent)'}`
}}>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
{progressGoal.name || progressGoal.label_de || progressGoal.goal_type}
</div>
<div style={{ fontSize: 14, color: 'var(--text2)' }}>
Ziel: {progressGoal.target_value} {progressGoal.unit}
</div>
</div>
{/* Progress Form */}
<div style={{ marginBottom: 24 }}>
<h3 style={{ fontSize: 16, marginBottom: 16 }}>Neuer Eintrag</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<div style={{
fontSize: 14,
fontWeight: 600,
marginBottom: 8,
color: 'var(--text1)'
}}>
Datum
</div>
<input
type="date"
className="form-input"
value={progressFormData.date}
onChange={(e) => setProgressFormData({ ...progressFormData, date: e.target.value })}
max={new Date().toISOString().split('T')[0]}
style={{ width: '100%', textAlign: 'left' }}
/>
</div>
<div>
<div style={{
fontSize: 14,
fontWeight: 600,
marginBottom: 8,
color: 'var(--text1)'
}}>
Wert ({progressGoal.unit})
</div>
<input
type="number"
step="0.01"
className="form-input"
value={progressFormData.value}
onChange={(e) => setProgressFormData({ ...progressFormData, value: e.target.value })}
placeholder={`z.B. ${progressGoal.current_value || progressGoal.target_value}`}
style={{ width: '100%', textAlign: 'left' }}
/>
</div>
<div>
<div style={{
fontSize: 14,
fontWeight: 600,
marginBottom: 8,
color: 'var(--text1)'
}}>
Notiz (optional)
</div>
<textarea
className="form-input"
value={progressFormData.note}
onChange={(e) => setProgressFormData({ ...progressFormData, note: e.target.value })}
placeholder="Optionale Notiz..."
rows={2}
style={{ width: '100%', textAlign: 'left' }}
/>
</div>
<button
className="btn-primary"
onClick={handleSaveProgress}
disabled={!progressFormData.value}
style={{ width: '100%' }}
>
Eintrag speichern
</button>
</div>
</div>
{/* Progress History */}
<div>
<h3 style={{ fontSize: 16, marginBottom: 12 }}>
Verlauf ({progressEntries.length} Einträge)
</h3>
{progressEntries.length === 0 ? (
<div style={{
padding: 24,
textAlign: 'center',
color: 'var(--text2)',
background: 'var(--surface2)',
borderRadius: 8
}}>
Noch keine Einträge vorhanden
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{progressEntries.map(entry => (
<div key={entry.id} style={{
padding: 12,
background: 'var(--surface2)',
borderRadius: 8,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontWeight: 600 }}>
{dayjs(entry.date).format('DD.MM.YYYY')}
</span>
<span style={{
fontSize: 18,
fontWeight: 600,
color: 'var(--accent)'
}}>
{entry.value} {progressGoal.unit}
</span>
{entry.source !== 'manual' && (
<span style={{
fontSize: 11,
padding: '2px 6px',
background: 'var(--surface)',
borderRadius: 4,
color: 'var(--text3)'
}}>
{entry.source}
</span>
)}
</div>
{entry.note && (
<div style={{ fontSize: 13, color: 'var(--text2)' }}>
{entry.note}
</div>
)}
</div>
{entry.source === 'manual' && (
<button
className="btn-secondary"
onClick={() => handleDeleteProgress(entry.id)}
style={{
padding: '6px 12px',
color: '#DC2626',
flexShrink: 0
}}
title="Löschen"
>
<Trash2 size={14} />
</button>
)}
</div>
))}
</div>
)}
</div>
{/* Close Button */}
<div style={{ marginTop: 24, display: 'flex', justifyContent: 'flex-end' }}>
<button
className="btn-secondary"
onClick={() => {
setShowProgressModal(false)
setProgressGoal(null)
setProgressEntries([])
setProgressFormData({
date: new Date().toISOString().split('T')[0],
value: '',
note: ''
})
}}
>
Schließen
</button>
</div>
</div>
</div>
)}
</div>
)
}