Improved UX for focus area configuration: - Sliders now use relative weights (0-10) instead of percentages - System automatically normalizes to percentages (sum=100%) - Live preview shows "weight → percent%" (e.g., "5 → 50%") - No more manual balancing required from user User sets: Kraft=5, Ausdauer=3, Flexibilität=2 System calculates: 50%, 30%, 20% Addresses user feedback: "Summe muss 100% sein" not user-friendly Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
843 lines
30 KiB
JavaScript
843 lines
30 KiB
JavaScript
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 Mode Definitions
|
||
const GOAL_MODES = [
|
||
{
|
||
id: 'weight_loss',
|
||
icon: '📉',
|
||
label: 'Gewichtsreduktion',
|
||
description: 'Kaloriendefizit, Fettabbau',
|
||
color: '#D85A30'
|
||
},
|
||
{
|
||
id: 'strength',
|
||
icon: '💪',
|
||
label: 'Kraftaufbau',
|
||
description: 'Muskelwachstum, progressive Belastung',
|
||
color: '#378ADD'
|
||
},
|
||
{
|
||
id: 'endurance',
|
||
icon: '🏃',
|
||
label: 'Ausdauer',
|
||
description: 'VO2Max, aerobe Kapazität',
|
||
color: '#1D9E75'
|
||
},
|
||
{
|
||
id: 'recomposition',
|
||
icon: '⚖️',
|
||
label: 'Körperkomposition',
|
||
description: 'Gleichzeitig Fett ab- & Muskeln aufbauen',
|
||
color: '#7B68EE'
|
||
},
|
||
{
|
||
id: 'health',
|
||
icon: '❤️',
|
||
label: 'Allgemeine Gesundheit',
|
||
description: 'Ausgewogen, präventiv',
|
||
color: '#E67E22'
|
||
}
|
||
]
|
||
|
||
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([])
|
||
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 [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,
|
||
target_value: '',
|
||
unit: 'kg',
|
||
target_date: '',
|
||
name: '',
|
||
description: ''
|
||
})
|
||
|
||
useEffect(() => {
|
||
loadData()
|
||
}, [])
|
||
|
||
const loadData = async () => {
|
||
setLoading(true)
|
||
setError(null)
|
||
try {
|
||
const [modeData, goalsData, typesData, focusData] = await Promise.all([
|
||
api.getGoalMode(),
|
||
api.listGoals(),
|
||
api.listGoalTypeDefinitions(), // Phase 1.5: Load from DB
|
||
api.getFocusAreas() // v2.0: Load focus areas
|
||
])
|
||
setGoalMode(modeData.goal_mode)
|
||
setGoals(goalsData)
|
||
setFocusAreas(focusData)
|
||
setFocusTemp(focusData) // Initialize temp state
|
||
|
||
// 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
|
||
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,
|
||
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'
|
||
}))
|
||
}
|
||
|
||
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,
|
||
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 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={() => 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 weight = Math.round(focusTemp[area.key] / 10)
|
||
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
|
||
const actualPercent = sum > 0 ? Math.round(focusTemp[area.key] / 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>
|
||
|
||
{/* Weight Total Display */}
|
||
<div style={{
|
||
padding: 12,
|
||
background: 'var(--surface2)',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: 8,
|
||
marginBottom: 16
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<span style={{ fontWeight: 600, color: 'var(--text2)' }}>
|
||
Gewichtung gesamt:
|
||
</span>
|
||
<span style={{ fontSize: 18, fontWeight: 600, color: 'var(--text1)' }}>
|
||
{Object.values(focusTemp).reduce((a, b) => a + b, 0) / 10}
|
||
</span>
|
||
</div>
|
||
<div style={{ fontSize: 12, marginTop: 4, color: 'var(--text3)' }}>
|
||
💡 Die Prozentanteile werden automatisch berechnet
|
||
</div>
|
||
</div>
|
||
|
||
{/* Action Buttons */}
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button
|
||
className="btn-primary"
|
||
onClick={async () => {
|
||
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
|
||
|
||
if (sum === 0) {
|
||
setError('Mindestens ein Bereich muss gewichtet sein')
|
||
return
|
||
}
|
||
|
||
// Normalize to percentages
|
||
const normalized = {}
|
||
Object.keys(focusTemp).forEach(key => {
|
||
normalized[key] = Math.round(focusTemp[key] / 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 */}
|
||
<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>
|
||
|
||
{goals.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: 12 }}>
|
||
{goals.map(goal => {
|
||
const typeInfo = goalTypesMap[goal.goal_type] || { label: goal.goal_type, unit: '', icon: '📊' }
|
||
return (
|
||
<div
|
||
key={goal.id}
|
||
className="card"
|
||
style={{
|
||
background: 'var(--surface2)',
|
||
border: goal.is_primary ? '2px solid var(--accent)' : '1px solid var(--border)'
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||
<span style={{ fontSize: 20 }}>{typeInfo.icon}</span>
|
||
<span style={{ fontWeight: 600 }}>
|
||
{goal.name || typeInfo.label}
|
||
</span>
|
||
{goal.is_primary && (
|
||
<span style={{
|
||
background: 'var(--accent)',
|
||
color: 'white',
|
||
fontSize: 11,
|
||
padding: '2px 8px',
|
||
borderRadius: 4
|
||
}}>
|
||
PRIMÄR
|
||
</span>
|
||
)}
|
||
<span style={{
|
||
background: goal.status === 'active' ? '#E6F4F1' : '#F3F4F6',
|
||
color: goal.status === 'active' ? 'var(--accent)' : 'var(--text2)',
|
||
fontSize: 11,
|
||
padding: '2px 8px',
|
||
borderRadius: 4
|
||
}}>
|
||
{goal.status === 'active' ? 'AKTIV' : goal.status?.toUpperCase()}
|
||
</span>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 24, marginBottom: 12, fontSize: 14 }}>
|
||
<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>{goal.target_value} {goal.unit}</strong>
|
||
</div>
|
||
{goal.target_date && (
|
||
<div>
|
||
<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
|
||
}}>
|
||
<span>Fortschritt</span>
|
||
<span style={{ fontWeight: 600 }}>{goal.progress_pct}%</span>
|
||
</div>
|
||
<div style={{
|
||
width: '100%',
|
||
height: 6,
|
||
background: 'var(--surface)',
|
||
borderRadius: 3,
|
||
overflow: 'hidden'
|
||
}}>
|
||
<div style={{
|
||
width: `${Math.min(100, Math.max(0, goal.progress_pct))}%`,
|
||
height: '100%',
|
||
background: getProgressColor(goal.progress_pct),
|
||
transition: 'width 0.3s ease'
|
||
}} />
|
||
</div>
|
||
|
||
{goal.on_track !== null && (
|
||
<div style={{ marginTop: 8, fontSize: 12 }}>
|
||
{goal.on_track ? (
|
||
<span style={{ color: 'var(--accent)' }}>
|
||
✓ Ziel voraussichtlich erreichbar bis {dayjs(goal.target_date).format('DD.MM.YYYY')}
|
||
</span>
|
||
) : (
|
||
<span style={{ color: '#D85A30' }}>
|
||
⚠ Prognose: {goal.projection_date ? dayjs(goal.projection_date).format('DD.MM.YYYY') : 'Offen'}
|
||
{goal.target_date && ' (später als geplant)'}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<button
|
||
className="btn-secondary"
|
||
onClick={() => handleEditGoal(goal)}
|
||
style={{ padding: '6px 12px' }}
|
||
>
|
||
<Pencil size={14} />
|
||
</button>
|
||
<button
|
||
className="btn-secondary"
|
||
onClick={() => handleDeleteGoal(goal.id)}
|
||
style={{ padding: '6px 12px', color: '#DC2626' }}
|
||
>
|
||
<Trash2 size={14} />
|
||
</button>
|
||
</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>
|
||
|
||
{/* Primärziel */}
|
||
<div style={{
|
||
padding: 12,
|
||
background: 'var(--surface2)',
|
||
borderRadius: 8,
|
||
marginBottom: 20
|
||
}}>
|
||
<label style={{
|
||
display: 'flex',
|
||
alignItems: 'flex-start',
|
||
gap: 10,
|
||
cursor: 'pointer'
|
||
}}>
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.is_primary}
|
||
onChange={e => setFormData(f => ({ ...f, is_primary: e.target.checked }))}
|
||
style={{ marginTop: 2 }}
|
||
/>
|
||
<div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 2 }}>
|
||
Als Primärziel setzen
|
||
</div>
|
||
<div style={{ fontSize: 12, color: 'var(--text2)' }}>
|
||
Dein Primärziel hat höchste Priorität in Analysen und Charts
|
||
</div>
|
||
</div>
|
||
</label>
|
||
</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>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|