mitai-jinkendo/frontend/src/pages/GoalsPage.jsx
Lars d7aa0eb3af
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
feat: show target_date in goal list next to target value
- Start value already showed start_date in parentheses
- Now target value also shows target_date in parentheses
- Consistent UX: both dates visible at their respective values
2026-03-28 14:50:34 +01:00

1485 lines
57 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.

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 [userFocusWeights, setUserFocusWeights] = useState([]) // v2.0: User's focus area weights
const [userFocusGrouped, setUserFocusGrouped] = useState({}) // Grouped by category
const [focusWeightsEditing, setFocusWeightsEditing] = useState(false)
const [focusWeightsTemp, setFocusWeightsTemp] = useState({}) // Temp: {focus_area_id: weight}
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 [focusAreas, setFocusAreas] = useState([]) // v2.0: All available focus areas (for selection)
const [focusAreasGrouped, setFocusAreasGrouped] = useState({}) // Grouped by category
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',
start_date: new Date().toISOString().split('T')[0], // Default to today
target_date: '',
name: '',
description: '',
focus_contributions: [] // v2.0: [{focus_area_id, contribution_weight}]
})
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
setLoading(true)
setError(null)
try {
const [modeData, goalsData, groupedData, typesData, userWeightsData, focusAreasData] = await Promise.all([
api.getGoalMode(),
api.listGoals(),
api.listGoalsGrouped(), // v2.1: Load grouped by category
api.listGoalTypeDefinitions(), // Phase 1.5: Load from DB
api.getUserFocusPreferences(), // v2.0: Load user's focus area weights
api.listFocusAreaDefinitions(false) // v2.0: Load all available focus areas
])
setGoalMode(modeData.goal_mode)
// Debug: Check what we received from API
console.log('[DEBUG] Received goals from API:', goalsData.length)
const weightGoal = goalsData.find(g => g.goal_type === 'weight')
if (weightGoal) {
console.log('[DEBUG] Weight goal from API:', JSON.stringify(weightGoal, null, 2))
}
setGoals(goalsData)
setGroupedGoals(groupedData)
// v2.0: User focus weights (dynamic)
setUserFocusWeights(userWeightsData.weights || [])
setUserFocusGrouped(userWeightsData.grouped || {})
// Build temp object for editing: {focus_area_id: weight}
const tempWeights = {}
if (userWeightsData.weights) {
userWeightsData.weights.forEach(w => {
tempWeights[w.id] = w.weight
})
}
setFocusWeightsTemp(tempWeights)
// 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)
// v2.0: All focus area definitions (for selection in goal form)
if (focusAreasData) {
setFocusAreas(focusAreasData.areas || [])
setFocusAreasGrouped(focusAreasData.grouped || {})
}
} 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 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: '',
focus_contributions: [] // v2.0: Empty for new goal
})
setShowGoalForm(true)
}
const handleEditGoal = (goal) => {
console.log('[DEBUG] Editing goal ID:', goal.id)
console.log('[DEBUG] Full goal object:', JSON.stringify(goal, null, 2))
console.log('[DEBUG] start_date from goal:', goal.start_date, 'type:', typeof goal.start_date)
console.log('[DEBUG] target_date from goal:', goal.target_date, 'type:', typeof goal.target_date)
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,
start_date: goal.start_date || '', // Load actual date or empty (not today!)
target_date: goal.target_date || '',
name: goal.name || '',
description: goal.description || '',
focus_contributions: goal.focus_contributions || [] // v2.0: Load existing contributions
})
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,
start_date: formData.start_date || null,
target_date: formData.target_date || null,
name: formData.name || null,
description: formData.description || null,
focus_contributions: formData.focus_contributions || [] // v2.0: Focus area assignments
}
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 - Dynamic) */}
<div className="card" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<h2 style={{ margin: 0 }}>🎯 Fokus-Bereiche</h2>
<button
className="btn-secondary"
onClick={() => {
// Initialize temp weights from current weights
const tempWeights = {}
focusAreas.forEach(fa => {
tempWeights[fa.id] = focusWeightsTemp[fa.id] || 0
})
setFocusWeightsTemp(tempWeights)
setFocusWeightsEditing(!focusWeightsEditing)
}}
style={{ padding: '6px 12px' }}
>
<Pencil size={14} /> {focusWeightsEditing ? 'Abbrechen' : 'Anpassen'}
</button>
</div>
<p style={{ color: 'var(--text2)', fontSize: 14, marginBottom: 16 }}>
Wähle deine Trainingsschwerpunkte und gewichte sie relativ zueinander. Prozente werden automatisch berechnet.
</p>
{focusWeightsEditing ? (
<>
{/* Edit Mode - Sliders grouped by category */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 24, marginBottom: 20 }}>
{Object.entries(focusAreasGrouped).map(([category, areas]) => (
<div key={category}>
<div style={{
fontSize: 11,
fontWeight: 700,
color: 'var(--text3)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginBottom: 12
}}>
{category}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{areas.map(area => {
const weight = focusWeightsTemp[area.id] || 0
const totalWeight = Object.values(focusWeightsTemp).reduce((sum, w) => sum + (w || 0), 0)
const percentage = totalWeight > 0 ? Math.round((weight / totalWeight) * 100) : 0
return (
<div key={area.id}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 18 }}>{area.icon}</span>
<span style={{ fontWeight: 500, fontSize: 14 }}>{area.name_de}</span>
</div>
<span style={{
fontSize: 14,
fontWeight: 600,
color: 'var(--accent)',
minWidth: 80,
textAlign: 'right'
}}>
{weight} {percentage}%
</span>
</div>
<input
type="range"
min="0"
max="100"
step="5"
value={weight}
onChange={e => setFocusWeightsTemp(f => ({
...f,
[area.id]: parseInt(e.target.value)
}))}
style={{
width: '100%',
height: 6,
borderRadius: 3,
background: `linear-gradient(to right, var(--accent) 0%, var(--accent) ${weight}%, var(--border) ${weight}%, var(--border) 100%)`,
outline: 'none',
cursor: 'pointer'
}}
/>
</div>
)
})}
</div>
</div>
))}
</div>
{/* Save Button */}
<button
className="btn-primary btn-full"
onClick={async () => {
try {
await api.updateUserFocusPreferences({ weights: focusWeightsTemp })
showToast('✓ Fokus-Bereiche aktualisiert')
await loadData()
setFocusWeightsEditing(false)
setError(null)
} catch (err) {
setError(err.message || 'Fehler beim Speichern')
}
}}
>
Speichern
</button>
</>
) : (
/* Display Mode - Cards for areas with weight > 0 */
userFocusWeights.length === 0 ? (
<div style={{
textAlign: 'center',
padding: '32px 16px',
color: 'var(--text3)',
background: 'var(--surface2)',
borderRadius: 8
}}>
<p style={{ margin: 0, marginBottom: 12 }}>Keine Fokus-Bereiche definiert</p>
<button
className="btn-secondary"
onClick={() => setFocusWeightsEditing(true)}
style={{ fontSize: 13 }}
>
Jetzt konfigurieren
</button>
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: 12 }}>
{userFocusWeights.map(area => (
<div
key={area.id}
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.name_de}</div>
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--accent)' }}>{area.percentage}%</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>
{/* Focus Area Badges (v2.0) */}
{goal.focus_contributions && goal.focus_contributions.length > 0 && (
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 6,
marginBottom: 12
}}>
{goal.focus_contributions.map(fc => (
<span
key={fc.focus_area_id}
style={{
fontSize: 11,
padding: '3px 8px',
background: 'var(--accent-light)',
color: 'var(--accent-dark)',
borderRadius: 4,
fontWeight: 500,
display: 'flex',
alignItems: 'center',
gap: 4
}}
title={`${fc.name_de}: ${fc.contribution_weight}%`}
>
{fc.icon && <span>{fc.icon}</span>}
<span>{fc.name_de}</span>
<span style={{ opacity: 0.7 }}>({fc.contribution_weight}%)</span>
</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>
{goal.start_date && (
<span style={{ fontSize: 12, color: 'var(--text3)', marginLeft: 4 }}>
({dayjs(goal.start_date).format('DD.MM.YY')})
</span>
)}
</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>
{goal.target_date && (
<span style={{ fontSize: 12, color: 'var(--text3)', marginLeft: 4 }}>
({dayjs(goal.target_date).format('DD.MM.YY')})
</span>
)}
</div>
</div>
{/* Timeline: Start → Ziel */}
{(goal.start_date || goal.target_date) && (
<div style={{ display: 'flex', gap: 12, fontSize: 13, color: 'var(--text2)', marginBottom: 12, alignItems: 'center' }}>
{goal.start_date && (
<>
<Calendar size={13} style={{ verticalAlign: 'middle' }} />
<span>{dayjs(goal.start_date).format('DD.MM.YY')}</span>
</>
)}
{goal.start_date && goal.target_date && <span></span>}
{goal.target_date && (
<span style={{ fontWeight: 500 }}>{dayjs(goal.target_date).format('DD.MM.YY')}</span>
)}
</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>
{/* Focus Areas (v2.0) */}
<div style={{ marginBottom: 16 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 600,
marginBottom: 8,
color: 'var(--text1)'
}}>
🎯 Zahlt ein auf (Fokusbereiche)
</label>
<div style={{
fontSize: 12,
color: 'var(--text3)',
marginBottom: 8
}}>
Wähle die Bereiche aus, auf die dieses Ziel einzahlt. Mehrfachauswahl möglich.
</div>
{Object.keys(focusAreasGrouped).length === 0 ? (
<div style={{
padding: 12,
background: 'var(--surface2)',
borderRadius: 8,
fontSize: 13,
color: 'var(--text3)'
}}>
Keine Focus Areas verfügbar
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{Object.entries(focusAreasGrouped).map(([category, areas]) => {
// Filter to only show focus areas the user has weighted
const userWeightedAreaIds = new Set(userFocusWeights.map(w => w.id))
const filteredAreas = areas.filter(area => userWeightedAreaIds.has(area.id))
// Skip category if no weighted areas
if (filteredAreas.length === 0) return null
return (
<div key={category}>
<div style={{
fontSize: 11,
fontWeight: 600,
color: 'var(--text3)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginBottom: 6
}}>
{category}
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 6
}}>
{filteredAreas.map(area => {
const isSelected = formData.focus_contributions?.some(
fc => fc.focus_area_id === area.id
)
return (
<button
key={area.id}
type="button"
onClick={() => {
if (isSelected) {
// Remove
setFormData(f => ({
...f,
focus_contributions: f.focus_contributions.filter(
fc => fc.focus_area_id !== area.id
)
}))
} else {
// Add with default weight 100%
setFormData(f => ({
...f,
focus_contributions: [
...(f.focus_contributions || []),
{
focus_area_id: area.id,
contribution_weight: 100
}
]
}))
}
}}
style={{
padding: '6px 12px',
background: isSelected ? 'var(--accent)' : 'var(--surface2)',
color: isSelected ? 'white' : 'var(--text2)',
border: isSelected ? '2px solid var(--accent)' : '1px solid var(--border)',
borderRadius: 8,
fontSize: 13,
fontWeight: isSelected ? 600 : 400,
cursor: 'pointer',
transition: 'all 0.15s',
fontFamily: 'var(--font)',
display: 'flex',
alignItems: 'center',
gap: 4
}}
>
{area.icon && <span>{area.icon}</span>}
<span>{area.name_de}</span>
</button>
)
})}
</div>
</div>
)
})}
</div>
)}
{/* Selected areas with weights */}
{formData.focus_contributions && formData.focus_contributions.length > 0 && (
<div style={{
marginTop: 12,
padding: 12,
background: 'var(--accent-light)',
borderRadius: 8,
border: '1px solid var(--accent)'
}}>
<div style={{
fontSize: 12,
fontWeight: 600,
color: 'var(--accent-dark)',
marginBottom: 8
}}>
Gewichtung ({formData.focus_contributions.length} ausgewählt)
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{formData.focus_contributions.map((fc, idx) => {
const area = focusAreas.find(a => a.id === fc.focus_area_id)
if (!area) return null
return (
<div key={fc.focus_area_id} style={{
display: 'flex',
alignItems: 'center',
gap: 8
}}>
<div style={{
flex: 1,
fontSize: 13,
fontWeight: 500,
color: 'var(--accent-dark)'
}}>
{area.icon} {area.name_de}
</div>
<input
type="number"
min="0"
max="100"
step="5"
value={fc.contribution_weight}
onChange={(e) => {
const newWeight = parseFloat(e.target.value) || 0
setFormData(f => ({
...f,
focus_contributions: f.focus_contributions.map((item, i) =>
i === idx ? { ...item, contribution_weight: newWeight } : item
)
}))
}}
style={{
width: 70,
padding: '4px 8px',
fontSize: 13,
textAlign: 'center',
border: '1px solid var(--accent)',
borderRadius: 6
}}
/>
<span style={{ fontSize: 12, color: 'var(--accent-dark)' }}>%</span>
</div>
)
})}
</div>
</div>
)}
</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>
{/* Startdatum */}
<div style={{ marginBottom: 16 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 500,
marginBottom: 4,
color: 'var(--text2)'
}}>
Startdatum
</label>
<input
type="date"
className="form-input"
style={{ width: '100%', textAlign: 'left' }}
value={formData.start_date || ''}
onChange={e => setFormData(f => ({ ...f, start_date: e.target.value }))}
/>
<div style={{ fontSize: 12, color: 'var(--text3)', marginTop: 4 }}>
Startwert wird automatisch aus historischen Daten ermittelt
</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>
)
}