Goalsystem V1 #50

Merged
Lars merged 51 commits from develop into main 2026-03-27 17:40:51 +01:00
Showing only changes of commit d97925d5a1 - Show all commits

View File

@ -46,6 +46,16 @@ const GOAL_MODES = [
export default function GoalsPage() { export default function GoalsPage() {
const [goalMode, setGoalMode] = useState(null) 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 [goals, setGoals] = useState([])
const [goalTypes, setGoalTypes] = useState([]) // Dynamic from DB (Phase 1.5) const [goalTypes, setGoalTypes] = useState([]) // Dynamic from DB (Phase 1.5)
const [goalTypesMap, setGoalTypesMap] = useState({}) // For quick lookup const [goalTypesMap, setGoalTypesMap] = useState({}) // For quick lookup
@ -74,13 +84,16 @@ export default function GoalsPage() {
setLoading(true) setLoading(true)
setError(null) setError(null)
try { try {
const [modeData, goalsData, typesData] = await Promise.all([ const [modeData, goalsData, typesData, focusData] = await Promise.all([
api.getGoalMode(), api.getGoalMode(),
api.listGoals(), api.listGoals(),
api.listGoalTypeDefinitions() // Phase 1.5: Load from DB api.listGoalTypeDefinitions(), // Phase 1.5: Load from DB
api.getFocusAreas() // v2.0: Load focus areas
]) ])
setGoalMode(modeData.goal_mode) setGoalMode(modeData.goal_mode)
setGoals(goalsData) setGoals(goalsData)
setFocusAreas(focusData)
setFocusTemp(focusData) // Initialize temp state
// Convert types array to map for quick lookup // Convert types array to map for quick lookup
const typesMap = {} const typesMap = {}
@ -258,43 +271,189 @@ export default function GoalsPage() {
</div> </div>
)} )}
{/* Strategic Goal Mode Selection */} {/* Focus Areas (v2.0) */}
<div className="card" style={{ marginBottom: 16 }}> <div className="card" style={{ marginBottom: 16 }}>
<h2 style={{ marginTop: 0 }}>🎯 Trainingsmodus</h2> <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 }}> <p style={{ color: 'var(--text2)', fontSize: 14, marginBottom: 16 }}>
Wähle deine grundlegende Trainingsausrichtung. Dies beeinflusst die Gewichtung Gewichte deine Trainingsziele individuell. Die Summe muss 100% ergeben.
und Interpretation aller Analysen. {focusAreas && !focusAreas.custom && (
<span style={{ display: 'block', marginTop: 4, fontStyle: 'italic' }}>
Aktuell abgeleitet aus Trainingsmodus "{goalMode}" - klicke "Anpassen" für individuelle Gewichtung
</span>
)}
</p> </p>
<div style={{ {focusEditing ? (
display: 'grid', <>
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', {/* Sliders */}
gap: 12 <div style={{ display: 'flex', flexDirection: 'column', gap: 20, marginBottom: 20 }}>
}}> {[
{GOAL_MODES.map(mode => ( { key: 'weight_loss_pct', label: 'Fettabbau', icon: '📉', color: '#D85A30' },
<button { key: 'muscle_gain_pct', label: 'Muskelaufbau', icon: '💪', color: '#378ADD' },
key={mode.id} { key: 'strength_pct', label: 'Kraftsteigerung', icon: '🏋️', color: '#7B68EE' },
onClick={() => handleGoalModeChange(mode.id)} { key: 'endurance_pct', label: 'Ausdauer', icon: '🏃', color: '#1D9E75' },
className={goalMode === mode.id ? 'btn-primary' : 'btn-secondary'} { key: 'flexibility_pct', label: 'Beweglichkeit', icon: '🤸', color: '#E67E22' },
style={{ { key: 'health_pct', label: 'Gesundheit', icon: '❤️', color: '#F59E0B' }
padding: 16, ].map(area => (
textAlign: 'left', <div key={area.key}>
background: goalMode === mode.id ? mode.color : 'var(--surface)', <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
borderColor: goalMode === mode.id ? mode.color : 'var(--border)', <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
color: goalMode === mode.id ? 'white' : 'var(--text1)' <span style={{ fontSize: 20 }}>{area.icon}</span>
}} <span style={{ fontWeight: 500 }}>{area.label}</span>
> </div>
<div style={{ fontSize: 24, marginBottom: 8 }}>{mode.icon}</div> <span style={{
<div style={{ fontWeight: 600, marginBottom: 4 }}>{mode.label}</div> fontSize: 18,
<div style={{ fontWeight: 600,
fontSize: 12, color: area.color,
opacity: goalMode === mode.id ? 0.9 : 0.7 minWidth: 50,
}}> textAlign: 'right'
{mode.description} }}>
{focusTemp[area.key]}%
</span>
</div>
<input
type="range"
min="0"
max="100"
step="5"
value={focusTemp[area.key]}
onChange={e => setFocusTemp(f => ({ ...f, [area.key]: parseInt(e.target.value) }))}
style={{
width: '100%',
height: 8,
borderRadius: 4,
background: `linear-gradient(to right, ${area.color} 0%, ${area.color} ${focusTemp[area.key]}%, var(--border) ${focusTemp[area.key]}%, var(--border) 100%)`,
outline: 'none',
cursor: 'pointer'
}}
/>
</div> </div>
</button>
))} ))}
</div> </div>
{/* Sum Display */}
<div style={{
padding: 12,
background: (() => {
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
if (sum === 100) return 'var(--accent)'
return '#FEF2F2'
})(),
border: (() => {
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
if (sum === 100) return '1px solid var(--accent)'
return '1px solid #FCA5A5'
})(),
borderRadius: 8,
marginBottom: 16
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{
fontWeight: 600,
color: (() => {
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
if (sum === 100) return 'white'
return '#DC2626'
})()
}}>
Summe:
</span>
<span style={{
fontSize: 20,
fontWeight: 700,
color: (() => {
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
if (sum === 100) return 'white'
return '#DC2626'
})()
}}>
{Object.values(focusTemp).reduce((a, b) => a + b, 0)}%
</span>
</div>
{Object.values(focusTemp).reduce((a, b) => a + b, 0) !== 100 && (
<div style={{ fontSize: 12, marginTop: 4, color: '#DC2626' }}>
Summe muss 100% ergeben
</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 !== 100) {
setError('Summe muss 100% ergeben')
return
}
try {
await api.updateFocusAreas(focusTemp)
showToast('✓ Fokus-Bereiche aktualisiert')
await loadData()
setFocusEditing(false)
setError(null)
} catch (err) {
setError(err.message || 'Fehler beim Speichern')
}
}}
disabled={Object.values(focusTemp).reduce((a, b) => a + b, 0) !== 100}
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> </div>
{/* Tactical Goals List */} {/* Tactical Goals List */}