mitai-jinkendo/frontend/src/pages/GoalsPage.jsx

1467 lines
56 KiB
React
Raw Normal View History

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)
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) => {
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>
</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>
)
}