feat: Frontend Phase 3.2 - Goal Form Focus Areas + Badges
**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:
parent
d14157f7ad
commit
6a961ce88f
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user