feat: Frontend Phase 3.2 - Goal Form Focus Areas + Badges
Some checks failed
Deploy Development / deploy (push) Failing after 34s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

**Goal Form Extended:**
- Load focus area definitions on page load
- Multi-Select UI grouped by category (7 categories)
- Chip-style selection (click to toggle)
- Weight sliders per selected area (0-100%)
- Selected areas highlighted in accent color
- Focus contributions saved/loaded on create/edit

**Goal Cards:**
- Focus Area badges below status
- Shows icon + name + weight percentage
- Hover shows full details
- Color-coded (accent-light background)

**Integration Complete:**
- State: focusAreas, focusAreasGrouped
- Handlers: handleCreateGoal, handleEditGoal
- Data flow: Backend → Frontend → Display

**Result:**
- User can assign goals to multiple focus areas
- Visual indication of what each goal contributes to
- Foundation for Phase 0b (goal-aware AI scoring)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-27 19:54:45 +01:00
parent d14157f7ad
commit 6a961ce88f

View File

@ -71,6 +71,8 @@ export default function GoalsPage() {
const [groupedGoals, setGroupedGoals] = useState({}) // Category-grouped goals
const [goalTypes, setGoalTypes] = useState([]) // Dynamic from DB (Phase 1.5)
const [goalTypesMap, setGoalTypesMap] = useState({}) // For quick lookup
const [focusAreas, setFocusAreas] = useState([]) // v2.0: Available focus areas
const [focusAreasGrouped, setFocusAreasGrouped] = useState({}) // Grouped by category
const [showGoalForm, setShowGoalForm] = useState(false)
const [editingGoal, setEditingGoal] = useState(null)
const [showProgressModal, setShowProgressModal] = useState(false)
@ -95,7 +97,8 @@ export default function GoalsPage() {
unit: 'kg',
target_date: '',
name: '',
description: ''
description: '',
focus_contributions: [] // v2.0: [{focus_area_id, contribution_weight}]
})
useEffect(() => {
@ -106,12 +109,13 @@ export default function GoalsPage() {
setLoading(true)
setError(null)
try {
const [modeData, goalsData, groupedData, typesData, focusData] = await Promise.all([
const [modeData, goalsData, groupedData, typesData, focusData, focusAreasData] = await Promise.all([
api.getGoalMode(),
api.listGoals(),
api.listGoalsGrouped(), // v2.1: Load grouped by category
api.listGoalTypeDefinitions(), // Phase 1.5: Load from DB
api.getFocusAreas() // v2.0: Load focus areas
api.getFocusAreas(), // v2.0: Load user focus preferences (legacy)
api.listFocusAreaDefinitions(false) // v2.0: Load available focus areas
])
setGoalMode(modeData.goal_mode)
setGoals(goalsData)
@ -148,6 +152,12 @@ export default function GoalsPage() {
setGoalTypes(typesData || [])
setGoalTypesMap(typesMap)
// v2.0: Set focus area definitions
if (focusAreasData) {
setFocusAreas(focusAreasData.areas || [])
setFocusAreasGrouped(focusAreasData.grouped || {})
}
} catch (err) {
console.error('Failed to load goals:', err)
setError(`Fehler beim Laden: ${err.message || err.toString()}`)
@ -188,7 +198,8 @@ export default function GoalsPage() {
unit: goalTypesMap[firstType]?.unit || 'kg',
target_date: '',
name: '',
description: ''
description: '',
focus_contributions: [] // v2.0: Empty for new goal
})
setShowGoalForm(true)
}
@ -204,7 +215,8 @@ export default function GoalsPage() {
unit: goal.unit,
target_date: goal.target_date || '',
name: goal.name || '',
description: goal.description || ''
description: goal.description || '',
focus_contributions: goal.focus_contributions || [] // v2.0: Load existing contributions
})
setShowGoalForm(true)
}
@ -652,6 +664,38 @@ export default function GoalsPage() {
</span>
</div>
{/* Focus Area Badges (v2.0) */}
{goal.focus_contributions && goal.focus_contributions.length > 0 && (
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 6,
marginBottom: 12
}}>
{goal.focus_contributions.map(fc => (
<span
key={fc.focus_area_id}
style={{
fontSize: 11,
padding: '3px 8px',
background: 'var(--accent-light)',
color: 'var(--accent-dark)',
borderRadius: 4,
fontWeight: 500,
display: 'flex',
alignItems: 'center',
gap: 4
}}
title={`${fc.name_de}: ${fc.contribution_weight}%`}
>
{fc.icon && <span>{fc.icon}</span>}
<span>{fc.name_de}</span>
<span style={{ opacity: 0.7 }}>({fc.contribution_weight}%)</span>
</span>
))}
</div>
)}
<div style={{ display: 'flex', gap: 20, marginBottom: 12, fontSize: 14, flexWrap: 'wrap' }}>
<div>
<span style={{ color: 'var(--text2)' }}>Start:</span>{' '}
@ -848,6 +892,182 @@ export default function GoalsPage() {
/>
</div>
{/* Focus Areas (v2.0) */}
<div style={{ marginBottom: 16 }}>
<label style={{
display: 'block',
fontSize: 13,
fontWeight: 600,
marginBottom: 8,
color: 'var(--text1)'
}}>
🎯 Zahlt ein auf (Fokusbereiche)
</label>
<div style={{
fontSize: 12,
color: 'var(--text3)',
marginBottom: 8
}}>
Wähle die Bereiche aus, auf die dieses Ziel einzahlt. Mehrfachauswahl möglich.
</div>
{Object.keys(focusAreasGrouped).length === 0 ? (
<div style={{
padding: 12,
background: 'var(--surface2)',
borderRadius: 8,
fontSize: 13,
color: 'var(--text3)'
}}>
Keine Focus Areas verfügbar
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{Object.entries(focusAreasGrouped).map(([category, areas]) => (
<div key={category}>
<div style={{
fontSize: 11,
fontWeight: 600,
color: 'var(--text3)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginBottom: 6
}}>
{category}
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 6
}}>
{areas.map(area => {
const isSelected = formData.focus_contributions?.some(
fc => fc.focus_area_id === area.id
)
return (
<button
key={area.id}
type="button"
onClick={() => {
if (isSelected) {
// Remove
setFormData(f => ({
...f,
focus_contributions: f.focus_contributions.filter(
fc => fc.focus_area_id !== area.id
)
}))
} else {
// Add with default weight 100%
setFormData(f => ({
...f,
focus_contributions: [
...(f.focus_contributions || []),
{
focus_area_id: area.id,
contribution_weight: 100
}
]
}))
}
}}
style={{
padding: '6px 12px',
background: isSelected ? 'var(--accent)' : 'var(--surface2)',
color: isSelected ? 'white' : 'var(--text2)',
border: isSelected ? '2px solid var(--accent)' : '1px solid var(--border)',
borderRadius: 8,
fontSize: 13,
fontWeight: isSelected ? 600 : 400,
cursor: 'pointer',
transition: 'all 0.15s',
fontFamily: 'var(--font)',
display: 'flex',
alignItems: 'center',
gap: 4
}}
>
{area.icon && <span>{area.icon}</span>}
<span>{area.name_de}</span>
</button>
)
})}
</div>
</div>
))}
</div>
)}
{/* Selected areas with weights */}
{formData.focus_contributions && formData.focus_contributions.length > 0 && (
<div style={{
marginTop: 12,
padding: 12,
background: 'var(--accent-light)',
borderRadius: 8,
border: '1px solid var(--accent)'
}}>
<div style={{
fontSize: 12,
fontWeight: 600,
color: 'var(--accent-dark)',
marginBottom: 8
}}>
Gewichtung ({formData.focus_contributions.length} ausgewählt)
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{formData.focus_contributions.map((fc, idx) => {
const area = focusAreas.find(a => a.id === fc.focus_area_id)
if (!area) return null
return (
<div key={fc.focus_area_id} style={{
display: 'flex',
alignItems: 'center',
gap: 8
}}>
<div style={{
flex: 1,
fontSize: 13,
fontWeight: 500,
color: 'var(--accent-dark)'
}}>
{area.icon} {area.name_de}
</div>
<input
type="number"
min="0"
max="100"
step="5"
value={fc.contribution_weight}
onChange={(e) => {
const newWeight = parseFloat(e.target.value) || 0
setFormData(f => ({
...f,
focus_contributions: f.focus_contributions.map((item, i) =>
i === idx ? { ...item, contribution_weight: newWeight } : item
)
}))
}}
style={{
width: 70,
padding: '4px 8px',
fontSize: 13,
textAlign: 'center',
border: '1px solid var(--accent)',
borderRadius: 6
}}
/>
<span style={{ fontSize: 12, color: 'var(--accent-dark)' }}>%</span>
</div>
)
})}
</div>
</div>
)}
</div>
{/* Zielwert */}
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8, marginTop: 20, color: 'var(--text1)' }}>
🎯 Zielwert