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 (
) } return (

Ziele

{error && (

{error}

)} {toast && (
{toast}
)} {/* Focus Areas (v2.0 - Dynamic) */}

🎯 Fokus-Bereiche

Wähle deine Trainingsschwerpunkte und gewichte sie relativ zueinander. Prozente werden automatisch berechnet.

{focusWeightsEditing ? ( <> {/* Edit Mode - Sliders grouped by category */}
{Object.entries(focusAreasGrouped).map(([category, areas]) => (
{category}
{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 (
{area.icon} {area.name_de}
{weight} → {percentage}%
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' }} />
) })}
))}
{/* Save Button */} ) : ( /* Display Mode - Cards for areas with weight > 0 */ userFocusWeights.length === 0 ? (

Keine Fokus-Bereiche definiert

) : (
{userFocusWeights.map(area => (
{area.icon}
{area.name_de}
{area.percentage}%
))}
) )}
{/* Tactical Goals List - Category Grouped */}

🎯 Konkrete Ziele

{Object.keys(groupedGoals).length === 0 ? (

Noch keine Ziele definiert

) : (
{Object.entries(GOAL_CATEGORIES).map(([catKey, catInfo]) => { const categoryGoals = groupedGoals[catKey] || [] if (categoryGoals.length === 0) return null return (
{/* Category Header */}
{catInfo.icon}

{catInfo.label}

{catInfo.description}

{categoryGoals.length} {categoryGoals.length === 1 ? 'Ziel' : 'Ziele'}
{/* Goals in Category */}
{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 (
{goal.icon || typeInfo.icon} {goal.name || goal.label_de || typeInfo.label_de || goal.goal_type} {priorityInfo.stars} {goal.status === 'active' ? 'AKTIV' : goal.status?.toUpperCase() || 'AKTIV'}
{/* Focus Area Badges (v2.0) */} {goal.focus_contributions && goal.focus_contributions.length > 0 && (
{goal.focus_contributions.map(fc => ( {fc.icon && {fc.icon}} {fc.name_de} ({fc.contribution_weight}%) ))}
)}
Start:{' '} {goal.start_value} {goal.unit} {goal.start_date && ( ({dayjs(goal.start_date).format('DD.MM.YY')}) )}
Aktuell:{' '} {goal.current_value || '—'} {goal.unit}
Ziel:{' '} {goal.target_value} {goal.unit} {goal.target_date && ( ({dayjs(goal.target_date).format('DD.MM.YY')}) )}
{/* Timeline: Start → Ziel */} {(goal.start_date || goal.target_date) && (
{goal.start_date && ( <> {dayjs(goal.start_date).format('DD.MM.YY')} )} {goal.start_date && goal.target_date && } {goal.target_date && ( {dayjs(goal.target_date).format('DD.MM.YY')} )}
)} {goal.progress_pct !== null && (
Fortschritt {goal.progress_pct}%
)}
{/* Progress button only for custom goals (no automatic data source) */} {!goal.source_table && ( )}
) })}
) })}
)}
{/* Goal Form Modal */} {showGoalForm && (
{editingGoal ? 'Ziel bearbeiten' : 'Neues Ziel'}
{error && (
{error}
)} {/* Zieltyp */}
{/* Warning for incomplete goal types */} {['bp', 'strength', 'flexibility'].includes(formData.goal_type) && (
⚠️ 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)'}
)}
{/* Name */}
setFormData(f => ({ ...f, name: e.target.value }))} placeholder="z.B. Sommerfigur 2026" />
{/* Focus Areas (v2.0) */}
Wähle die Bereiche aus, auf die dieses Ziel einzahlt. Mehrfachauswahl möglich.
{Object.keys(focusAreasGrouped).length === 0 ? (
Keine Focus Areas verfügbar
) : (
{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 (
{category}
{filteredAreas.map(area => { const isSelected = formData.focus_contributions?.some( fc => fc.focus_area_id === area.id ) return ( ) })}
) })}
)} {/* Selected areas with weights */} {formData.focus_contributions && formData.focus_contributions.length > 0 && (
Gewichtung ({formData.focus_contributions.length} ausgewählt)
{formData.focus_contributions.map((fc, idx) => { const area = focusAreas.find(a => a.id === fc.focus_area_id) if (!area) return null return (
{area.icon} {area.name_de}
{ 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 }} /> %
) })}
)}
{/* Zielwert */}
🎯 Zielwert
setFormData(f => ({ ...f, target_value: e.target.value }))} placeholder="Zielwert eingeben" />
{formData.unit}
{/* Startdatum */}
setFormData(f => ({ ...f, start_date: e.target.value }))} />
Startwert wird automatisch aus historischen Daten ermittelt
{/* Zieldatum */}
setFormData(f => ({ ...f, target_date: e.target.value }))} />
{/* Beschreibung */}