v9d Phase 2d: Vitals Module Refactoring (Baseline + Blood Pressure) #22
403
frontend/src/components/ProfileBuilder.jsx
Normal file
403
frontend/src/components/ProfileBuilder.jsx
Normal file
|
|
@ -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 <div className="spinner" />
|
||||||
|
|
||||||
|
const minReq = profile.rule_sets.minimum_requirements
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: 'var(--surface)', borderRadius: '12px', padding: '20px' }}>
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<h3 style={{ marginBottom: '8px' }}>
|
||||||
|
{trainingType.icon} {trainingType.name_de} - Profil konfigurieren
|
||||||
|
</h3>
|
||||||
|
<p style={{ fontSize: '13px', color: 'var(--text2)' }}>
|
||||||
|
Definiere Mindestanforderungen und Bewertungskriterien für diesen Trainingstyp
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Minimum Requirements */}
|
||||||
|
<div className="card" style={{ marginBottom: '16px' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => toggleSection('minReq')}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<h4 style={{ margin: 0 }}>Mindestanforderungen</h4>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '13px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={minReq.enabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
updateRuleSet('minimum_requirements', { enabled: e.target.checked })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Aktiviert
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{expandedSections.minReq ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expandedSections.minReq && minReq.enabled && (
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
{/* Strategy */}
|
||||||
|
<div style={{ marginBottom: '16px', paddingBottom: '16px', borderBottom: '1px solid var(--border)' }}>
|
||||||
|
<div className="form-row" style={{ marginBottom: '8px' }}>
|
||||||
|
<label className="form-label">Pass-Strategie</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={minReq.pass_strategy}
|
||||||
|
onChange={(e) => updateRuleSet('minimum_requirements', { pass_strategy: e.target.value })}
|
||||||
|
>
|
||||||
|
{PASS_STRATEGIES.map(s => (
|
||||||
|
<option key={s.value} value={s.value}>{s.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{minReq.pass_strategy === 'weighted_score' && (
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Threshold (Score 0-1)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-input"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
value={minReq.pass_threshold}
|
||||||
|
onChange={(e) => updateRuleSet('minimum_requirements', { pass_threshold: parseFloat(e.target.value) })}
|
||||||
|
/>
|
||||||
|
<span className="form-unit">{(minReq.pass_threshold * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rules */}
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<div style={{ fontWeight: '600', fontSize: '14px', marginBottom: '12px' }}>
|
||||||
|
Regeln ({minReq.rules.length})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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 (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
padding: '12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1.5fr 1fr 0.8fr 40px', gap: '8px', marginBottom: '8px' }}>
|
||||||
|
{/* Parameter */}
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={rule.parameter}
|
||||||
|
onChange={(e) => updateRule(idx, { parameter: e.target.value })}
|
||||||
|
style={{ fontSize: '13px' }}
|
||||||
|
>
|
||||||
|
{parameters.map(p => (
|
||||||
|
<option key={p.key} value={p.key}>
|
||||||
|
{p.name_de} ({p.unit || p.data_type})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Operator */}
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={rule.operator}
|
||||||
|
onChange={(e) => updateRule(idx, { operator: e.target.value })}
|
||||||
|
style={{ fontSize: '13px' }}
|
||||||
|
>
|
||||||
|
{availableOps.map(op => (
|
||||||
|
<option key={op.value} value={op.value}>{op.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Value */}
|
||||||
|
{rule.operator === 'between' ? (
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Min"
|
||||||
|
value={Array.isArray(rule.value) ? rule.value[0] : 0}
|
||||||
|
onChange={(e) => updateRule(idx, {
|
||||||
|
value: [parseFloat(e.target.value), Array.isArray(rule.value) ? rule.value[1] : 0]
|
||||||
|
})}
|
||||||
|
style={{ fontSize: '13px' }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Max"
|
||||||
|
value={Array.isArray(rule.value) ? rule.value[1] : 0}
|
||||||
|
onChange={(e) => updateRule(idx, {
|
||||||
|
value: [Array.isArray(rule.value) ? rule.value[0] : 0, parseFloat(e.target.value)]
|
||||||
|
})}
|
||||||
|
style={{ fontSize: '13px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-input"
|
||||||
|
value={rule.value}
|
||||||
|
onChange={(e) => updateRule(idx, { value: parseFloat(e.target.value) || 0 })}
|
||||||
|
style={{ fontSize: '13px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Weight */}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Gewicht"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value={rule.weight}
|
||||||
|
onChange={(e) => updateRule(idx, { weight: parseInt(e.target.value) || 1 })}
|
||||||
|
style={{ fontSize: '13px' }}
|
||||||
|
title="Gewichtung 1-10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Delete */}
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={() => removeRule(idx)}
|
||||||
|
style={{ padding: '6px', minWidth: 'auto' }}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reason */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Begründung (optional)"
|
||||||
|
value={rule.reason}
|
||||||
|
onChange={(e) => updateRule(idx, { reason: e.target.value })}
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Optional Checkbox */}
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px', marginTop: '4px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={rule.optional}
|
||||||
|
onChange={(e) => updateRule(idx, { optional: e.target.checked })}
|
||||||
|
/>
|
||||||
|
Optional (Regel wird übersprungen wenn Parameter fehlt)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary btn-full"
|
||||||
|
onClick={addRule}
|
||||||
|
style={{ marginTop: '8px' }}
|
||||||
|
>
|
||||||
|
<Plus size={14} /> Regel hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Other Rule Sets - Placeholder */}
|
||||||
|
<div className="card" style={{ marginBottom: '16px', padding: '12px', background: 'var(--surface2)' }}>
|
||||||
|
<div style={{ fontSize: '13px', color: 'var(--text2)' }}>
|
||||||
|
<strong>Weitere Dimensionen:</strong> Intensitätszonen, Training Effects, Periodization, Performance, Safety
|
||||||
|
<br />
|
||||||
|
<span style={{ fontSize: '12px' }}>
|
||||||
|
→ Aktuell nur JSON-Editor verfügbar. Visual Builder folgt in nächster Iteration.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div style={{ display: 'flex', gap: '12px', marginTop: '20px' }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
💾 {loading ? 'Speichern...' : 'Profil speichern'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
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 { api } from '../utils/api'
|
||||||
|
import ProfileBuilder from '../components/ProfileBuilder'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AdminTrainingTypesPage - CRUD for training types
|
* AdminTrainingTypesPage - CRUD for training types
|
||||||
|
|
@ -16,6 +17,8 @@ export default function AdminTrainingTypesPage() {
|
||||||
const [formData, setFormData] = useState(null)
|
const [formData, setFormData] = useState(null)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [editingProfileId, setEditingProfileId] = useState(null)
|
||||||
|
const [parameters, setParameters] = useState([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load()
|
load()
|
||||||
|
|
@ -25,10 +28,12 @@ export default function AdminTrainingTypesPage() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
Promise.all([
|
Promise.all([
|
||||||
api.adminListTrainingTypes(),
|
api.adminListTrainingTypes(),
|
||||||
api.getTrainingCategories()
|
api.getTrainingCategories(),
|
||||||
]).then(([typesData, catsData]) => {
|
api.getTrainingParameters()
|
||||||
|
]).then(([typesData, catsData, paramsData]) => {
|
||||||
setTypes(typesData)
|
setTypes(typesData)
|
||||||
setCategories(catsData)
|
setCategories(catsData)
|
||||||
|
setParameters(paramsData.parameters || [])
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('Failed to load training types:', 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
|
// Group by category
|
||||||
const grouped = {}
|
const grouped = {}
|
||||||
types.forEach(type => {
|
types.forEach(type => {
|
||||||
|
|
@ -162,6 +189,18 @@ export default function AdminTrainingTypesPage() {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Profile Builder */}
|
||||||
|
{editingProfileId && (
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<ProfileBuilder
|
||||||
|
trainingType={types.find(t => t.id === editingProfileId)}
|
||||||
|
parameters={parameters}
|
||||||
|
onSave={handleSaveProfile}
|
||||||
|
onCancel={cancelEditProfile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Edit form */}
|
{/* Edit form */}
|
||||||
{editingId && formData && (
|
{editingId && formData && (
|
||||||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||||
|
|
@ -336,6 +375,18 @@ export default function AdminTrainingTypesPage() {
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 13, fontWeight: 500 }}>
|
<div style={{ fontSize: 13, fontWeight: 500 }}>
|
||||||
{type.name_de} <span style={{ color: 'var(--text3)' }}>/ {type.name_en}</span>
|
{type.name_de} <span style={{ color: 'var(--text3)' }}>/ {type.name_en}</span>
|
||||||
|
{type.profile && (
|
||||||
|
<span style={{
|
||||||
|
marginLeft: 8,
|
||||||
|
padding: '2px 6px',
|
||||||
|
background: 'var(--accent)',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 10
|
||||||
|
}}>
|
||||||
|
✓ Profil
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{type.subcategory && (
|
{type.subcategory && (
|
||||||
<div style={{ fontSize: 11, color: 'var(--text3)' }}>
|
<div style={{ fontSize: 11, color: 'var(--text3)' }}>
|
||||||
|
|
@ -343,6 +394,19 @@ export default function AdminTrainingTypesPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => startEditProfile(type.id)}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: 6,
|
||||||
|
color: 'var(--accent)'
|
||||||
|
}}
|
||||||
|
title="Profil konfigurieren"
|
||||||
|
>
|
||||||
|
<Settings size={16} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => startEdit(type)}
|
onClick={() => startEdit(type)}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user