diff --git a/backend/migrations/055_training_category_parameter_seed_and_eav_backfill.sql b/backend/migrations/055_training_category_parameter_seed_and_eav_backfill.sql new file mode 100644 index 0000000..e5218b0 --- /dev/null +++ b/backend/migrations/055_training_category_parameter_seed_and_eav_backfill.sql @@ -0,0 +1,213 @@ +-- Migration 055: Seed training_category_parameter (all categories × parameters with activity_log source_field) +-- + idempotent backfill activity_log → activity_session_metrics (EAV) +-- Date: 2026-04-15 +-- SAFE: INSERT … ON CONFLICT DO NOTHING only; no DELETE/TRUNCATE on activity_log. +-- Agent guide: .claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md + +--1) Jede in training_types vorkommende Kategorie erhält alle aktiven Parameter mit source_field (Spalte in activity_log). +INSERT INTO training_category_parameter ( + training_category, + training_parameter_id, + sort_order, + required, + ui_group +) +SELECT + tc.training_category, + tp.id, + ROW_NUMBER() OVER ( + PARTITION BY tc.training_category + ORDER BY tp.category, tp.id + ), + false, + NULL +FROM ( + SELECT DISTINCT category AS training_category + FROM training_types + WHERE category IS NOT NULL AND trim(category) <> '' +) tc +CROSS JOIN training_parameters tp +WHERE tp.is_active = true + AND tp.source_field IS NOT NULL + AND trim(tp.source_field) <> '' +ON CONFLICT (training_category, training_parameter_id) DO NOTHING; + +-- 2) Backfill: activity_log-Spalten → EAV (nur wenn noch keine Zeile existiert) + +-- duration_min → integer +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT + a.id, + tp.id, + NULL, + ROUND(a.duration_min::numeric)::bigint, + NULL, + NULL, + NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'duration_min' AND tp.is_active = true +WHERE a.duration_min IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +-- distance_km → float +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, a.distance_km::double precision, NULL, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'distance_km' AND tp.is_active = true +WHERE a.distance_km IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +-- kcal_active +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, ROUND(a.kcal_active::numeric)::bigint, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'kcal_active' AND tp.is_active = true +WHERE a.kcal_active IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, ROUND(a.kcal_resting::numeric)::bigint, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'kcal_resting' AND tp.is_active = true +WHERE a.kcal_resting IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +-- hr_avg / hr_max → keys avg_hr, max_hr +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, ROUND(a.hr_avg::numeric)::bigint, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'avg_hr' AND tp.is_active = true +WHERE a.hr_avg IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, ROUND(a.hr_max::numeric)::bigint, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'max_hr' AND tp.is_active = true +WHERE a.hr_max IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +-- rpe +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, a.rpe::bigint, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'rpe' AND tp.is_active = true +WHERE a.rpe IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +-- min_hr (Spalte hr_min nach Migration 014) +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, a.hr_min, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'min_hr' AND tp.is_active = true +WHERE a.hr_min IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, a.pace_min_per_km::double precision, NULL, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'pace_min_per_km' AND tp.is_active = true +WHERE a.pace_min_per_km IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, a.cadence, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'cadence' AND tp.is_active = true +WHERE a.cadence IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, a.avg_power, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'avg_power' AND tp.is_active = true +WHERE a.avg_power IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, a.elevation_gain, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'elevation_gain' AND tp.is_active = true +WHERE a.elevation_gain IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, a.temperature_celsius::double precision, NULL, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'temperature_celsius' AND tp.is_active = true +WHERE a.temperature_celsius IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, NULL, a.humidity_percent, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'humidity_percent' AND tp.is_active = true +WHERE a.humidity_percent IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, a.avg_hr_percent::double precision, NULL, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'avg_hr_percent' AND tp.is_active = true +WHERE a.avg_hr_percent IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +INSERT INTO activity_session_metrics ( + activity_log_id, training_parameter_id, + value_num, value_int, value_text, value_bool, updated_at +) +SELECT a.id, tp.id, a.kcal_per_km::double precision, NULL, NULL, NULL, NOW() +FROM activity_log a +JOIN training_parameters tp ON tp.key = 'kcal_per_km' AND tp.is_active = true +WHERE a.kcal_per_km IS NOT NULL +ON CONFLICT (activity_log_id, training_parameter_id) DO NOTHING; + +DO $$ +BEGIN + RAISE NOTICE 'Migration 055: category parameter seed + EAV backfill from activity_log (no row deletes)'; +END $$; diff --git a/backend/routers/activity.py b/backend/routers/activity.py index ec7f014..492ba24 100644 --- a/backend/routers/activity.py +++ b/backend/routers/activity.py @@ -35,18 +35,18 @@ logger = logging.getLogger(__name__) def list_activity( limit: int = Query(200, ge=1, le=50_000), days: Optional[int] = Query(None, ge=1, le=4000, description="Nur Einträge mit date >= HEUTE − days (Kalendertage)"), - x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth), ): """Get activity entries for current profile. Optional *days* filter by calendar window (not the same as *limit*).""" - pid = get_pid(x_profile_id) + # Immer das Profil der gültigen Session (X-Profile-Id wird hier nicht verwendet). + pid = str(session["profile_id"]) with get_db() as conn: cur = get_cursor(conn) # Issue #31: Apply global quality filter (profile from DB = saved level) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) profile = r2d(cur.fetchone()) - quality_filter = get_quality_filter_sql(profile) + quality_filter = get_quality_filter_sql(profile or {}) if days is not None: cur.execute( @@ -229,13 +229,23 @@ def get_activity_session( @router.get("/stats") -def activity_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): +def activity_stats(session: dict = Depends(require_auth)): """Get activity statistics (last 30 entries).""" - pid = get_pid(x_profile_id) + pid = str(session["profile_id"]) with get_db() as conn: cur = get_cursor(conn) + cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) + profile = r2d(cur.fetchone()) + quality_filter = get_quality_filter_sql(profile or {}) cur.execute( - "SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date DESC LIMIT 30", (pid,)) + f""" + SELECT * FROM activity_log + WHERE profile_id=%s {quality_filter} + ORDER BY date DESC + LIMIT 30 + """, + (pid,), + ) rows = [r2d(r) for r in cur.fetchall()] if not rows: return {"count":0,"total_kcal":0,"total_min":0,"by_type":{}} total_kcal=sum(float(r.get('kcal_active') or 0) for r in rows) diff --git a/backend/routers/admin_activity_attribute_profiles.py b/backend/routers/admin_activity_attribute_profiles.py index d955956..34e1ef9 100644 --- a/backend/routers/admin_activity_attribute_profiles.py +++ b/backend/routers/admin_activity_attribute_profiles.py @@ -30,6 +30,18 @@ class TypeParameterCreate(BaseModel): ui_group: Optional[str] = Field(None, max_length=50) +class CategoryParameterUpdate(BaseModel): + sort_order: Optional[int] = None + required: Optional[bool] = None + ui_group: Optional[str] = Field(None, max_length=50) + + +class TypeParameterUpdate(BaseModel): + sort_order: Optional[int] = None + required: Optional[bool] = None + ui_group: Optional[str] = Field(None, max_length=50) + + @router.get("/training-category-parameters") def admin_list_category_parameters( category: Optional[str] = Query(None, description="Filter: training_types.category"), @@ -91,6 +103,41 @@ def admin_add_category_parameter( return {"id": new_id} +@router.put("/training-category-parameters/{link_id}") +def admin_update_category_parameter( + link_id: int, + body: CategoryParameterUpdate, + session: dict = Depends(require_admin), +): + patch = body.model_dump(exclude_unset=True) + if not patch: + raise HTTPException(400, "Keine Felder zum Aktualisieren") + cols: list[str] = [] + vals: list = [] + if "sort_order" in patch: + cols.append("sort_order = %s") + vals.append(patch["sort_order"]) + if "required" in patch: + cols.append("required = %s") + vals.append(patch["required"]) + if "ui_group" in patch: + cols.append("ui_group = %s") + vals.append(patch["ui_group"].strip() if patch["ui_group"] else None) + if not cols: + raise HTTPException(400, "Keine Felder zum Aktualisieren") + vals.append(link_id) + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + f"UPDATE training_category_parameter SET {', '.join(cols)} WHERE id = %s RETURNING id", + vals, + ) + if not cur.fetchone(): + raise HTTPException(404, "Eintrag nicht gefunden") + conn.commit() + return {"ok": True, "id": link_id} + + @router.delete("/training-category-parameters/{link_id}") def admin_delete_category_parameter( link_id: int, @@ -167,6 +214,41 @@ def admin_add_type_parameter( return {"id": new_id} +@router.put("/training-type-parameters/{link_id}") +def admin_update_type_parameter( + link_id: int, + body: TypeParameterUpdate, + session: dict = Depends(require_admin), +): + patch = body.model_dump(exclude_unset=True) + if not patch: + raise HTTPException(400, "Keine Felder zum Aktualisieren") + cols: list[str] = [] + vals: list = [] + if "sort_order" in patch: + cols.append("sort_order = %s") + vals.append(patch["sort_order"]) + if "required" in patch: + cols.append("required = %s") + vals.append(patch["required"]) + if "ui_group" in patch: + cols.append("ui_group = %s") + vals.append(patch["ui_group"].strip() if patch["ui_group"] else None) + if not cols: + raise HTTPException(400, "Keine Felder zum Aktualisieren") + vals.append(link_id) + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + f"UPDATE training_type_parameter SET {', '.join(cols)} WHERE id = %s RETURNING id", + vals, + ) + if not cur.fetchone(): + raise HTTPException(404, "Eintrag nicht gefunden") + conn.commit() + return {"ok": True, "id": link_id} + + @router.delete("/training-type-parameters/{link_id}") def admin_delete_type_parameter( link_id: int, diff --git a/frontend/src/pages/AdminActivityAttributeProfilesPage.jsx b/frontend/src/pages/AdminActivityAttributeProfilesPage.jsx index 3fae2b4..f0e21cd 100644 --- a/frontend/src/pages/AdminActivityAttributeProfilesPage.jsx +++ b/frontend/src/pages/AdminActivityAttributeProfilesPage.jsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react' import { Link } from 'react-router-dom' -import { Plus, Trash2, Save, RefreshCw } from 'lucide-react' +import { Plus, Trash2, Save, RefreshCw, Pencil } from 'lucide-react' import { api } from '../utils/api' const PARAM_GROUP = ['physical', 'physiological', 'subjective', 'environmental', 'performance'] @@ -18,6 +18,7 @@ const emptyParamForm = () => ({ }) export default function AdminActivityAttributeProfilesPage() { + const [tab, setTab] = useState('catalog') const [params, setParams] = useState([]) const [includeInactive, setIncludeInactive] = useState(false) const [catMeta, setCatMeta] = useState({}) @@ -28,6 +29,7 @@ export default function AdminActivityAttributeProfilesPage() { const [showParamForm, setShowParamForm] = useState(false) const [paramForm, setParamForm] = useState(emptyParamForm()) + const [editParam, setEditParam] = useState(null) const [selCategory, setSelCategory] = useState('cardio') const [catLinks, setCatLinks] = useState([]) @@ -37,6 +39,8 @@ export default function AdminActivityAttributeProfilesPage() { 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([]) @@ -46,6 +50,8 @@ export default function AdminActivityAttributeProfilesPage() { required: '', ui_group: '', }) + const [editingTypeId, setEditingTypeId] = useState(null) + const [typeDraft, setTypeDraft] = useState({ sort_order: '', required: '', ui_group: '' }) const showToast = (msg) => { setToast(msg) @@ -108,6 +114,11 @@ export default function AdminActivityAttributeProfilesPage() { 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()) { @@ -135,6 +146,28 @@ export default function AdminActivityAttributeProfilesPage() { } } + 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 { @@ -168,6 +201,21 @@ export default function AdminActivityAttributeProfilesPage() { } } + 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) @@ -179,7 +227,8 @@ export default function AdminActivityAttributeProfilesPage() { 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, + required: + typeAdd.required === '' ? null : typeAdd.required === 'true' || typeAdd.required === true, ui_group: typeAdd.ui_group.trim() || null, } try { @@ -192,10 +241,28 @@ export default function AdminActivityAttributeProfilesPage() { } } - const categoryKeys = - Object.keys(catMeta).length > 0 - ? Object.keys(catMeta).sort() - : ['cardio', 'strength', 'hiit', 'martial_arts', 'mobility', 'recovery', 'other'] + 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 ( @@ -213,11 +280,30 @@ export default function AdminActivityAttributeProfilesPage() {

Session-Metriken (EAV)

-

- Messgrößen-Katalog und Zuordnung zu Kategorie (Basis) bzw.{' '} - Trainingstyp (Zusatz/Override). Nutzer sehen die Felder beim Bearbeiten einer - Aktivität, wenn der Eintrag passend kategorisiert ist. -

+ +
+ Hinweise + +
{toast && (
)} -
-
- Parameter-Katalog - - - -
- - {showParamForm && ( -
+ {[ + ['catalog', 'Katalog'], + ['category', 'Kategorie'], + ['type', 'Trainingstyp'], + ].map(([id, label]) => ( + - -
-
- )} + {label} + + ))} +
-
- - - - - - - - - - - - {params.map((r) => ( - - - - - - - - - ))} - -
IDkeyDETypaktiv -
{r.id} - {r.key} - {r.name_de} - {r.data_type} · {r.category} - {r.is_active === false ? 'nein' : 'ja'} - {r.is_active !== false && ( + {tab === 'catalog' && ( +
+
+ Parameter-Katalog + + + +
+ + {showParamForm && ( +
+
+ + setParamForm((f) => ({ ...f, key: e.target.value }))} + /> +
+
+ + setParamForm((f) => ({ ...f, name_de: e.target.value }))} + /> + setParamForm((f) => ({ ...f, name_en: e.target.value }))} + /> +
+
+ + + +
+
+ + setParamForm((f) => ({ ...f, unit: e.target.value }))} + /> + setParamForm((f) => ({ ...f, source_field: e.target.value }))} + /> +
+
+ + +
+
+ )} + + {editParam && ( +
+
+ Bearbeiten: {editParam.key} +
+
+ + setEditParam((p) => ({ ...p, name_de: e.target.value }))} + /> + setEditParam((p) => ({ ...p, name_en: e.target.value }))} + /> +
+
+ + + +
+
+ + setEditParam((p) => ({ ...p, unit: e.target.value }))} + /> + setEditParam((p) => ({ ...p, source_field: e.target.value }))} + /> +
+ +
+ + +
+
+ )} + +
+ + + + + + + + + + + + {params.map((r) => ( + + + + + + + + + ))} + +
IDkeyDETypaktiv +
{r.id} + {r.key} + {r.name_de} + {r.data_type} · {r.category} + {r.is_active === false ? 'nein' : 'ja'} +
+ + {r.is_active !== false && ( + + )} +
+
+
+
+ )} + + {tab === 'category' && ( +
+
Zuordnung: Trainings-Kategorie
+
+ + +
+
+
+ + +
+
+ + setCatAdd((a) => ({ ...a, sort_order: e.target.value }))} + /> +
+ +
+ + setCatAdd((a) => ({ ...a, ui_group: e.target.value }))} + /> +
+ +
+
    + {catLinks.map((l) => ( +
  • + {editingCatId === l.id ? ( +
    + + {l.parameter_key} · {l.parameter_name_de} + +
    + + setCatDraft((d) => ({ ...d, sort_order: e.target.value }))} + /> +
    + + setCatDraft((d) => ({ ...d, ui_group: e.target.value }))} + /> + + +
    + ) : ( +
    + + {l.parameter_key} · {l.parameter_name_de} · sort {l.sort_order} + {l.required ? ' · Pflicht' : ''} + {l.ui_group ? ` · ${l.ui_group}` : ''} + +
    + - )} -
-
- - -
-
Zuordnung: Trainings-Kategorie
-
- - +
-
-
- + )} + + {tab === 'type' && ( +
+
Zuordnung: Trainingstyp (Zusatz / Override)
+
+
-
- - setCatAdd((a) => ({ ...a, sort_order: e.target.value }))} - /> +
+
+ + +
+
+ + setTypeAdd((a) => ({ ...a, sort_order: e.target.value }))} + /> +
+
+ + +
+
+ + setTypeAdd((a) => ({ ...a, ui_group: e.target.value }))} + /> +
+
- -
- - setCatAdd((a) => ({ ...a, ui_group: e.target.value }))} - /> -
- -
-
    - {catLinks.map((l) => ( -
  • - - {l.parameter_key} · {l.parameter_name_de} · sort {l.sort_order} - {l.required ? ' · Pflicht' : ''} - {l.ui_group ? ` · ${l.ui_group}` : ''} - - -
  • - ))} -
-
- -
-
Zuordnung: Trainingstyp (Zusatz / Override)
-
- - setTypeDraft((d) => ({ ...d, sort_order: e.target.value }))} + /> +
+ + setTypeDraft((d) => ({ ...d, ui_group: e.target.value }))} + /> + + +
+ ) : ( +
+ + {l.parameter_key} · sort {l.sort_order ?? '—'} · Pflicht{' '} + {l.required === null || l.required === undefined + ? '—' + : l.required + ? 'ja' + : 'nein'} + +
+ + +
+
+ )} + ))} - +
-
-
- - -
-
- - setTypeAdd((a) => ({ ...a, sort_order: e.target.value }))} - /> -
-
- - -
-
- - setTypeAdd((a) => ({ ...a, ui_group: e.target.value }))} - /> -
- -
- -
+ )}
) } diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index f09d55c..89cfb02 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -326,11 +326,15 @@ export const api = { `/admin/training-category-parameters${category ? `?category=${encodeURIComponent(category)}` : ''}`, ), adminAddTrainingCategoryParameter: (d) => req('/admin/training-category-parameters', json(d)), + adminUpdateTrainingCategoryParameter: (id, d) => + req(`/admin/training-category-parameters/${id}`, jput(d)), adminDeleteTrainingCategoryParameter: (id) => req(`/admin/training-category-parameters/${id}`, { method: 'DELETE' }), adminListTrainingTypeParameters: (trainingTypeId) => req(`/admin/training-type-parameters?training_type_id=${encodeURIComponent(trainingTypeId)}`), adminAddTrainingTypeParameter: (d) => req('/admin/training-type-parameters', json(d)), + adminUpdateTrainingTypeParameter: (id, d) => + req(`/admin/training-type-parameters/${id}`, jput(d)), adminDeleteTrainingTypeParameter: (id) => req(`/admin/training-type-parameters/${id}`, { method: 'DELETE' }),