mitai-jinkendo/frontend/src/pages/AdminActivityAttributeProfilesPage.jsx
Lars 196b6c5cf1
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / pytest-backend (push) Successful in 8s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s
feat: Add update functionality for training category and type parameters
- Introduced new endpoints for updating training category and type parameters in the backend.
- Added corresponding update functions in the frontend API utility.
- Enhanced the Admin Activity Attribute Profiles page to support editing and saving changes for category and type parameters.
- Implemented state management for editing parameters and improved error handling during updates.
2026-04-14 12:26:52 +02:00

913 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom'
import { Plus, Trash2, Save, RefreshCw, Pencil } from 'lucide-react'
import { api } from '../utils/api'
const PARAM_GROUP = ['physical', 'physiological', 'subjective', 'environmental', 'performance']
const DATA_TYPES = ['integer', 'float', 'string', 'boolean']
const emptyParamForm = () => ({
key: '',
name_de: '',
name_en: '',
category: 'physical',
data_type: 'float',
unit: '',
source_field: '',
is_active: true,
})
export default function AdminActivityAttributeProfilesPage() {
const [tab, setTab] = useState('catalog')
const [params, setParams] = useState([])
const [includeInactive, setIncludeInactive] = useState(false)
const [catMeta, setCatMeta] = useState({})
const [flatTypes, setFlatTypes] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [toast, setToast] = useState(null)
const [showParamForm, setShowParamForm] = useState(false)
const [paramForm, setParamForm] = useState(emptyParamForm())
const [editParam, setEditParam] = useState(null)
const [selCategory, setSelCategory] = useState('cardio')
const [catLinks, setCatLinks] = useState([])
const [catAdd, setCatAdd] = useState({
training_parameter_id: '',
sort_order: 0,
required: false,
ui_group: '',
})
const [editingCatId, setEditingCatId] = useState(null)
const [catDraft, setCatDraft] = useState({ sort_order: 0, required: false, ui_group: '' })
const [selTypeId, setSelTypeId] = useState('')
const [typeLinks, setTypeLinks] = useState([])
const [typeAdd, setTypeAdd] = useState({
training_parameter_id: '',
sort_order: '',
required: '',
ui_group: '',
})
const [editingTypeId, setEditingTypeId] = useState(null)
const [typeDraft, setTypeDraft] = useState({ sort_order: '', required: '', ui_group: '' })
const showToast = (msg) => {
setToast(msg)
setTimeout(() => setToast(null), 2800)
}
const refreshCatalog = useCallback(async () => {
setLoading(true)
setError(null)
try {
const [p, cats, flat] = await Promise.all([
api.adminListTrainingParameters(includeInactive),
api.getTrainingCategories(),
api.listTrainingTypesFlat(),
])
setParams(Array.isArray(p) ? p : [])
setCatMeta(cats && typeof cats === 'object' ? cats : {})
setFlatTypes(Array.isArray(flat) ? flat : [])
setSelTypeId((prev) => (prev ? prev : flat?.length ? String(flat[0].id) : ''))
} catch (e) {
setError(e.message || 'Laden fehlgeschlagen')
} finally {
setLoading(false)
}
}, [includeInactive])
useEffect(() => {
refreshCatalog()
}, [refreshCatalog])
const loadCatLinks = useCallback(async () => {
try {
const data = await api.adminListTrainingCategoryParameters(selCategory)
setCatLinks(Array.isArray(data) ? data : [])
} catch (e) {
setError(e.message || 'Kategorie-Zuordnungen')
}
}, [selCategory])
useEffect(() => {
loadCatLinks()
}, [loadCatLinks])
const loadTypeLinks = useCallback(async () => {
if (!selTypeId) {
setTypeLinks([])
return
}
try {
const data = await api.adminListTrainingTypeParameters(Number(selTypeId))
setTypeLinks(Array.isArray(data) ? data : [])
} catch (e) {
setError(e.message || 'Typ-Zuordnungen')
}
}, [selTypeId])
useEffect(() => {
loadTypeLinks()
}, [loadTypeLinks])
const activeParams = params.filter((p) => p.is_active !== false)
const categoryKeys =
Object.keys(catMeta).length > 0
? Object.keys(catMeta).sort()
: ['cardio', 'strength', 'hiit', 'martial_arts', 'mobility', 'recovery', 'other']
const saveNewParameter = async () => {
setError(null)
if (!paramForm.key.trim() || !paramForm.name_de.trim() || !paramForm.name_en.trim()) {
setError('key, name_de und name_en sind Pflicht.')
return
}
try {
await api.adminCreateTrainingParameter({
key: paramForm.key.trim().toLowerCase(),
name_de: paramForm.name_de.trim(),
name_en: paramForm.name_en.trim(),
category: paramForm.category,
data_type: paramForm.data_type,
unit: paramForm.unit.trim() || null,
source_field: paramForm.source_field.trim() || null,
is_active: paramForm.is_active,
validation_rules: {},
})
showToast('Parameter angelegt')
setShowParamForm(false)
setParamForm(emptyParamForm())
await refreshCatalog()
} catch (e) {
setError(e.message || 'Speichern fehlgeschlagen')
}
}
const saveEditedParameter = async () => {
if (!editParam) return
setError(null)
try {
await api.adminUpdateTrainingParameter(editParam.id, {
name_de: editParam.name_de.trim(),
name_en: editParam.name_en.trim(),
category: editParam.category,
data_type: editParam.data_type,
unit: editParam.unit?.trim() || null,
source_field: editParam.source_field?.trim() || null,
is_active: !!editParam.is_active,
validation_rules: editParam.validation_rules || {},
})
showToast('Parameter gespeichert')
setEditParam(null)
await refreshCatalog()
} catch (e) {
setError(e.message || 'Update fehlgeschlagen')
}
}
const deactivateParameter = async (id) => {
if (!confirm('Parameter deaktivieren? (Bestehende EAV-Zeilen bleiben erhalten.)')) return
try {
await api.adminDeleteTrainingParameter(id)
showToast('Deaktiviert')
await refreshCatalog()
} catch (e) {
setError(e.message || 'Löschen fehlgeschlagen')
}
}
const addCatLink = async () => {
const pid = parseInt(catAdd.training_parameter_id, 10)
if (!pid) {
setError('Parameter-ID wählen')
return
}
try {
await api.adminAddTrainingCategoryParameter({
training_category: selCategory,
training_parameter_id: pid,
sort_order: Number(catAdd.sort_order) || 0,
required: !!catAdd.required,
ui_group: catAdd.ui_group.trim() || null,
})
showToast('Zuordnung gespeichert')
setCatAdd({ training_parameter_id: '', sort_order: 0, required: false, ui_group: '' })
await loadCatLinks()
} catch (e) {
setError(e.message || 'Konflikt oder ungültige Daten')
}
}
const saveCatLink = async (linkId) => {
try {
await api.adminUpdateTrainingCategoryParameter(linkId, {
sort_order: Number(catDraft.sort_order) || 0,
required: !!catDraft.required,
ui_group: catDraft.ui_group.trim() || null,
})
showToast('Kategorie-Zuordnung aktualisiert')
setEditingCatId(null)
await loadCatLinks()
} catch (e) {
setError(e.message || 'Update fehlgeschlagen')
}
}
const addTypeLink = async () => {
const tid = Number(selTypeId)
const pid = parseInt(typeAdd.training_parameter_id, 10)
if (!tid || !pid) {
setError('Trainingstyp und Parameter wählen')
return
}
const body = {
training_type_id: tid,
training_parameter_id: pid,
sort_order: typeAdd.sort_order === '' ? null : Number(typeAdd.sort_order),
required:
typeAdd.required === '' ? null : typeAdd.required === 'true' || typeAdd.required === true,
ui_group: typeAdd.ui_group.trim() || null,
}
try {
await api.adminAddTrainingTypeParameter(body)
showToast('Typ-Zuordnung gespeichert')
setTypeAdd({ training_parameter_id: '', sort_order: '', required: '', ui_group: '' })
await loadTypeLinks()
} catch (e) {
setError(e.message || 'Konflikt oder ungültige Daten')
}
}
const saveTypeLink = async (linkId) => {
try {
const payload = {}
if (typeDraft.sort_order !== '' && typeDraft.sort_order != null) {
payload.sort_order = Number(typeDraft.sort_order)
} else {
payload.sort_order = null
}
if (typeDraft.required === '' || typeDraft.required == null) {
payload.required = null
} else {
payload.required = typeDraft.required === true || typeDraft.required === 'true'
}
payload.ui_group = typeDraft.ui_group.trim() || null
await api.adminUpdateTrainingTypeParameter(linkId, payload)
showToast('Typ-Zuordnung aktualisiert')
setEditingTypeId(null)
await loadTypeLinks()
} catch (e) {
setError(e.message || 'Update fehlgeschlagen')
}
}
if (loading && !params.length) {
return (
<div className="card section-gap">
<div className="spinner" /> Lade
</div>
)
}
return (
<div className="capture-page">
<div style={{ marginBottom: 12 }}>
<Link to="/admin/g/training" className="text-link" style={{ fontSize: 13 }}>
Training (Hub)
</Link>
</div>
<h1 className="page-title">Session-Metriken (EAV)</h1>
<div
className="card section-gap"
style={{ background: 'var(--surface2)', border: '1px solid var(--border)', fontSize: 13, lineHeight: 1.55 }}
>
<strong>Hinweise</strong>
<ul style={{ margin: '8px 0 0', paddingLeft: 18 }}>
<li>
<strong>Daten:</strong> Offizielle Migrationen löschen keine Zeilen in <code>activity_log</code>. Leere
Tabellen nach Deploy deuten auf neues DB-Volume, manuelles SQL oder falsche Umgebung hin vor Prod immer{' '}
<code>pg_dump</code>.
</li>
<li>
<strong>Doppelzuordnung:</strong> Dieselbe Metrik darf <em>eine</em> Zeile pro Kategorie und <em>eine</em>{' '}
pro Trainingstyp haben (Unique-Constraint). Wenn dieselbe Metrik in <strong>Kategorie</strong> und{' '}
<strong>Trainingstyp</strong> vorkommt, überschreibt die <strong>Typ-Zeile</strong> sort/required/ui_group
(Merge in Layer&nbsp;1) kein Datenfehler.
</li>
<li>
Nach Migration <strong>055</strong> werden Standard-Parameter allen Kategorien zugeordnet und vorhandene{' '}
<code>activity_log</code>-Spalten idempotent nach EAV gespiegelt (sofern noch keine EAV-Zeile existiert).
</li>
</ul>
</div>
{toast && (
<div
className="card"
style={{ background: 'var(--accent-light)', color: 'var(--accent-dark)', marginBottom: 12 }}
>
{toast}
</div>
)}
{error && (
<div
className="card"
style={{ background: '#FCEBEB', color: '#D85A30', marginBottom: 12, fontSize: 14 }}
>
{error}
<button type="button" className="btn btn-secondary" style={{ marginLeft: 8 }} onClick={() => setError(null)}>
OK
</button>
</div>
)}
<div className="tabs" style={{ marginBottom: 16, overflowX: 'auto' }}>
{[
['catalog', 'Katalog'],
['category', 'Kategorie'],
['type', 'Trainingstyp'],
].map(([id, label]) => (
<button
key={id}
type="button"
className={'tab' + (tab === id ? ' active' : '')}
onClick={() => setTab(id)}
>
{label}
</button>
))}
</div>
{tab === 'catalog' && (
<div className="card section-gap">
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<span>Parameter-Katalog</span>
<label style={{ fontSize: 12, fontWeight: 400, display: 'flex', alignItems: 'center', gap: 6 }}>
<input
type="checkbox"
checked={includeInactive}
onChange={(e) => setIncludeInactive(e.target.checked)}
/>
Inaktive
</label>
<button type="button" className="btn btn-secondary" onClick={() => refreshCatalog()}>
<RefreshCw size={14} /> Neu laden
</button>
<button type="button" className="btn btn-primary" onClick={() => setShowParamForm((v) => !v)}>
<Plus size={14} /> Neu
</button>
</div>
{showParamForm && (
<div
style={{
border: '1px solid var(--border)',
borderRadius: 8,
padding: 12,
marginBottom: 12,
background: 'var(--surface2)',
}}
>
<div className="form-row">
<label className="form-label">key</label>
<input
className="form-input"
placeholder="z. B. avg_power"
value={paramForm.key}
onChange={(e) => setParamForm((f) => ({ ...f, key: e.target.value }))}
/>
</div>
<div className="form-row">
<label className="form-label">name_de / name_en</label>
<input
className="form-input"
value={paramForm.name_de}
onChange={(e) => setParamForm((f) => ({ ...f, name_de: e.target.value }))}
/>
<input
className="form-input"
value={paramForm.name_en}
onChange={(e) => setParamForm((f) => ({ ...f, name_en: e.target.value }))}
/>
</div>
<div className="form-row">
<label className="form-label">Gruppe / Datentyp</label>
<select
className="form-input"
value={paramForm.category}
onChange={(e) => setParamForm((f) => ({ ...f, category: e.target.value }))}
>
{PARAM_GROUP.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
<select
className="form-input"
value={paramForm.data_type}
onChange={(e) => setParamForm((f) => ({ ...f, data_type: e.target.value }))}
>
{DATA_TYPES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
<div className="form-row">
<label className="form-label">Einheit / source_field</label>
<input
className="form-input"
value={paramForm.unit}
onChange={(e) => setParamForm((f) => ({ ...f, unit: e.target.value }))}
/>
<input
className="form-input"
value={paramForm.source_field}
onChange={(e) => setParamForm((f) => ({ ...f, source_field: e.target.value }))}
/>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<button type="button" className="btn btn-primary" onClick={saveNewParameter}>
<Save size={14} /> Anlegen
</button>
<button type="button" className="btn btn-secondary" onClick={() => setShowParamForm(false)}>
Abbrechen
</button>
</div>
</div>
)}
{editParam && (
<div
style={{
border: '1px solid var(--accent)',
borderRadius: 8,
padding: 12,
marginBottom: 12,
}}
>
<div className="card-title" style={{ fontSize: 14 }}>
Bearbeiten: <code>{editParam.key}</code>
</div>
<div className="form-row">
<label className="form-label">name_de / name_en</label>
<input
className="form-input"
value={editParam.name_de || ''}
onChange={(e) => setEditParam((p) => ({ ...p, name_de: e.target.value }))}
/>
<input
className="form-input"
value={editParam.name_en || ''}
onChange={(e) => setEditParam((p) => ({ ...p, name_en: e.target.value }))}
/>
</div>
<div className="form-row">
<label className="form-label">Gruppe / Typ</label>
<select
className="form-input"
value={editParam.category}
onChange={(e) => setEditParam((p) => ({ ...p, category: e.target.value }))}
>
{PARAM_GROUP.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
<select
className="form-input"
value={editParam.data_type}
onChange={(e) => setEditParam((p) => ({ ...p, data_type: e.target.value }))}
>
{DATA_TYPES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
<div className="form-row">
<label className="form-label">Einheit / source_field</label>
<input
className="form-input"
value={editParam.unit || ''}
onChange={(e) => setEditParam((p) => ({ ...p, unit: e.target.value }))}
/>
<input
className="form-input"
value={editParam.source_field || ''}
onChange={(e) => setEditParam((p) => ({ ...p, source_field: e.target.value }))}
/>
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, marginBottom: 8 }}>
<input
type="checkbox"
checked={!!editParam.is_active}
onChange={(e) => setEditParam((p) => ({ ...p, is_active: e.target.checked }))}
/>
aktiv
</label>
<div style={{ display: 'flex', gap: 8 }}>
<button type="button" className="btn btn-primary" onClick={saveEditedParameter}>
<Save size={14} /> Speichern
</button>
<button type="button" className="btn btn-secondary" onClick={() => setEditParam(null)}>
Abbrechen
</button>
</div>
</div>
)}
<div style={{ overflowX: 'auto' }}>
<table className="data-table" style={{ width: '100%', fontSize: 13 }}>
<thead>
<tr>
<th>ID</th>
<th>key</th>
<th>DE</th>
<th>Typ</th>
<th>aktiv</th>
<th />
</tr>
</thead>
<tbody>
{params.map((r) => (
<tr key={r.id}>
<td>{r.id}</td>
<td>
<code>{r.key}</code>
</td>
<td>{r.name_de}</td>
<td>
{r.data_type} · {r.category}
</td>
<td>{r.is_active === false ? 'nein' : 'ja'}</td>
<td>
<div style={{ display: 'flex', gap: 4 }}>
<button
type="button"
className="btn btn-secondary"
style={{ padding: '4px 8px' }}
onClick={() => setEditParam({ ...r })}
>
<Pencil size={14} />
</button>
{r.is_active !== false && (
<button
type="button"
className="btn btn-danger"
style={{ padding: '4px 8px' }}
onClick={() => deactivateParameter(r.id)}
>
<Trash2 size={14} />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{tab === 'category' && (
<div className="card section-gap">
<div className="card-title">Zuordnung: Trainings-Kategorie</div>
<div className="form-row">
<label className="form-label">Kategorie</label>
<select
className="form-input"
value={selCategory}
onChange={(e) => setSelCategory(e.target.value)}
style={{ maxWidth: 280 }}
>
{categoryKeys.map((k) => (
<option key={k} value={k}>
{catMeta[k]?.name_de || k}
</option>
))}
</select>
</div>
<div className="form-row" style={{ alignItems: 'flex-end', flexWrap: 'wrap', gap: 8 }}>
<div style={{ flex: 1, minWidth: 200 }}>
<label className="form-label">Parameter</label>
<select
className="form-input"
value={catAdd.training_parameter_id}
onChange={(e) => setCatAdd((a) => ({ ...a, training_parameter_id: e.target.value }))}
>
<option value=""> wählen </option>
{activeParams.map((p) => (
<option key={p.id} value={p.id}>
{p.id} · {p.key} ({p.name_de})
</option>
))}
</select>
</div>
<div>
<label className="form-label">sort</label>
<input
type="number"
className="form-input"
style={{ width: 80 }}
value={catAdd.sort_order}
onChange={(e) => setCatAdd((a) => ({ ...a, sort_order: e.target.value }))}
/>
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
<input
type="checkbox"
checked={catAdd.required}
onChange={(e) => setCatAdd((a) => ({ ...a, required: e.target.checked }))}
/>
Pflicht
</label>
<div>
<label className="form-label">ui_group</label>
<input
className="form-input"
style={{ width: 120 }}
value={catAdd.ui_group}
onChange={(e) => setCatAdd((a) => ({ ...a, ui_group: e.target.value }))}
/>
</div>
<button type="button" className="btn btn-primary" onClick={addCatLink}>
<Plus size={14} /> Hinzufügen
</button>
</div>
<ul style={{ listStyle: 'none', padding: 0, marginTop: 12 }}>
{catLinks.map((l) => (
<li
key={l.id}
style={{
padding: '10px 0',
borderBottom: '1px solid var(--border)',
fontSize: 13,
}}
>
{editingCatId === l.id ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'flex-end' }}>
<span style={{ flex: '1 1 200px' }}>
<strong>{l.parameter_key}</strong> · {l.parameter_name_de}
</span>
<div>
<label className="form-label">sort</label>
<input
type="number"
className="form-input"
style={{ width: 72 }}
value={catDraft.sort_order}
onChange={(e) => setCatDraft((d) => ({ ...d, sort_order: e.target.value }))}
/>
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input
type="checkbox"
checked={!!catDraft.required}
onChange={(e) => setCatDraft((d) => ({ ...d, required: e.target.checked }))}
/>
Pflicht
</label>
<input
className="form-input"
style={{ width: 100 }}
placeholder="ui_group"
value={catDraft.ui_group}
onChange={(e) => setCatDraft((d) => ({ ...d, ui_group: e.target.value }))}
/>
<button type="button" className="btn btn-primary" onClick={() => saveCatLink(l.id)}>
Speichern
</button>
<button type="button" className="btn btn-secondary" onClick={() => setEditingCatId(null)}>
Abbrechen
</button>
</div>
) : (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
<span>
<strong>{l.parameter_key}</strong> · {l.parameter_name_de} · sort {l.sort_order}
{l.required ? ' · Pflicht' : ''}
{l.ui_group ? ` · ${l.ui_group}` : ''}
</span>
<div style={{ display: 'flex', gap: 4 }}>
<button
type="button"
className="btn btn-secondary"
style={{ padding: '4px 8px' }}
onClick={() => {
setEditingCatId(l.id)
setCatDraft({
sort_order: l.sort_order,
required: !!l.required,
ui_group: l.ui_group || '',
})
}}
>
<Pencil size={14} />
</button>
<button
type="button"
className="btn btn-danger"
style={{ padding: '4px 8px' }}
onClick={async () => {
if (!confirm('Zuordnung entfernen?')) return
await api.adminDeleteTrainingCategoryParameter(l.id)
await loadCatLinks()
showToast('Entfernt')
}}
>
<Trash2 size={14} />
</button>
</div>
</div>
)}
</li>
))}
</ul>
</div>
)}
{tab === 'type' && (
<div className="card section-gap">
<div className="card-title">Zuordnung: Trainingstyp (Zusatz / Override)</div>
<div className="form-row">
<label className="form-label">Trainingstyp</label>
<select
className="form-input"
value={selTypeId}
onChange={(e) => setSelTypeId(e.target.value)}
style={{ maxWidth: 420 }}
>
{flatTypes.map((t) => (
<option key={t.id} value={t.id}>
{t.id} · {t.name_de} ({t.category})
</option>
))}
</select>
</div>
<div className="form-row" style={{ alignItems: 'flex-end', flexWrap: 'wrap', gap: 8 }}>
<div style={{ flex: 1, minWidth: 200 }}>
<label className="form-label">Parameter</label>
<select
className="form-input"
value={typeAdd.training_parameter_id}
onChange={(e) => setTypeAdd((a) => ({ ...a, training_parameter_id: e.target.value }))}
>
<option value=""> wählen </option>
{activeParams.map((p) => (
<option key={p.id} value={p.id}>
{p.id} · {p.key}
</option>
))}
</select>
</div>
<div>
<label className="form-label">sort (leer=Erben)</label>
<input
type="number"
className="form-input"
style={{ width: 80 }}
value={typeAdd.sort_order}
onChange={(e) => setTypeAdd((a) => ({ ...a, sort_order: e.target.value }))}
/>
</div>
<div>
<label className="form-label">Pflicht (leer=Erben)</label>
<select
className="form-input"
style={{ width: 100 }}
value={typeAdd.required}
onChange={(e) => setTypeAdd((a) => ({ ...a, required: e.target.value }))}
>
<option value=""></option>
<option value="true">ja</option>
<option value="false">nein</option>
</select>
</div>
<div>
<label className="form-label">ui_group</label>
<input
className="form-input"
style={{ width: 120 }}
value={typeAdd.ui_group}
onChange={(e) => setTypeAdd((a) => ({ ...a, ui_group: e.target.value }))}
/>
</div>
<button type="button" className="btn btn-primary" onClick={addTypeLink}>
<Plus size={14} /> Hinzufügen
</button>
</div>
<ul style={{ listStyle: 'none', padding: 0, marginTop: 12 }}>
{typeLinks.map((l) => (
<li
key={l.id}
style={{
padding: '10px 0',
borderBottom: '1px solid var(--border)',
fontSize: 13,
}}
>
{editingTypeId === l.id ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'flex-end' }}>
<span style={{ flex: '1 1 200px' }}>
<strong>{l.parameter_key}</strong>
</span>
<div>
<label className="form-label">sort</label>
<input
type="number"
className="form-input"
style={{ width: 72 }}
value={typeDraft.sort_order}
onChange={(e) => setTypeDraft((d) => ({ ...d, sort_order: e.target.value }))}
/>
</div>
<select
className="form-input"
style={{ width: 100 }}
value={typeDraft.required}
onChange={(e) => setTypeDraft((d) => ({ ...d, required: e.target.value }))}
>
<option value="">Erben</option>
<option value="true">ja</option>
<option value="false">nein</option>
</select>
<input
className="form-input"
style={{ width: 100 }}
placeholder="ui_group"
value={typeDraft.ui_group}
onChange={(e) => setTypeDraft((d) => ({ ...d, ui_group: e.target.value }))}
/>
<button type="button" className="btn btn-primary" onClick={() => saveTypeLink(l.id)}>
Speichern
</button>
<button type="button" className="btn btn-secondary" onClick={() => setEditingTypeId(null)}>
Abbrechen
</button>
</div>
) : (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
<span>
<strong>{l.parameter_key}</strong> · sort {l.sort_order ?? ''} · Pflicht{' '}
{l.required === null || l.required === undefined
? '—'
: l.required
? 'ja'
: 'nein'}
</span>
<div style={{ display: 'flex', gap: 4 }}>
<button
type="button"
className="btn btn-secondary"
style={{ padding: '4px 8px' }}
onClick={() => {
setEditingTypeId(l.id)
setTypeDraft({
sort_order: l.sort_order ?? '',
required:
l.required === null || l.required === undefined
? ''
: l.required
? 'true'
: 'false',
ui_group: l.ui_group || '',
})
}}
>
<Pencil size={14} />
</button>
<button
type="button"
className="btn btn-danger"
style={{ padding: '4px 8px' }}
onClick={async () => {
if (!confirm('Zuordnung entfernen?')) return
await api.adminDeleteTrainingTypeParameter(l.id)
await loadTypeLinks()
showToast('Entfernt')
}}
>
<Trash2 size={14} />
</button>
</div>
</div>
)}
</li>
))}
</ul>
</div>
)}
</div>
)
}