Goalsystem V1 #50

Merged
Lars merged 51 commits from develop into main 2026-03-27 17:40:51 +01:00
3 changed files with 162 additions and 19 deletions
Showing only changes of commit 2c978bf948 - Show all commits

View File

@ -508,6 +508,108 @@ def _calculate_norm_category(test_type: str, value: float, unit: str) -> Optiona
# Goal Type Definitions (Phase 1.5 - Flexible Goal System)
# ============================================================================
@router.get("/schema-info")
def get_schema_info(session: dict = Depends(require_auth)):
"""
Get available tables and columns for goal type creation.
Admin-only endpoint for building custom goal types.
Returns structure with descriptions for UX guidance.
"""
pid = session['profile_id']
# Check admin role
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT role FROM profiles WHERE id = %s", (pid,))
profile = cur.fetchone()
if not profile or profile['role'] != 'admin':
raise HTTPException(status_code=403, detail="Admin-Zugriff erforderlich")
# Define relevant tables with descriptions
# Only include tables that make sense for goal tracking
schema = {
"weight_log": {
"description": "Gewichtsverlauf",
"columns": {
"weight": {"type": "DECIMAL", "description": "Körpergewicht in kg"}
}
},
"caliper_log": {
"description": "Caliper-Messungen (Hautfalten)",
"columns": {
"body_fat_pct": {"type": "DECIMAL", "description": "Körperfettanteil in %"},
"sum_mm": {"type": "DECIMAL", "description": "Summe Hautfalten in mm"}
}
},
"circumference_log": {
"description": "Umfangsmessungen",
"columns": {
"c_neck": {"type": "DECIMAL", "description": "Nackenumfang in cm"},
"c_chest": {"type": "DECIMAL", "description": "Brustumfang in cm"},
"c_waist": {"type": "DECIMAL", "description": "Taillenumfang in cm"},
"c_hips": {"type": "DECIMAL", "description": "Hüftumfang in cm"},
"c_thigh_l": {"type": "DECIMAL", "description": "Oberschenkel links in cm"},
"c_thigh_r": {"type": "DECIMAL", "description": "Oberschenkel rechts in cm"},
"c_calf_l": {"type": "DECIMAL", "description": "Wade links in cm"},
"c_calf_r": {"type": "DECIMAL", "description": "Wade rechts in cm"},
"c_bicep_l": {"type": "DECIMAL", "description": "Bizeps links in cm"},
"c_bicep_r": {"type": "DECIMAL", "description": "Bizeps rechts in cm"}
}
},
"activity_log": {
"description": "Trainingseinheiten",
"columns": {
"id": {"type": "UUID", "description": "ID (für Zählung von Einheiten)"},
"duration_minutes": {"type": "INTEGER", "description": "Trainingsdauer in Minuten"},
"perceived_exertion": {"type": "INTEGER", "description": "Belastungsempfinden (1-10)"},
"quality_rating": {"type": "INTEGER", "description": "Qualitätsbewertung (1-10)"}
}
},
"nutrition_log": {
"description": "Ernährungstagebuch",
"columns": {
"calories": {"type": "INTEGER", "description": "Kalorien in kcal"},
"protein_g": {"type": "DECIMAL", "description": "Protein in g"},
"carbs_g": {"type": "DECIMAL", "description": "Kohlenhydrate in g"},
"fat_g": {"type": "DECIMAL", "description": "Fett in g"}
}
},
"sleep_log": {
"description": "Schlafprotokoll",
"columns": {
"total_minutes": {"type": "INTEGER", "description": "Gesamtschlafdauer in Minuten"}
}
},
"vitals_baseline": {
"description": "Vitalwerte (morgens)",
"columns": {
"resting_hr": {"type": "INTEGER", "description": "Ruhepuls in bpm"},
"hrv_rmssd": {"type": "INTEGER", "description": "Herzratenvariabilität (RMSSD) in ms"},
"vo2_max": {"type": "DECIMAL", "description": "VO2 Max in ml/kg/min"},
"spo2": {"type": "INTEGER", "description": "Sauerstoffsättigung in %"},
"respiratory_rate": {"type": "INTEGER", "description": "Atemfrequenz pro Minute"}
}
},
"blood_pressure_log": {
"description": "Blutdruckmessungen",
"columns": {
"systolic": {"type": "INTEGER", "description": "Systolischer Blutdruck in mmHg"},
"diastolic": {"type": "INTEGER", "description": "Diastolischer Blutdruck in mmHg"},
"pulse": {"type": "INTEGER", "description": "Puls in bpm"}
}
},
"rest_days": {
"description": "Ruhetage",
"columns": {
"id": {"type": "UUID", "description": "ID (für Zählung von Ruhetagen)"}
}
}
}
return schema
@router.get("/goal-types")
def list_goal_type_definitions(session: dict = Depends(require_auth)):
"""

View File

@ -4,6 +4,7 @@ import { api } from '../utils/api'
export default function AdminGoalTypesPage() {
const [goalTypes, setGoalTypes] = useState([])
const [schemaInfo, setSchemaInfo] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showForm, setShowForm] = useState(false)
@ -42,9 +43,14 @@ export default function AdminGoalTypesPage() {
setLoading(true)
setError(null)
try {
const data = await api.listGoalTypeDefinitions()
console.log('[DEBUG] Loaded goal types:', data)
setGoalTypes(data || [])
const [typesData, schema] = await Promise.all([
api.listGoalTypeDefinitions(),
api.getSchemaInfo()
])
console.log('[DEBUG] Loaded goal types:', typesData)
console.log('[DEBUG] Loaded schema info:', schema)
setGoalTypes(typesData || [])
setSchemaInfo(schema || {})
} catch (err) {
console.error('[ERROR] Failed to load goal types:', err)
setError(`Fehler beim Laden der Goal Types: ${err.message || err.toString()}`)
@ -375,25 +381,59 @@ export default function AdminGoalTypesPage() {
<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"
/>
{schemaInfo ? (
<select
className="form-input"
style={{ width: '100%' }}
value={formData.source_table}
onChange={e => setFormData(f => ({ ...f, source_table: e.target.value, source_column: '' }))}
>
<option value="">-- Optional --</option>
{Object.entries(schemaInfo).map(([table, info]) => (
<option key={table} value={table} title={info.description}>
{table} - {info.description}
</option>
))}
</select>
) : (
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.source_table}
onChange={e => setFormData(f => ({ ...f, source_table: e.target.value }))}
placeholder="Lade Schema..."
disabled
/>
)}
</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"
/>
{schemaInfo && formData.source_table && schemaInfo[formData.source_table] ? (
<select
className="form-input"
style={{ width: '100%' }}
value={formData.source_column}
onChange={e => setFormData(f => ({ ...f, source_column: e.target.value }))}
>
<option value="">-- Wählen --</option>
{Object.entries(schemaInfo[formData.source_table].columns).map(([col, info]) => (
<option key={col} value={col} title={info.description}>
{col} - {info.description}
</option>
))}
</select>
) : (
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.source_column}
onChange={e => setFormData(f => ({ ...f, source_column: e.target.value }))}
placeholder={formData.source_table ? "Spalte wählen..." : "Erst Tabelle wählen"}
disabled={!formData.source_table}
/>
)}
</div>
</div>

View File

@ -344,6 +344,7 @@ export const api = {
createGoalType: (d) => req('/goals/goal-types', json(d)),
updateGoalType: (id,d) => req(`/goals/goal-types/${id}`, jput(d)),
deleteGoalType: (id) => req(`/goals/goal-types/${id}`, {method:'DELETE'}),
getSchemaInfo: () => req('/goals/schema-info'),
// Training Phases
listTrainingPhases: () => req('/goals/phases'),