**UX Improvements:** - Progress modal: full-width inputs, label-as-heading, left-aligned text - Progress button only visible for custom goals (no source_table) - Prevents confusion with automatic tracking (Weight, Activity, etc.) **New Page: Custom Goals (Capture/Eigene Ziele):** - Dedicated page for daily custom goal value entry - Clean goal selection with progress bars - Quick entry form (date, value, note) - Recent progress history (last 5 entries) - Mobile-optimized for daily use **Architecture:** - Analysis/Goals → Strategic (define goals, set priorities) - Capture/Custom Goals → Tactical (daily value entry) - History → Evaluation (goal achievement analysis) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
371 lines
13 KiB
JavaScript
371 lines
13 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { api } from '../utils/api'
|
|
import { Target, TrendingUp, Calendar, CheckCircle2, AlertCircle } from 'lucide-react'
|
|
import dayjs from 'dayjs'
|
|
|
|
export default function CustomGoalsPage() {
|
|
const [customGoals, setCustomGoals] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState(null)
|
|
const [selectedGoal, setSelectedGoal] = useState(null)
|
|
const [formData, setFormData] = useState({
|
|
date: new Date().toISOString().split('T')[0],
|
|
value: '',
|
|
note: ''
|
|
})
|
|
const [recentProgress, setRecentProgress] = useState([])
|
|
|
|
useEffect(() => {
|
|
loadCustomGoals()
|
|
}, [])
|
|
|
|
const loadCustomGoals = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const grouped = await api.listGoalsGrouped()
|
|
|
|
// Extract all goals and filter for custom only (no source_table)
|
|
const allGoals = Object.values(grouped).flat()
|
|
const custom = allGoals.filter(g => !g.source_table)
|
|
|
|
setCustomGoals(custom)
|
|
setError(null)
|
|
} catch (err) {
|
|
console.error('Failed to load custom goals:', err)
|
|
setError(err.message || 'Fehler beim Laden')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadRecentProgress = async (goalId) => {
|
|
try {
|
|
const entries = await api.listGoalProgress(goalId)
|
|
setRecentProgress(entries.slice(0, 5)) // Last 5 entries
|
|
} catch (err) {
|
|
console.error('Failed to load progress:', err)
|
|
setRecentProgress([])
|
|
}
|
|
}
|
|
|
|
const handleSelectGoal = async (goal) => {
|
|
setSelectedGoal(goal)
|
|
setFormData({
|
|
date: new Date().toISOString().split('T')[0],
|
|
value: goal.current_value || '',
|
|
note: ''
|
|
})
|
|
await loadRecentProgress(goal.id)
|
|
}
|
|
|
|
const handleSaveProgress = async () => {
|
|
if (!formData.value || !formData.date) {
|
|
setError('Bitte Datum und Wert eingeben')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const data = {
|
|
date: formData.date,
|
|
value: parseFloat(formData.value),
|
|
note: formData.note || null
|
|
}
|
|
|
|
await api.createGoalProgress(selectedGoal.id, data)
|
|
|
|
// Reset form and reload
|
|
setFormData({
|
|
date: new Date().toISOString().split('T')[0],
|
|
value: '',
|
|
note: ''
|
|
})
|
|
|
|
await loadCustomGoals()
|
|
await loadRecentProgress(selectedGoal.id)
|
|
|
|
// Update selected goal with new current_value
|
|
const updated = customGoals.find(g => g.id === selectedGoal.id)
|
|
if (updated) setSelectedGoal(updated)
|
|
|
|
setError(null)
|
|
} catch (err) {
|
|
console.error('Failed to save progress:', err)
|
|
setError(err.message || 'Fehler beim Speichern')
|
|
}
|
|
}
|
|
|
|
const getProgressPercentage = (goal) => {
|
|
if (!goal.current_value || !goal.target_value) return 0
|
|
|
|
const current = parseFloat(goal.current_value)
|
|
const target = parseFloat(goal.target_value)
|
|
const start = parseFloat(goal.start_value) || 0
|
|
|
|
if (goal.direction === 'decrease') {
|
|
return Math.min(100, Math.max(0, ((start - current) / (start - target)) * 100))
|
|
} else {
|
|
return Math.min(100, Math.max(0, ((current - start) / (target - start)) * 100))
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ padding: 20, textAlign: 'center' }}>
|
|
<div className="spinner" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div style={{ paddingBottom: 80 }}>
|
|
{/* Header */}
|
|
<div style={{
|
|
background: 'linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%)',
|
|
color: 'white',
|
|
padding: '24px 16px',
|
|
marginBottom: 16
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
|
|
<Target size={28} />
|
|
<h1 style={{ fontSize: 24, fontWeight: 700, margin: 0 }}>Eigene Ziele</h1>
|
|
</div>
|
|
<div style={{ fontSize: 14, opacity: 0.9 }}>
|
|
Erfasse Fortschritte für deine individuellen Ziele
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div style={{
|
|
margin: '0 16px 16px',
|
|
padding: 12,
|
|
background: '#FEE2E2',
|
|
color: '#991B1B',
|
|
borderRadius: 8,
|
|
fontSize: 14
|
|
}}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{customGoals.length === 0 ? (
|
|
<div className="card" style={{ margin: 16, textAlign: 'center', padding: 40 }}>
|
|
<Target size={48} style={{ color: 'var(--text3)', margin: '0 auto 16px' }} />
|
|
<div style={{ fontSize: 16, color: 'var(--text2)', marginBottom: 8 }}>
|
|
Keine eigenen Ziele vorhanden
|
|
</div>
|
|
<div style={{ fontSize: 14, color: 'var(--text3)' }}>
|
|
Erstelle eigene Ziele über die Ziele-Seite in der Analyse
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, padding: 16 }}>
|
|
{/* Goal Selection */}
|
|
<div className="card">
|
|
<h2 style={{ fontSize: 16, marginBottom: 12, fontWeight: 600 }}>
|
|
Ziel auswählen ({customGoals.length})
|
|
</h2>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
{customGoals.map(goal => {
|
|
const progress = getProgressPercentage(goal)
|
|
const isSelected = selectedGoal?.id === goal.id
|
|
|
|
return (
|
|
<button
|
|
key={goal.id}
|
|
onClick={() => handleSelectGoal(goal)}
|
|
style={{
|
|
width: '100%',
|
|
padding: 12,
|
|
background: isSelected ? 'var(--accent)' : 'var(--surface2)',
|
|
color: isSelected ? 'white' : 'var(--text1)',
|
|
border: 'none',
|
|
borderRadius: 8,
|
|
cursor: 'pointer',
|
|
textAlign: 'left',
|
|
transition: 'all 0.2s'
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
|
<span style={{ fontWeight: 600 }}>
|
|
{goal.name || goal.label_de || goal.goal_type}
|
|
</span>
|
|
{goal.current_value && (
|
|
<span style={{
|
|
fontSize: 18,
|
|
fontWeight: 700,
|
|
opacity: isSelected ? 1 : 0.8
|
|
}}>
|
|
{goal.current_value} {goal.unit}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{goal.target_value && (
|
|
<>
|
|
<div style={{ fontSize: 13, opacity: isSelected ? 0.9 : 0.7, marginBottom: 6 }}>
|
|
Ziel: {goal.target_value} {goal.unit}
|
|
</div>
|
|
|
|
<div style={{
|
|
width: '100%',
|
|
height: 6,
|
|
background: isSelected ? 'rgba(255,255,255,0.2)' : 'var(--surface)',
|
|
borderRadius: 3,
|
|
overflow: 'hidden'
|
|
}}>
|
|
<div style={{
|
|
width: `${progress}%`,
|
|
height: '100%',
|
|
background: isSelected ? 'white' : 'var(--accent)',
|
|
transition: 'width 0.3s'
|
|
}} />
|
|
</div>
|
|
</>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress Entry Form */}
|
|
{selectedGoal && (
|
|
<div className="card">
|
|
<h2 style={{ fontSize: 16, marginBottom: 16, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<TrendingUp size={20} style={{ color: 'var(--accent)' }} />
|
|
Fortschritt erfassen
|
|
</h2>
|
|
|
|
<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={formData.date}
|
|
onChange={(e) => setFormData({ ...formData, 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 ({selectedGoal.unit})
|
|
</div>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
className="form-input"
|
|
value={formData.value}
|
|
onChange={(e) => setFormData({ ...formData, value: e.target.value })}
|
|
placeholder={`Aktueller Wert in ${selectedGoal.unit}`}
|
|
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={formData.note}
|
|
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
|
placeholder="Optionale Notiz zu dieser Messung..."
|
|
rows={2}
|
|
style={{ width: '100%', textAlign: 'left' }}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
className="btn-primary"
|
|
onClick={handleSaveProgress}
|
|
disabled={!formData.value}
|
|
style={{ width: '100%' }}
|
|
>
|
|
<CheckCircle2 size={18} style={{ marginRight: 8 }} />
|
|
Wert speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recent Progress */}
|
|
{selectedGoal && recentProgress.length > 0 && (
|
|
<div className="card">
|
|
<h2 style={{ fontSize: 16, marginBottom: 12, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<Calendar size={20} style={{ color: 'var(--accent)' }} />
|
|
Letzte Einträge
|
|
</h2>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
{recentProgress.map(entry => (
|
|
<div key={entry.id} style={{
|
|
padding: 12,
|
|
background: 'var(--surface2)',
|
|
borderRadius: 8
|
|
}}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
|
<span style={{ fontSize: 14, color: 'var(--text2)' }}>
|
|
{dayjs(entry.date).format('DD.MM.YYYY')}
|
|
</span>
|
|
<span style={{ fontSize: 18, fontWeight: 600, color: 'var(--accent)' }}>
|
|
{entry.value} {selectedGoal.unit}
|
|
</span>
|
|
</div>
|
|
{entry.note && (
|
|
<div style={{ fontSize: 13, color: 'var(--text3)' }}>
|
|
{entry.note}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Help Card */}
|
|
{!selectedGoal && (
|
|
<div style={{
|
|
padding: 16,
|
|
background: 'var(--surface2)',
|
|
borderRadius: 8,
|
|
border: '1px solid var(--border)'
|
|
}}>
|
|
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
|
<AlertCircle size={20} style={{ color: 'var(--accent)', flexShrink: 0, marginTop: 2 }} />
|
|
<div style={{ fontSize: 14, color: 'var(--text2)' }}>
|
|
<div style={{ fontWeight: 600, marginBottom: 4, color: 'var(--text1)' }}>
|
|
Eigene Ziele erfassen
|
|
</div>
|
|
Wähle ein Ziel aus und erfasse regelmäßig deine Fortschritte.
|
|
Die Werte werden automatisch in deine Zielverfolgung übernommen.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|