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 [goalTypesMap, setGoalTypesMap] = useState({}) // For quick lookup
|
||||||
const [showGoalForm, setShowGoalForm] = useState(false)
|
const [showGoalForm, setShowGoalForm] = useState(false)
|
||||||
const [editingGoal, setEditingGoal] = useState(null)
|
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 [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [toast, setToast] = 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) => {
|
const getProgressColor = (progress) => {
|
||||||
if (progress >= 100) return 'var(--accent)'
|
if (progress >= 100) return 'var(--accent)'
|
||||||
if (progress >= 75) return '#1D9E75'
|
if (progress >= 75) return '#1D9E75'
|
||||||
|
|
@ -620,6 +704,14 @@ export default function GoalsPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
<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
|
<button
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
onClick={() => handleEditGoal(goal)}
|
onClick={() => handleEditGoal(goal)}
|
||||||
|
|
@ -914,6 +1006,197 @@ export default function GoalsPage() {
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user