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

683 lines
23 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 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 [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] = await Promise.all([
api.getGoalMode(),
api.listGoals(),
api.listGoalTypeDefinitions() // Phase 1.5: Load from DB
])
setGoalMode(modeData.goal_mode)
setGoals(goalsData)
// Convert types array to map for quick lookup
const typesMap = {}
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 der Ziele')
} 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 = () => {
setEditingGoal(null)
const firstType = goalTypes.length > 0 ? goalTypes[0].type_key : 'weight'
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>
)}
{/* Strategic Goal Mode Selection */}
<div className="card" style={{ marginBottom: 16 }}>
<h2 style={{ marginTop: 0 }}>🎯 Trainingsmodus</h2>
<p style={{ color: 'var(--text2)', fontSize: 14, marginBottom: 16 }}>
Wähle deine grundlegende Trainingsausrichtung. Dies beeinflusst die Gewichtung
und Interpretation aller Analysen.
</p>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: 12
}}>
{GOAL_MODES.map(mode => (
<button
key={mode.id}
onClick={() => handleGoalModeChange(mode.id)}
className={goalMode === mode.id ? 'btn-primary' : 'btn-secondary'}
style={{
padding: 16,
textAlign: 'left',
background: goalMode === mode.id ? mode.color : 'var(--surface)',
borderColor: goalMode === mode.id ? mode.color : 'var(--border)',
color: goalMode === mode.id ? 'white' : 'var(--text1)'
}}
>
<div style={{ fontSize: 24, marginBottom: 8 }}>{mode.icon}</div>
<div style={{ fontWeight: 600, marginBottom: 4 }}>{mode.label}</div>
<div style={{
fontSize: 12,
opacity: goalMode === mode.id ? 0.9 : 0.7
}}>
{mode.description}
</div>
</button>
))}
</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>
)
}