diff --git a/frontend/src/components/ProfileBuilder.jsx b/frontend/src/components/ProfileBuilder.jsx
new file mode 100644
index 0000000..8624e32
--- /dev/null
+++ b/frontend/src/components/ProfileBuilder.jsx
@@ -0,0 +1,403 @@
+import { useState, useEffect } from 'react'
+import { Trash2, Plus, ChevronDown, ChevronUp } from 'lucide-react'
+import '../app.css'
+
+const OPERATORS = [
+ { value: 'gte', label: '≥ Größer gleich', types: ['integer', 'float'] },
+ { value: 'lte', label: '≤ Kleiner gleich', types: ['integer', 'float'] },
+ { value: 'gt', label: '> Größer', types: ['integer', 'float'] },
+ { value: 'lt', label: '< Kleiner', types: ['integer', 'float'] },
+ { value: 'eq', label: '= Gleich', types: ['integer', 'float', 'string'] },
+ { value: 'neq', label: '≠ Ungleich', types: ['integer', 'float', 'string'] },
+ { value: 'between', label: '⟷ Zwischen', types: ['integer', 'float'] },
+]
+
+const PASS_STRATEGIES = [
+ { value: 'weighted_score', label: 'Gewichteter Score' },
+ { value: 'all_must_pass', label: 'Alle müssen erfüllt sein' },
+ { value: 'at_least_n', label: 'Mindestens N Regeln' },
+]
+
+export default function ProfileBuilder({ trainingType, onSave, onCancel, parameters }) {
+ const [profile, setProfile] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [expandedSections, setExpandedSections] = useState({ minReq: true })
+
+ useEffect(() => {
+ // Initialize or load existing profile
+ if (trainingType.profile) {
+ setProfile(trainingType.profile)
+ } else {
+ // Create empty profile structure
+ setProfile({
+ version: '1.0',
+ name: `${trainingType.name_de} (Profil)`,
+ description: '',
+ rule_sets: {
+ minimum_requirements: {
+ enabled: true,
+ pass_strategy: 'weighted_score',
+ pass_threshold: 0.6,
+ rules: []
+ },
+ intensity_zones: {
+ enabled: false,
+ zones: []
+ },
+ training_effects: {
+ enabled: false,
+ default_effects: {
+ primary_abilities: [],
+ secondary_abilities: []
+ }
+ },
+ periodization: {
+ enabled: false,
+ frequency: {
+ per_week_optimal: 3,
+ per_week_max: 5
+ },
+ recovery: {
+ min_hours_between: 24
+ }
+ },
+ performance_indicators: {
+ enabled: false
+ },
+ safety: {
+ enabled: false,
+ warnings: []
+ }
+ }
+ })
+ }
+ }, [trainingType])
+
+ const toggleSection = (section) => {
+ setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }))
+ }
+
+ const updateRuleSet = (key, updates) => {
+ setProfile(prev => ({
+ ...prev,
+ rule_sets: {
+ ...prev.rule_sets,
+ [key]: {
+ ...prev.rule_sets[key],
+ ...updates
+ }
+ }
+ }))
+ }
+
+ const addRule = () => {
+ const newRule = {
+ parameter: parameters[0]?.key || 'duration_min',
+ operator: 'gte',
+ value: 0,
+ weight: 3,
+ optional: false,
+ reason: ''
+ }
+
+ setProfile(prev => ({
+ ...prev,
+ rule_sets: {
+ ...prev.rule_sets,
+ minimum_requirements: {
+ ...prev.rule_sets.minimum_requirements,
+ rules: [...prev.rule_sets.minimum_requirements.rules, newRule]
+ }
+ }
+ }))
+ }
+
+ const updateRule = (index, updates) => {
+ setProfile(prev => {
+ const rules = [...prev.rule_sets.minimum_requirements.rules]
+ rules[index] = { ...rules[index], ...updates }
+ return {
+ ...prev,
+ rule_sets: {
+ ...prev.rule_sets,
+ minimum_requirements: {
+ ...prev.rule_sets.minimum_requirements,
+ rules
+ }
+ }
+ }
+ })
+ }
+
+ const removeRule = (index) => {
+ setProfile(prev => ({
+ ...prev,
+ rule_sets: {
+ ...prev.rule_sets,
+ minimum_requirements: {
+ ...prev.rule_sets.minimum_requirements,
+ rules: prev.rule_sets.minimum_requirements.rules.filter((_, i) => i !== index)
+ }
+ }
+ }))
+ }
+
+ const handleSave = async () => {
+ setLoading(true)
+ try {
+ await onSave(profile)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ if (!profile) return
+
+ const minReq = profile.rule_sets.minimum_requirements
+
+ return (
+
+
+
+ {trainingType.icon} {trainingType.name_de} - Profil konfigurieren
+
+
+ Definiere Mindestanforderungen und Bewertungskriterien für diesen Trainingstyp
+
+
+
+ {/* Minimum Requirements */}
+
+
toggleSection('minReq')}
+ >
+
+
Mindestanforderungen
+
+
+ {expandedSections.minReq ?
:
}
+
+
+ {expandedSections.minReq && minReq.enabled && (
+
+ {/* Strategy */}
+
+
+
+
+
+
+ {minReq.pass_strategy === 'weighted_score' && (
+
+
+ updateRuleSet('minimum_requirements', { pass_threshold: parseFloat(e.target.value) })}
+ />
+ {(minReq.pass_threshold * 100).toFixed(0)}%
+
+ )}
+
+
+ {/* Rules */}
+
+
+ Regeln ({minReq.rules.length})
+
+
+ {minReq.rules.map((rule, idx) => {
+ const param = parameters.find(p => p.key === rule.parameter)
+ const availableOps = OPERATORS.filter(op =>
+ param ? op.types.includes(param.data_type) : true
+ )
+
+ return (
+
+ )
+ })}
+
+
+
+
+ )}
+
+
+ {/* Other Rule Sets - Placeholder */}
+
+
+ Weitere Dimensionen: Intensitätszonen, Training Effects, Periodization, Performance, Safety
+
+
+ → Aktuell nur JSON-Editor verfügbar. Visual Builder folgt in nächster Iteration.
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/AdminTrainingTypesPage.jsx b/frontend/src/pages/AdminTrainingTypesPage.jsx
index f693b37..77b3bd8 100644
--- a/frontend/src/pages/AdminTrainingTypesPage.jsx
+++ b/frontend/src/pages/AdminTrainingTypesPage.jsx
@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
-import { Pencil, Trash2, Plus, Save, X, ArrowLeft } from 'lucide-react'
+import { Pencil, Trash2, Plus, Save, X, ArrowLeft, Settings } from 'lucide-react'
import { api } from '../utils/api'
+import ProfileBuilder from '../components/ProfileBuilder'
/**
* AdminTrainingTypesPage - CRUD for training types
@@ -16,6 +17,8 @@ export default function AdminTrainingTypesPage() {
const [formData, setFormData] = useState(null)
const [error, setError] = useState(null)
const [saving, setSaving] = useState(false)
+ const [editingProfileId, setEditingProfileId] = useState(null)
+ const [parameters, setParameters] = useState([])
useEffect(() => {
load()
@@ -25,10 +28,12 @@ export default function AdminTrainingTypesPage() {
setLoading(true)
Promise.all([
api.adminListTrainingTypes(),
- api.getTrainingCategories()
- ]).then(([typesData, catsData]) => {
+ api.getTrainingCategories(),
+ api.getTrainingParameters()
+ ]).then(([typesData, catsData, paramsData]) => {
setTypes(typesData)
setCategories(catsData)
+ setParameters(paramsData.parameters || [])
setLoading(false)
}).catch(err => {
console.error('Failed to load training types:', err)
@@ -109,6 +114,28 @@ export default function AdminTrainingTypesPage() {
}
}
+ const startEditProfile = (typeId) => {
+ setEditingProfileId(typeId)
+ setEditingId(null) // Close type editor if open
+ setFormData(null)
+ }
+
+ const cancelEditProfile = () => {
+ setEditingProfileId(null)
+ }
+
+ const handleSaveProfile = async (profile) => {
+ try {
+ await api.adminUpdateTrainingType(editingProfileId, { profile })
+ await load()
+ setEditingProfileId(null)
+ alert('✓ Profil gespeichert!')
+ } catch (err) {
+ alert('Speichern fehlgeschlagen: ' + err.message)
+ throw err
+ }
+ }
+
// Group by category
const grouped = {}
types.forEach(type => {
@@ -162,6 +189,18 @@ export default function AdminTrainingTypesPage() {
)}
+ {/* Profile Builder */}
+ {editingProfileId && (
+
+
t.id === editingProfileId)}
+ parameters={parameters}
+ onSave={handleSaveProfile}
+ onCancel={cancelEditProfile}
+ />
+
+ )}
+
{/* Edit form */}
{editingId && formData && (
@@ -336,6 +375,18 @@ export default function AdminTrainingTypesPage() {
{type.name_de} / {type.name_en}
+ {type.profile && (
+
+ ✓ Profil
+
+ )}
{type.subcategory && (
@@ -343,6 +394,19 @@ export default function AdminTrainingTypesPage() {
)}
+