feat: Enhance goal progress tracking and display
All checks were successful
Deploy Development / deploy (push) Successful in 55s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 17s

- Added a function to calculate goal progress percentage based on start, target, and current values.
- Updated GoalsPage to display progress in a user-friendly format, including visual progress bars.
- Implemented error handling for goal progress updates in the backend to ensure robustness.
This commit is contained in:
Lars 2026-04-14 10:43:49 +02:00
parent df8e732709
commit 1b01f5e6d0
2 changed files with 61 additions and 12 deletions

View File

@ -699,6 +699,12 @@ def get_goals_grouped(session: dict = Depends(require_auth)):
grouped[cat] = []
goal_dict = r2d(goal)
# Keep in sync with GET /goals/list: derive current_value & progress from live data
try:
_update_goal_progress(conn, pid, goal_dict)
except Exception as e:
print(f"[ERROR] Failed to update progress for goal {goal_dict.get('id')}: {e}")
goal_dict['focus_contributions'] = focus_map.get(goal['id'], [])
grouped[cat].append(goal_dict)

View File

@ -55,6 +55,28 @@ const getCategoryForGoalType = (goalType) => {
return mapping[goalType] || 'other'
}
/** Fortschritt in % von Start zum Ziel; bevorzugt `progress_pct` vom Server, sonst aus Ist-Werten. */
function getGoalProgressPercent(goal) {
const apiVal = goal?.progress_pct
if (apiVal !== null && apiVal !== undefined && Number.isFinite(Number(apiVal))) {
return Number(apiVal)
}
const s = goal?.start_value != null ? Number(goal.start_value) : NaN
const t = goal?.target_value != null ? Number(goal.target_value) : NaN
const c =
goal?.current_value != null && goal.current_value !== ''
? Number(goal.current_value)
: NaN
if (!Number.isFinite(s) || !Number.isFinite(t) || !Number.isFinite(c)) return null
const span = Math.abs(t - s)
if (span < 1e-9) {
if (Math.abs(c - s) < 1e-9) return 100
return null
}
if (t > s) return ((c - s) / (t - s)) * 100
return ((s - c) / (s - t)) * 100
}
export default function GoalsPage() {
const [goalMode, setGoalMode] = useState(null)
const [userFocusWeights, setUserFocusWeights] = useState([]) // v2.0: User's focus area weights
@ -613,6 +635,17 @@ export default function GoalsPage() {
{categoryGoals.map(goal => {
const typeInfo = goalTypesMap[goal.goal_type] || { label_de: goal.goal_type, unit: '', icon: '📊' }
const priorityInfo = PRIORITY_LEVELS[goal.priority] || PRIORITY_LEVELS[2]
const progressPct = getGoalProgressPercent(goal)
const progressLabel =
progressPct != null
? (Math.abs(progressPct - Math.round(progressPct)) < 0.05
? `${Math.round(progressPct)}`
: `${Math.round(progressPct * 10) / 10}`.replace('.', ','))
: null
const progressBarWidth =
progressPct != null
? Math.min(100, Math.max(0, progressPct))
: 0
return (
<div
@ -719,30 +752,40 @@ export default function GoalsPage() {
</div>
)}
{goal.progress_pct !== null && (
<div>
{progressPct != null && (
<div style={{ marginTop: 4 }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
fontSize: 12,
marginBottom: 4,
color: 'var(--text2)'
marginBottom: 6,
color: 'var(--text2)',
alignItems: 'baseline',
gap: 8
}}>
<span>Fortschritt</span>
<span style={{ fontWeight: 600, color: 'var(--text1)' }}>{goal.progress_pct}%</span>
<span>Fortschritt (Start Ziel)</span>
<span style={{ fontWeight: 600, color: 'var(--text1)' }}>
{progressLabel}%
{progressPct > 100 && (
<span style={{ fontWeight: 500, color: 'var(--text3)', marginLeft: 4 }}>
(über Ziel)
</span>
)}
</span>
</div>
<div style={{
width: '100%',
height: 8,
height: 10,
background: 'var(--surface)',
borderRadius: 4,
overflow: 'hidden'
borderRadius: 6,
overflow: 'hidden',
border: '1px solid var(--border)'
}}>
<div style={{
width: `${Math.min(100, Math.max(0, goal.progress_pct))}%`,
width: `${progressBarWidth}%`,
height: '100%',
background: catInfo.color,
transition: 'width 0.3s ease'
background: getProgressColor(Math.min(100, Math.max(0, progressPct))),
transition: 'width 0.35s ease'
}} />
</div>
</div>