feat: Goal Progress Log UI - complete frontend
- 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:
parent
7db98a4fa6
commit
398c645a98
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user