feat: Enhance goal progress tracking and display
- 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:
parent
df8e732709
commit
1b01f5e6d0
|
|
@ -699,6 +699,12 @@ def get_goals_grouped(session: dict = Depends(require_auth)):
|
||||||
grouped[cat] = []
|
grouped[cat] = []
|
||||||
|
|
||||||
goal_dict = r2d(goal)
|
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'], [])
|
goal_dict['focus_contributions'] = focus_map.get(goal['id'], [])
|
||||||
grouped[cat].append(goal_dict)
|
grouped[cat].append(goal_dict)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,28 @@ const getCategoryForGoalType = (goalType) => {
|
||||||
return mapping[goalType] || 'other'
|
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() {
|
export default function GoalsPage() {
|
||||||
const [goalMode, setGoalMode] = useState(null)
|
const [goalMode, setGoalMode] = useState(null)
|
||||||
const [userFocusWeights, setUserFocusWeights] = useState([]) // v2.0: User's focus area weights
|
const [userFocusWeights, setUserFocusWeights] = useState([]) // v2.0: User's focus area weights
|
||||||
|
|
@ -613,6 +635,17 @@ export default function GoalsPage() {
|
||||||
{categoryGoals.map(goal => {
|
{categoryGoals.map(goal => {
|
||||||
const typeInfo = goalTypesMap[goal.goal_type] || { label_de: goal.goal_type, unit: '', icon: '📊' }
|
const typeInfo = goalTypesMap[goal.goal_type] || { label_de: goal.goal_type, unit: '', icon: '📊' }
|
||||||
const priorityInfo = PRIORITY_LEVELS[goal.priority] || PRIORITY_LEVELS[2]
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -719,30 +752,40 @@ export default function GoalsPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{goal.progress_pct !== null && (
|
{progressPct != null && (
|
||||||
<div>
|
<div style={{ marginTop: 4 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginBottom: 4,
|
marginBottom: 6,
|
||||||
color: 'var(--text2)'
|
color: 'var(--text2)',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
gap: 8
|
||||||
}}>
|
}}>
|
||||||
<span>Fortschritt</span>
|
<span>Fortschritt (Start → Ziel)</span>
|
||||||
<span style={{ fontWeight: 600, color: 'var(--text1)' }}>{goal.progress_pct}%</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>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 8,
|
height: 10,
|
||||||
background: 'var(--surface)',
|
background: 'var(--surface)',
|
||||||
borderRadius: 4,
|
borderRadius: 6,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
|
border: '1px solid var(--border)'
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: `${Math.min(100, Math.max(0, goal.progress_pct))}%`,
|
width: `${progressBarWidth}%`,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: catInfo.color,
|
background: getProgressColor(Math.min(100, Math.max(0, progressPct))),
|
||||||
transition: 'width 0.3s ease'
|
transition: 'width 0.35s ease'
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user