feat: Goal Progress Log UI - complete frontend
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

- Added Progress button (TrendingUp icon) to each goal card
- Created Progress Modal with:
  • Form to add new progress entry (date, value, note)
  • Historical entries list with delete option
  • Category-colored goal info header
  • Auto-disables manual delete for non-manual entries
- Integration complete: handlers → API → backend

Completes Phase 0a Progress Tracking (Migration 030 + Backend + Frontend)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-27 14:02:24 +01:00
parent 7db98a4fa6
commit 398c645a98

View File

@ -73,6 +73,14 @@ export default function GoalsPage() {
const [goalTypesMap, setGoalTypesMap] = useState({}) // For quick lookup
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)
@ -261,6 +269,82 @@ export default function GoalsPage() {
}
}
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'
@ -620,6 +704,14 @@ export default function GoalsPage() {
</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<button
className="btn-secondary"
onClick={() => handleOpenProgressModal(goal)}
style={{ padding: '6px 12px' }}
title="Fortschritt erfassen"
>
<TrendingUp size={14} />
</button>
<button
className="btn-secondary"
onClick={() => handleEditGoal(goal)}
@ -914,6 +1006,197 @@ export default function GoalsPage() {
</div>
</div>
)}
{/* Progress Modal */}
{showProgressModal && progressGoal && (
<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,
overflowY: 'auto'
}}>
<div className="card" style={{
width: '100%',
maxWidth: 600,
marginTop: 40,
marginBottom: 40
}}>
<h2 style={{ marginBottom: 20, display: 'flex', alignItems: 'center', gap: 8 }}>
<TrendingUp size={24} style={{ color: 'var(--accent)' }} />
Fortschritt erfassen
</h2>
<div style={{
padding: 12,
background: 'var(--surface2)',
borderRadius: 8,
marginBottom: 24,
borderLeft: `4px solid ${GOAL_CATEGORIES[progressGoal.category]?.color || 'var(--accent)'}`
}}>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
{progressGoal.name || progressGoal.label_de || progressGoal.goal_type}
</div>
<div style={{ fontSize: 14, color: 'var(--text2)' }}>
Ziel: {progressGoal.target_value} {progressGoal.unit}
</div>
</div>
{/* Progress Form */}
<div style={{ marginBottom: 24 }}>
<h3 style={{ fontSize: 16, marginBottom: 12 }}>Neuer Eintrag</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div>
<label className="form-label">Datum</label>
<input
type="date"
className="form-input"
value={progressFormData.date}
onChange={(e) => setProgressFormData({ ...progressFormData, date: e.target.value })}
max={new Date().toISOString().split('T')[0]}
/>
</div>
<div>
<label className="form-label">Wert ({progressGoal.unit})</label>
<input
type="number"
step="0.01"
className="form-input"
value={progressFormData.value}
onChange={(e) => setProgressFormData({ ...progressFormData, value: e.target.value })}
placeholder={`z.B. ${progressGoal.current_value || progressGoal.target_value}`}
/>
</div>
<div>
<label className="form-label">Notiz (optional)</label>
<textarea
className="form-input"
value={progressFormData.note}
onChange={(e) => setProgressFormData({ ...progressFormData, note: e.target.value })}
placeholder="Optionale Notiz..."
rows={2}
/>
</div>
<button
className="btn-primary"
onClick={handleSaveProgress}
disabled={!progressFormData.value}
>
Eintrag speichern
</button>
</div>
</div>
{/* Progress History */}
<div>
<h3 style={{ fontSize: 16, marginBottom: 12 }}>
Verlauf ({progressEntries.length} Einträge)
</h3>
{progressEntries.length === 0 ? (
<div style={{
padding: 24,
textAlign: 'center',
color: 'var(--text2)',
background: 'var(--surface2)',
borderRadius: 8
}}>
Noch keine Einträge vorhanden
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{progressEntries.map(entry => (
<div key={entry.id} style={{
padding: 12,
background: 'var(--surface2)',
borderRadius: 8,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontWeight: 600 }}>
{dayjs(entry.date).format('DD.MM.YYYY')}
</span>
<span style={{
fontSize: 18,
fontWeight: 600,
color: 'var(--accent)'
}}>
{entry.value} {progressGoal.unit}
</span>
{entry.source !== 'manual' && (
<span style={{
fontSize: 11,
padding: '2px 6px',
background: 'var(--surface)',
borderRadius: 4,
color: 'var(--text3)'
}}>
{entry.source}
</span>
)}
</div>
{entry.note && (
<div style={{ fontSize: 13, color: 'var(--text2)' }}>
{entry.note}
</div>
)}
</div>
{entry.source === 'manual' && (
<button
className="btn-secondary"
onClick={() => handleDeleteProgress(entry.id)}
style={{
padding: '6px 12px',
color: '#DC2626',
flexShrink: 0
}}
title="Löschen"
>
<Trash2 size={14} />
</button>
)}
</div>
))}
</div>
)}
</div>
{/* Close Button */}
<div style={{ marginTop: 24, display: 'flex', justifyContent: 'flex-end' }}>
<button
className="btn-secondary"
onClick={() => {
setShowProgressModal(false)
setProgressGoal(null)
setProgressEntries([])
setProgressFormData({
date: new Date().toISOString().split('T')[0],
value: '',
note: ''
})
}}
>
Schließen
</button>
</div>
</div>
</div>
)}
</div>
)
}