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 ( +
+
+ {/* Parameter */} + + + {/* Operator */} + + + {/* Value */} + {rule.operator === 'between' ? ( +
+ updateRule(idx, { + value: [parseFloat(e.target.value), Array.isArray(rule.value) ? rule.value[1] : 0] + })} + style={{ fontSize: '13px' }} + /> + updateRule(idx, { + value: [Array.isArray(rule.value) ? rule.value[0] : 0, parseFloat(e.target.value)] + })} + style={{ fontSize: '13px' }} + /> +
+ ) : ( + updateRule(idx, { value: parseFloat(e.target.value) || 0 })} + style={{ fontSize: '13px' }} + /> + )} + + {/* Weight */} + updateRule(idx, { weight: parseInt(e.target.value) || 1 })} + style={{ fontSize: '13px' }} + title="Gewichtung 1-10" + /> + + {/* Delete */} + +
+ + {/* Reason */} + updateRule(idx, { reason: e.target.value })} + style={{ fontSize: '12px' }} + /> + + {/* Optional Checkbox */} + +
+ ) + })} + + +
+
+ )} +
+ + {/* 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() {
)}
+