Goalsystem V1 #50
|
|
@ -31,6 +31,7 @@ import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage'
|
||||||
import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage'
|
import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage'
|
||||||
import AdminTrainingProfiles from './pages/AdminTrainingProfiles'
|
import AdminTrainingProfiles from './pages/AdminTrainingProfiles'
|
||||||
import AdminPromptsPage from './pages/AdminPromptsPage'
|
import AdminPromptsPage from './pages/AdminPromptsPage'
|
||||||
|
import AdminGoalTypesPage from './pages/AdminGoalTypesPage'
|
||||||
import SubscriptionPage from './pages/SubscriptionPage'
|
import SubscriptionPage from './pages/SubscriptionPage'
|
||||||
import SleepPage from './pages/SleepPage'
|
import SleepPage from './pages/SleepPage'
|
||||||
import RestDaysPage from './pages/RestDaysPage'
|
import RestDaysPage from './pages/RestDaysPage'
|
||||||
|
|
@ -188,6 +189,7 @@ function AppShell() {
|
||||||
<Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/>
|
<Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/>
|
||||||
<Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/>
|
<Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/>
|
||||||
<Route path="/admin/prompts" element={<AdminPromptsPage/>}/>
|
<Route path="/admin/prompts" element={<AdminPromptsPage/>}/>
|
||||||
|
<Route path="/admin/goal-types" element={<AdminGoalTypesPage/>}/>
|
||||||
<Route path="/subscription" element={<SubscriptionPage/>}/>
|
<Route path="/subscription" element={<SubscriptionPage/>}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
446
frontend/src/pages/AdminGoalTypesPage.jsx
Normal file
446
frontend/src/pages/AdminGoalTypesPage.jsx
Normal file
|
|
@ -0,0 +1,446 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Settings, Plus, Pencil, Trash2, Database } from 'lucide-react'
|
||||||
|
import { api } from '../utils/api'
|
||||||
|
|
||||||
|
export default function AdminGoalTypesPage() {
|
||||||
|
const [goalTypes, setGoalTypes] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const [showForm, setShowForm] = useState(false)
|
||||||
|
const [editingType, setEditingType] = useState(null)
|
||||||
|
const [toast, setToast] = useState(null)
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
type_key: '',
|
||||||
|
label_de: '',
|
||||||
|
unit: '',
|
||||||
|
icon: '',
|
||||||
|
category: 'custom',
|
||||||
|
source_table: '',
|
||||||
|
source_column: '',
|
||||||
|
aggregation_method: 'latest',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const CATEGORIES = ['body', 'mind', 'activity', 'nutrition', 'recovery', 'custom']
|
||||||
|
const AGGREGATION_METHODS = [
|
||||||
|
{ value: 'latest', label: 'Letzter Wert' },
|
||||||
|
{ value: 'avg_7d', label: 'Durchschnitt 7 Tage' },
|
||||||
|
{ value: 'avg_30d', label: 'Durchschnitt 30 Tage' },
|
||||||
|
{ value: 'sum_30d', label: 'Summe 30 Tage' },
|
||||||
|
{ value: 'count_7d', label: 'Anzahl 7 Tage' },
|
||||||
|
{ value: 'count_30d', label: 'Anzahl 30 Tage' },
|
||||||
|
{ value: 'min_30d', label: 'Minimum 30 Tage' },
|
||||||
|
{ value: 'max_30d', label: 'Maximum 30 Tage' }
|
||||||
|
]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadGoalTypes()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadGoalTypes = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const data = await api.listGoalTypeDefinitions()
|
||||||
|
setGoalTypes(data)
|
||||||
|
} catch (err) {
|
||||||
|
setError('Fehler beim Laden der Goal Types')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showToast = (message) => {
|
||||||
|
setToast(message)
|
||||||
|
setTimeout(() => setToast(null), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
setEditingType(null)
|
||||||
|
setFormData({
|
||||||
|
type_key: '',
|
||||||
|
label_de: '',
|
||||||
|
unit: '',
|
||||||
|
icon: '',
|
||||||
|
category: 'custom',
|
||||||
|
source_table: '',
|
||||||
|
source_column: '',
|
||||||
|
aggregation_method: 'latest',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
setShowForm(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (type) => {
|
||||||
|
setEditingType(type.id)
|
||||||
|
setFormData({
|
||||||
|
type_key: type.type_key,
|
||||||
|
label_de: type.label_de,
|
||||||
|
unit: type.unit,
|
||||||
|
icon: type.icon || '',
|
||||||
|
category: type.category || 'custom',
|
||||||
|
source_table: type.source_table || '',
|
||||||
|
source_column: type.source_column || '',
|
||||||
|
aggregation_method: type.aggregation_method || 'latest',
|
||||||
|
description: type.description || ''
|
||||||
|
})
|
||||||
|
setShowForm(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!formData.label_de || !formData.unit) {
|
||||||
|
setError('Bitte Label und Einheit ausfüllen')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingType) {
|
||||||
|
await api.updateGoalType(editingType, formData)
|
||||||
|
showToast('✓ Goal Type aktualisiert')
|
||||||
|
} else {
|
||||||
|
if (!formData.type_key) {
|
||||||
|
setError('Bitte eindeutigen Key angeben (z.B. meditation_minutes)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await api.createGoalType(formData)
|
||||||
|
showToast('✓ Goal Type erstellt')
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadGoalTypes()
|
||||||
|
setShowForm(false)
|
||||||
|
setError(null)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Fehler beim Speichern')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (typeId, typeName, isSystem) => {
|
||||||
|
if (isSystem) {
|
||||||
|
if (!confirm(`System Goal Type "${typeName}" deaktivieren? (Nicht löschbar)`)) return
|
||||||
|
} else {
|
||||||
|
if (!confirm(`Goal Type "${typeName}" wirklich löschen?`)) return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.deleteGoalType(typeId)
|
||||||
|
showToast('✓ Goal Type gelöscht/deaktiviert')
|
||||||
|
await loadGoalTypes()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Fehler beim Löschen')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<div style={{ textAlign: 'center', padding: 40 }}>
|
||||||
|
<div className="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<div className="page-header">
|
||||||
|
<h1><Database size={24} /> Goal Type Verwaltung</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="card" style={{ background: '#FEF2F2', border: '1px solid #FCA5A5', marginBottom: 16 }}>
|
||||||
|
<p style={{ color: '#DC2626', margin: 0 }}>{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toast && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
background: 'var(--accent)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '12px 20px',
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||||
|
zIndex: 1000
|
||||||
|
}}>
|
||||||
|
{toast}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="card" style={{ marginBottom: 16 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||||
|
<div>
|
||||||
|
<h2 style={{ margin: 0, marginBottom: 4 }}>Verfügbare Goal Types</h2>
|
||||||
|
<p style={{ fontSize: 13, color: 'var(--text2)', margin: 0 }}>
|
||||||
|
{goalTypes.length} Types registriert ({goalTypes.filter(t => t.is_system).length} System, {goalTypes.filter(t => !t.is_system).length} Custom)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button className="btn-primary" onClick={handleCreate}>
|
||||||
|
<Plus size={16} /> Neuer Type
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{goalTypes.map(type => (
|
||||||
|
<div
|
||||||
|
key={type.id}
|
||||||
|
className="card"
|
||||||
|
style={{
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
padding: 12,
|
||||||
|
border: type.is_system ? '2px solid var(--accent)' : '1px solid var(--border)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||||
|
<span style={{ fontSize: 20 }}>{type.icon || '📊'}</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>{type.label_de}</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
background: 'var(--surface)',
|
||||||
|
color: 'var(--text2)'
|
||||||
|
}}>
|
||||||
|
{type.unit}
|
||||||
|
</span>
|
||||||
|
{type.is_system && (
|
||||||
|
<span style={{
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
background: 'var(--accent)',
|
||||||
|
color: 'white'
|
||||||
|
}}>
|
||||||
|
SYSTEM
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!type.is_active && (
|
||||||
|
<span style={{
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
background: '#F59E0B',
|
||||||
|
color: 'white'
|
||||||
|
}}>
|
||||||
|
INAKTIV
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text2)' }}>
|
||||||
|
<strong>Key:</strong> {type.type_key}
|
||||||
|
{type.source_table && (
|
||||||
|
<>
|
||||||
|
{' | '}<strong>Quelle:</strong> {type.source_table}.{type.source_column}
|
||||||
|
{' | '}<strong>Methode:</strong> {type.aggregation_method}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type.calculation_formula && (
|
||||||
|
<>
|
||||||
|
{' | '}<strong>Formel:</strong> Komplex (JSON)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{type.description && (
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text2)', marginTop: 4, fontStyle: 'italic' }}>
|
||||||
|
{type.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={() => handleEdit(type)}
|
||||||
|
style={{ padding: '6px 12px' }}
|
||||||
|
>
|
||||||
|
<Pencil size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={() => handleDelete(type.id, type.label_de, type.is_system)}
|
||||||
|
style={{ padding: '6px 12px', color: type.is_system ? '#F59E0B' : '#DC2626' }}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Modal */}
|
||||||
|
{showForm && (
|
||||||
|
<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,
|
||||||
|
paddingTop: 40,
|
||||||
|
overflowY: 'auto'
|
||||||
|
}}>
|
||||||
|
<div className="card" style={{ maxWidth: 600, width: '100%', marginBottom: 40 }}>
|
||||||
|
<div className="card-title">
|
||||||
|
{editingType ? 'Goal Type bearbeiten' : 'Neuer Goal Type'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
{/* Type Key (nur bei Create) */}
|
||||||
|
{!editingType && (
|
||||||
|
<div>
|
||||||
|
<label className="form-label">
|
||||||
|
Eindeutiger Key * (z.B. meditation_minutes)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={formData.type_key}
|
||||||
|
onChange={e => setFormData(f => ({ ...f, type_key: e.target.value }))}
|
||||||
|
placeholder="snake_case verwenden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Label */}
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Label (Deutsch) *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={formData.label_de}
|
||||||
|
onChange={e => setFormData(f => ({ ...f, label_de: e.target.value }))}
|
||||||
|
placeholder="z.B. Meditation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Unit & Icon */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Einheit *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={formData.unit}
|
||||||
|
onChange={e => setFormData(f => ({ ...f, unit: e.target.value }))}
|
||||||
|
placeholder="z.B. min/Tag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Icon (Emoji)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={formData.icon}
|
||||||
|
onChange={e => setFormData(f => ({ ...f, icon: e.target.value }))}
|
||||||
|
placeholder="🧘"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category */}
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Kategorie</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={formData.category}
|
||||||
|
onChange={e => setFormData(f => ({ ...f, category: e.target.value }))}
|
||||||
|
>
|
||||||
|
{CATEGORIES.map(cat => (
|
||||||
|
<option key={cat} value={cat}>{cat}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Data Source */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Tabelle</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={formData.source_table}
|
||||||
|
onChange={e => setFormData(f => ({ ...f, source_table: e.target.value }))}
|
||||||
|
placeholder="z.B. meditation_log"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Spalte</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={formData.source_column}
|
||||||
|
onChange={e => setFormData(f => ({ ...f, source_column: e.target.value }))}
|
||||||
|
placeholder="z.B. duration_minutes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Aggregation Method */}
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Aggregationsmethode</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={formData.aggregation_method}
|
||||||
|
onChange={e => setFormData(f => ({ ...f, aggregation_method: e.target.value }))}
|
||||||
|
>
|
||||||
|
{AGGREGATION_METHODS.map(method => (
|
||||||
|
<option key={method.value} value={method.value}>{method.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Beschreibung (optional)</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '100%', minHeight: 60 }}
|
||||||
|
value={formData.description}
|
||||||
|
onChange={e => setFormData(f => ({ ...f, description: e.target.value }))}
|
||||||
|
placeholder="Kurze Erklärung..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
|
||||||
|
<button className="btn-primary" onClick={handleSave} style={{ flex: 1 }}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowForm(false)
|
||||||
|
setError(null)
|
||||||
|
}}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -44,21 +44,11 @@ const GOAL_MODES = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// Goal Type Definitions
|
|
||||||
const GOAL_TYPES = {
|
|
||||||
weight: { label: 'Gewicht', unit: 'kg', icon: '⚖️' },
|
|
||||||
body_fat: { label: 'Körperfett', unit: '%', icon: '📊' },
|
|
||||||
lean_mass: { label: 'Muskelmasse', unit: 'kg', icon: '💪' },
|
|
||||||
vo2max: { label: 'VO2Max', unit: 'ml/kg/min', icon: '🫁' },
|
|
||||||
strength: { label: 'Kraft', unit: 'kg', icon: '🏋️' },
|
|
||||||
flexibility: { label: 'Beweglichkeit', unit: 'cm', icon: '🤸' },
|
|
||||||
bp: { label: 'Blutdruck', unit: 'mmHg', icon: '❤️' },
|
|
||||||
rhr: { label: 'Ruhepuls', unit: 'bpm', icon: '💓' }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GoalsPage() {
|
export default function GoalsPage() {
|
||||||
const [goalMode, setGoalMode] = useState(null)
|
const [goalMode, setGoalMode] = useState(null)
|
||||||
const [goals, setGoals] = useState([])
|
const [goals, setGoals] = useState([])
|
||||||
|
const [goalTypes, setGoalTypes] = useState([]) // Dynamic from DB (Phase 1.5)
|
||||||
|
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 [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
@ -84,12 +74,28 @@ export default function GoalsPage() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const [modeData, goalsData] = await Promise.all([
|
const [modeData, goalsData, typesData] = await Promise.all([
|
||||||
api.getGoalMode(),
|
api.getGoalMode(),
|
||||||
api.listGoals()
|
api.listGoals(),
|
||||||
|
api.listGoalTypeDefinitions() // Phase 1.5: Load from DB
|
||||||
])
|
])
|
||||||
setGoalMode(modeData.goal_mode)
|
setGoalMode(modeData.goal_mode)
|
||||||
setGoals(goalsData)
|
setGoals(goalsData)
|
||||||
|
|
||||||
|
// Convert types array to map for quick lookup
|
||||||
|
const typesMap = {}
|
||||||
|
typesData.forEach(type => {
|
||||||
|
typesMap[type.type_key] = {
|
||||||
|
label: type.label_de,
|
||||||
|
unit: type.unit,
|
||||||
|
icon: type.icon || '📊',
|
||||||
|
category: type.category,
|
||||||
|
is_system: type.is_system
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setGoalTypes(typesData)
|
||||||
|
setGoalTypesMap(typesMap)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load goals:', err)
|
console.error('Failed to load goals:', err)
|
||||||
setError('Fehler beim Laden der Ziele')
|
setError('Fehler beim Laden der Ziele')
|
||||||
|
|
@ -116,11 +122,12 @@ export default function GoalsPage() {
|
||||||
|
|
||||||
const handleCreateGoal = () => {
|
const handleCreateGoal = () => {
|
||||||
setEditingGoal(null)
|
setEditingGoal(null)
|
||||||
|
const firstType = goalTypes.length > 0 ? goalTypes[0].type_key : 'weight'
|
||||||
setFormData({
|
setFormData({
|
||||||
goal_type: 'weight',
|
goal_type: firstType,
|
||||||
is_primary: goals.length === 0, // First goal is primary by default
|
is_primary: goals.length === 0, // First goal is primary by default
|
||||||
target_value: '',
|
target_value: '',
|
||||||
unit: GOAL_TYPES['weight'].unit,
|
unit: goalTypesMap[firstType]?.unit || 'kg',
|
||||||
target_date: '',
|
target_date: '',
|
||||||
name: '',
|
name: '',
|
||||||
description: ''
|
description: ''
|
||||||
|
|
@ -146,7 +153,7 @@ export default function GoalsPage() {
|
||||||
setFormData(f => ({
|
setFormData(f => ({
|
||||||
...f,
|
...f,
|
||||||
goal_type: type,
|
goal_type: type,
|
||||||
unit: GOAL_TYPES[type].unit
|
unit: goalTypesMap[type]?.unit || 'unit'
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,7 +311,7 @@ export default function GoalsPage() {
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
{goals.map(goal => {
|
{goals.map(goal => {
|
||||||
const typeInfo = GOAL_TYPES[goal.goal_type] || {}
|
const typeInfo = goalTypesMap[goal.goal_type] || { label: goal.goal_type, unit: '', icon: '📊' }
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={goal.id}
|
key={goal.id}
|
||||||
|
|
@ -489,9 +496,9 @@ export default function GoalsPage() {
|
||||||
value={formData.goal_type}
|
value={formData.goal_type}
|
||||||
onChange={e => handleGoalTypeChange(e.target.value)}
|
onChange={e => handleGoalTypeChange(e.target.value)}
|
||||||
>
|
>
|
||||||
{Object.entries(GOAL_TYPES).map(([key, info]) => (
|
{goalTypes.map(type => (
|
||||||
<option key={key} value={key}>
|
<option key={type.type_key} value={type.type_key}>
|
||||||
{info.icon} {info.label}
|
{type.icon} {type.label_de}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user