feat: enhance exercise mapping and filtering capabilities
- Added support for style direction mappings in the backend, allowing for improved categorization of exercises. - Introduced a new function to normalize property synonyms, enhancing the mapping of exercise properties. - Updated the exercise catalog assignment logic to include style directions, ensuring proper database entries. - Enhanced the ExercisesListPage with new filtering options for style directions, improving user experience and search capabilities.
This commit is contained in:
parent
76098f5244
commit
025b161d2f
|
|
@ -518,6 +518,29 @@ def _upsert_exercise(mapped: dict, reimport: bool, created_by: int) -> Optional[
|
||||||
return ex_id
|
return ex_id
|
||||||
|
|
||||||
|
|
||||||
|
def _find_skill_id_by_label(cur, label: str) -> Optional[int]:
|
||||||
|
"""Katalog-Skill anhand Wiki-Label; exakt, ohne Leerzeichen, zuletzt Teilstring."""
|
||||||
|
if not label or not str(label).strip():
|
||||||
|
return None
|
||||||
|
raw = str(label).strip()
|
||||||
|
cur.execute("SELECT id FROM skills WHERE TRIM(name) ILIKE %s", (raw,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
return row["id"]
|
||||||
|
comp = "".join(raw.split())
|
||||||
|
if len(comp) >= 3:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id FROM skills WHERE regexp_replace(TRIM(name), E'\\s+', '', 'g') ILIKE %s",
|
||||||
|
(comp,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
return row["id"]
|
||||||
|
cur.execute("SELECT id FROM skills WHERE name ILIKE %s LIMIT 1", (f"%{raw}%",))
|
||||||
|
row = cur.fetchone()
|
||||||
|
return row["id"] if row else None
|
||||||
|
|
||||||
|
|
||||||
def _assign_exercise_catalogs(cur, conn, exercise_id: int, mapped: dict):
|
def _assign_exercise_catalogs(cur, conn, exercise_id: int, mapped: dict):
|
||||||
"""Weist M:N Katalog-Zuordnungen für eine importierte Übung zu."""
|
"""Weist M:N Katalog-Zuordnungen für eine importierte Übung zu."""
|
||||||
|
|
||||||
|
|
@ -535,16 +558,18 @@ def _assign_exercise_catalogs(cur, conn, exercise_id: int, mapped: dict):
|
||||||
else:
|
else:
|
||||||
logger.warning("Focus Area '%s' nicht im Katalog gefunden", name)
|
logger.warning("Focus Area '%s' nicht im Katalog gefunden", name)
|
||||||
|
|
||||||
# Style Directions
|
# Stilrichtungen
|
||||||
for name in mapped.get("style_names", []):
|
for idx, name in enumerate(mapped.get("style_names", [])):
|
||||||
cur.execute("SELECT id FROM style_directions WHERE name ILIKE %s", (name,))
|
cur.execute("SELECT id FROM style_directions WHERE name ILIKE %s", (name.strip(),))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if row:
|
if row:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""INSERT INTO exercise_training_styles (exercise_id, style_direction_id)
|
"""INSERT INTO exercise_style_directions (exercise_id, style_direction_id, is_primary)
|
||||||
VALUES (%s, %s) ON CONFLICT DO NOTHING""",
|
VALUES (%s, %s, %s) ON CONFLICT (exercise_id, style_direction_id) DO NOTHING""",
|
||||||
(exercise_id, row['id'])
|
(exercise_id, row["id"], idx == 0),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.warning("Stilrichtung '%s' nicht im Katalog gefunden", name)
|
||||||
|
|
||||||
# Target Groups
|
# Target Groups
|
||||||
for name in mapped.get("target_group_names", []):
|
for name in mapped.get("target_group_names", []):
|
||||||
|
|
@ -557,16 +582,7 @@ def _assign_exercise_catalogs(cur, conn, exercise_id: int, mapped: dict):
|
||||||
(exercise_id, row['id'])
|
(exercise_id, row['id'])
|
||||||
)
|
)
|
||||||
|
|
||||||
# Skills
|
# Fähigkeiten: nur über _assign_exercise_skills (Levels + is_primary), nicht doppelt hier
|
||||||
for name in mapped.get("skill_names", []):
|
|
||||||
cur.execute("SELECT id FROM skills WHERE name ILIKE %s", (name,))
|
|
||||||
row = cur.fetchone()
|
|
||||||
if row:
|
|
||||||
cur.execute(
|
|
||||||
"""INSERT INTO exercise_skills (exercise_id, skill_id)
|
|
||||||
VALUES (%s, %s) ON CONFLICT DO NOTHING""",
|
|
||||||
(exercise_id, row['id'])
|
|
||||||
)
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
@ -574,10 +590,9 @@ def _assign_exercise_catalogs(cur, conn, exercise_id: int, mapped: dict):
|
||||||
def _assign_exercise_skills(cur, conn, exercise_id: int, skill_assignments: list):
|
def _assign_exercise_skills(cur, conn, exercise_id: int, skill_assignments: list):
|
||||||
"""Weist Skill-Zuordnungen mit Levels einer Übung zu."""
|
"""Weist Skill-Zuordnungen mit Levels einer Übung zu."""
|
||||||
for assignment in skill_assignments:
|
for assignment in skill_assignments:
|
||||||
cur.execute("SELECT id FROM skills WHERE name ILIKE %s", (assignment["skill_name"],))
|
sid = _find_skill_id_by_label(cur, assignment.get("skill_name") or "")
|
||||||
row = cur.fetchone()
|
if not sid:
|
||||||
if not row:
|
logger.warning("Skill '%s' nicht im Katalog gefunden", assignment.get("skill_name"))
|
||||||
logger.warning("Skill '%s' nicht im Katalog gefunden", assignment["skill_name"])
|
|
||||||
continue
|
continue
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""INSERT INTO exercise_skills
|
"""INSERT INTO exercise_skills
|
||||||
|
|
@ -587,7 +602,8 @@ def _assign_exercise_skills(cur, conn, exercise_id: int, skill_assignments: list
|
||||||
target_level = EXCLUDED.target_level,
|
target_level = EXCLUDED.target_level,
|
||||||
is_primary = EXCLUDED.is_primary""",
|
is_primary = EXCLUDED.is_primary""",
|
||||||
(
|
(
|
||||||
exercise_id, row['id'],
|
exercise_id,
|
||||||
|
sid,
|
||||||
assignment.get("target_level"),
|
assignment.get("target_level"),
|
||||||
assignment.get("required_level"),
|
assignment.get("required_level"),
|
||||||
assignment.get("intensity"),
|
assignment.get("intensity"),
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ EXERCISE_PROPERTY_MAP = {
|
||||||
"Zielgruppe": "target_group_names",
|
"Zielgruppe": "target_group_names",
|
||||||
"Altersgruppe": "age_group_names",
|
"Altersgruppe": "age_group_names",
|
||||||
"Trainingsmethode": "method_names", # Wiki-Seitenname z.B. "Plyometrisches_Training"
|
"Trainingsmethode": "method_names", # Wiki-Seitenname z.B. "Plyometrisches_Training"
|
||||||
|
"Stilrichtung": "style_names", # z. B. Shotokan; siehe EXERCISE_PROPERTY_SYNONYM_TO_TARGET
|
||||||
# Fähigkeiten (als Namen + Level)
|
# Fähigkeiten (als Namen + Level)
|
||||||
"PrimaryCapability": "skill_names", # Skill-Namen (können mehrere sein)
|
"PrimaryCapability": "skill_names", # Skill-Namen (können mehrere sein)
|
||||||
"CapabilityLevel": "skill_levels_raw", # Integer-Levels ["3", "2"] → aufbau, grundlagen
|
"CapabilityLevel": "skill_levels_raw", # Integer-Levels ["3", "2"] → aufbau, grundlagen
|
||||||
|
|
@ -171,6 +172,49 @@ def map_capability_level(level_str: str) -> str:
|
||||||
return CAPABILITY_LEVEL_MAP.get(level_str.strip(), "basis")
|
return CAPABILITY_LEVEL_MAP.get(level_str.strip(), "basis")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# SMW-Property-Label → Mapper-Zielfeld (Werte wie in EXERCISE_PROPERTY_MAP) #
|
||||||
|
# browse_subject liefert Anzeigenamen, nicht zwingend interne Property-IDs. #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def _norm_prop_synonym(name: str) -> str:
|
||||||
|
s = (name or "").strip().lower()
|
||||||
|
for a, b in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")):
|
||||||
|
s = s.replace(a, b)
|
||||||
|
return "".join(c for c in s if c.isalnum())
|
||||||
|
|
||||||
|
|
||||||
|
# alternative Labels → Zielfeld-Name (gleiche Strings wie Werte in EXERCISE_PROPERTY_MAP)
|
||||||
|
EXERCISE_PROPERTY_SYNONYM_TO_TARGET: dict[str, str] = {
|
||||||
|
"primarycapability": "skill_names",
|
||||||
|
"hauptfaehigkeit": "skill_names",
|
||||||
|
"primaerefaehigkeit": "skill_names",
|
||||||
|
"hauptfhigkeit": "skill_names",
|
||||||
|
"hauptfahigkeit": "skill_names",
|
||||||
|
"capabilitylevel": "skill_levels_raw",
|
||||||
|
"faehigkeitsstufe": "skill_levels_raw",
|
||||||
|
"faehigkeitslevel": "skill_levels_raw",
|
||||||
|
"capabilitystufe": "skill_levels_raw",
|
||||||
|
"stilrichtung": "style_names",
|
||||||
|
"trainingsstilrichtung": "style_names",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _exercise_property_target(prop_name: str) -> str | None:
|
||||||
|
"""Ermittelt Zielfeld für eine SMW-Property; None = unbekannt."""
|
||||||
|
if prop_name in EXERCISE_PROPERTY_MAP:
|
||||||
|
return EXERCISE_PROPERTY_MAP[prop_name]
|
||||||
|
n = _norm_prop_synonym(prop_name)
|
||||||
|
if n in EXERCISE_PROPERTY_SYNONYM_TO_TARGET:
|
||||||
|
return EXERCISE_PROPERTY_SYNONYM_TO_TARGET[n]
|
||||||
|
nlow = (prop_name or "").lower()
|
||||||
|
if "primary" in nlow and "capab" in nlow and "level" not in nlow:
|
||||||
|
return "skill_names"
|
||||||
|
if "capab" in nlow and "level" in nlow:
|
||||||
|
return "skill_levels_raw"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Haupt-Mapping-Funktion #
|
# Haupt-Mapping-Funktion #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
@ -206,6 +250,7 @@ def map_wiki_to_exercise(
|
||||||
"age_group_names": [],
|
"age_group_names": [],
|
||||||
"skill_names": [],
|
"skill_names": [],
|
||||||
"skill_levels_raw": [], # Integer-Strings ["3", "2"]
|
"skill_levels_raw": [], # Integer-Strings ["3", "2"]
|
||||||
|
"style_names": [],
|
||||||
"method_names": [],
|
"method_names": [],
|
||||||
# Equipment
|
# Equipment
|
||||||
"equipment": [],
|
"equipment": [],
|
||||||
|
|
@ -217,7 +262,7 @@ def map_wiki_to_exercise(
|
||||||
if not values:
|
if not values:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
target = EXERCISE_PROPERTY_MAP.get(prop_name)
|
target = _exercise_property_target(prop_name)
|
||||||
if not target:
|
if not target:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -259,6 +304,9 @@ def map_wiki_to_exercise(
|
||||||
elif target == "method_names":
|
elif target == "method_names":
|
||||||
mapped["method_names"] = [wiki_name_to_label(v) for v in (values if isinstance(values, list) else [values])]
|
mapped["method_names"] = [wiki_name_to_label(v) for v in (values if isinstance(values, list) else [values])]
|
||||||
|
|
||||||
|
elif target == "style_names":
|
||||||
|
mapped["style_names"] = [wiki_name_to_label(v) for v in (values if isinstance(values, list) else [values])]
|
||||||
|
|
||||||
elif target == "skill_names":
|
elif target == "skill_names":
|
||||||
mapped["skill_names"] = [wiki_name_to_label(v) for v in (values if isinstance(values, list) else [values])]
|
mapped["skill_names"] = [wiki_name_to_label(v) for v in (values if isinstance(values, list) else [values])]
|
||||||
|
|
||||||
|
|
|
||||||
67
frontend/src/components/SearchableSelect.jsx
Normal file
67
frontend/src/components/SearchableSelect.jsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kombination aus Filter-Eingabe und Auswahl: Liste wird per Tippen eingeschränkt.
|
||||||
|
* Die aktuelle Auswahl bleibt sichtbar, auch wenn sie durch den Filter ausgeblendet würde.
|
||||||
|
*/
|
||||||
|
export default function SearchableSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
options = [],
|
||||||
|
idKey = 'id',
|
||||||
|
labelKey = 'label',
|
||||||
|
allLabel = 'Alle',
|
||||||
|
filterPlaceholder = 'Tippen zum Filtern…',
|
||||||
|
selectClassName = 'form-input',
|
||||||
|
className = '',
|
||||||
|
}) {
|
||||||
|
const [q, setQ] = useState('')
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
return options.map((o) => ({
|
||||||
|
id: o[idKey],
|
||||||
|
label: typeof o[labelKey] === 'function' ? o[labelKey](o) : String(o[labelKey] ?? ''),
|
||||||
|
}))
|
||||||
|
}, [options, idKey, labelKey])
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const t = q.trim().toLowerCase()
|
||||||
|
if (!t) return rows
|
||||||
|
return rows.filter((r) => r.label.toLowerCase().includes(t) || String(r.id).includes(t))
|
||||||
|
}, [rows, q])
|
||||||
|
|
||||||
|
const withSelection = useMemo(() => {
|
||||||
|
if (value === '' || value == null) return filtered
|
||||||
|
const sel = String(value)
|
||||||
|
if (filtered.some((r) => String(r.id) === sel)) return filtered
|
||||||
|
const found = rows.find((r) => String(r.id) === sel)
|
||||||
|
if (found) return [found, ...filtered]
|
||||||
|
return filtered
|
||||||
|
}, [filtered, rows, value])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
className={selectClassName}
|
||||||
|
placeholder={filterPlaceholder}
|
||||||
|
value={q}
|
||||||
|
onChange={(e) => setQ(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
aria-label="Filter"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className={selectClassName}
|
||||||
|
value={value === '' || value == null ? '' : String(value)}
|
||||||
|
onChange={(e) => onChange(e.target.value === '' ? '' : e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">{allLabel}</option>
|
||||||
|
{withSelection.map((r) => (
|
||||||
|
<option key={String(r.id)} value={r.id}>
|
||||||
|
{r.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels'
|
import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels'
|
||||||
|
import SearchableSelect from '../components/SearchableSelect'
|
||||||
|
|
||||||
const PAGE_SIZE = 100
|
const PAGE_SIZE = 100
|
||||||
const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null)
|
const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null)
|
||||||
|
|
@ -46,6 +47,52 @@ function ExercisesListPage() {
|
||||||
return () => clearTimeout(t)
|
return () => clearTimeout(t)
|
||||||
}, [aiSearchInput])
|
}, [aiSearchInput])
|
||||||
|
|
||||||
|
const focusOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
catalogs.focusAreas.map((fa) => ({
|
||||||
|
id: fa.id,
|
||||||
|
label: `${fa.icon || ''} ${fa.name || ''}`.trim(),
|
||||||
|
})),
|
||||||
|
[catalogs.focusAreas]
|
||||||
|
)
|
||||||
|
const styleOptions = useMemo(
|
||||||
|
() => catalogs.styleDirections.map((s) => ({ id: s.id, label: s.name || String(s.id) })),
|
||||||
|
[catalogs.styleDirections]
|
||||||
|
)
|
||||||
|
const trainingTypeOptions = useMemo(
|
||||||
|
() => catalogs.trainingTypes.map((t) => ({ id: t.id, label: t.name || String(t.id) })),
|
||||||
|
[catalogs.trainingTypes]
|
||||||
|
)
|
||||||
|
const targetGroupOptions = useMemo(
|
||||||
|
() => catalogs.targetGroups.map((g) => ({ id: g.id, label: g.name || String(g.id) })),
|
||||||
|
[catalogs.targetGroups]
|
||||||
|
)
|
||||||
|
const skillOptions = useMemo(
|
||||||
|
() => catalogs.skills.map((s) => ({ id: s.id, label: s.name || String(s.id) })),
|
||||||
|
[catalogs.skills]
|
||||||
|
)
|
||||||
|
const levelFilterOptions = useMemo(
|
||||||
|
() => LEVEL_FILTER_OPTS.map((o) => ({ id: o.level, label: o.label })),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const visibilityOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ id: 'private', label: 'Privat' },
|
||||||
|
{ id: 'club', label: 'Verein' },
|
||||||
|
{ id: 'official', label: 'Offiziell' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const statusOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ id: 'draft', label: 'Entwurf' },
|
||||||
|
{ id: 'in_review', label: 'In Prüfung' },
|
||||||
|
{ id: 'approved', label: 'Freigegeben' },
|
||||||
|
{ id: 'archived', label: 'Archiviert' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const queryBase = useMemo(() => {
|
const queryBase = useMemo(() => {
|
||||||
const q = {}
|
const q = {}
|
||||||
const n = (v) => (v === '' || v == null ? undefined : Number(v))
|
const n = (v) => (v === '' || v == null ? undefined : Number(v))
|
||||||
|
|
@ -205,135 +252,93 @@ function ExercisesListPage() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Fokus</label>
|
<label className="form-label">Fokus</label>
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-input"
|
|
||||||
value={filters.focus_area}
|
value={filters.focus_area}
|
||||||
onChange={(e) => setFilters({ ...filters, focus_area: e.target.value })}
|
onChange={(v) => setFilters({ ...filters, focus_area: v })}
|
||||||
>
|
options={focusOptions}
|
||||||
<option value="">Alle</option>
|
allLabel="Alle Fokusbereiche"
|
||||||
{catalogs.focusAreas.map((fa) => (
|
filterPlaceholder="Fokus filtern…"
|
||||||
<option key={fa.id} value={fa.id}>
|
/>
|
||||||
{fa.icon} {fa.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Stilrichtung</label>
|
<label className="form-label">Stilrichtung</label>
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-input"
|
|
||||||
value={filters.style_direction_id}
|
value={filters.style_direction_id}
|
||||||
onChange={(e) => setFilters({ ...filters, style_direction_id: e.target.value })}
|
onChange={(v) => setFilters({ ...filters, style_direction_id: v })}
|
||||||
>
|
options={styleOptions}
|
||||||
<option value="">Alle</option>
|
allLabel="Alle Stilrichtungen"
|
||||||
{catalogs.styleDirections.map((s) => (
|
filterPlaceholder="Stilrichtung filtern…"
|
||||||
<option key={s.id} value={s.id}>
|
/>
|
||||||
{s.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Trainingsstil</label>
|
<label className="form-label">Trainingsstil</label>
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-input"
|
|
||||||
value={filters.training_type_id}
|
value={filters.training_type_id}
|
||||||
onChange={(e) => setFilters({ ...filters, training_type_id: e.target.value })}
|
onChange={(v) => setFilters({ ...filters, training_type_id: v })}
|
||||||
>
|
options={trainingTypeOptions}
|
||||||
<option value="">Alle</option>
|
allLabel="Alle Trainingsstile"
|
||||||
{catalogs.trainingTypes.map((t) => (
|
filterPlaceholder="Trainingsstil filtern…"
|
||||||
<option key={t.id} value={t.id}>
|
/>
|
||||||
{t.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Zielgruppe</label>
|
<label className="form-label">Zielgruppe</label>
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-input"
|
|
||||||
value={filters.target_group_id}
|
value={filters.target_group_id}
|
||||||
onChange={(e) => setFilters({ ...filters, target_group_id: e.target.value })}
|
onChange={(v) => setFilters({ ...filters, target_group_id: v })}
|
||||||
>
|
options={targetGroupOptions}
|
||||||
<option value="">Alle</option>
|
allLabel="Alle Zielgruppen"
|
||||||
{catalogs.targetGroups.map((g) => (
|
filterPlaceholder="Zielgruppe filtern…"
|
||||||
<option key={g.id} value={g.id}>
|
/>
|
||||||
{g.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Fähigkeit</label>
|
<label className="form-label">Fähigkeit</label>
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-input"
|
|
||||||
value={filters.skill_id}
|
value={filters.skill_id}
|
||||||
onChange={(e) => setFilters({ ...filters, skill_id: e.target.value })}
|
onChange={(v) => setFilters({ ...filters, skill_id: v })}
|
||||||
>
|
options={skillOptions}
|
||||||
<option value="">Alle</option>
|
allLabel="Alle Fähigkeiten"
|
||||||
{catalogs.skills.map((s) => (
|
filterPlaceholder="Fähigkeit filtern…"
|
||||||
<option key={s.id} value={s.id}>
|
/>
|
||||||
{s.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Fähigkeit Stufe von</label>
|
<label className="form-label">Fähigkeit Stufe von</label>
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-input"
|
|
||||||
value={filters.skill_min_level}
|
value={filters.skill_min_level}
|
||||||
onChange={(e) => setFilters({ ...filters, skill_min_level: e.target.value })}
|
onChange={(v) => setFilters({ ...filters, skill_min_level: v })}
|
||||||
>
|
options={levelFilterOptions}
|
||||||
<option value="">egal</option>
|
allLabel="egal (min)"
|
||||||
{LEVEL_FILTER_OPTS.map((o) => (
|
filterPlaceholder="Stufe suchen…"
|
||||||
<option key={o.value} value={String(o.level)}>
|
/>
|
||||||
{o.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Fähigkeit Stufe bis</label>
|
<label className="form-label">Fähigkeit Stufe bis</label>
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-input"
|
|
||||||
value={filters.skill_max_level}
|
value={filters.skill_max_level}
|
||||||
onChange={(e) => setFilters({ ...filters, skill_max_level: e.target.value })}
|
onChange={(v) => setFilters({ ...filters, skill_max_level: v })}
|
||||||
>
|
options={levelFilterOptions}
|
||||||
<option value="">egal</option>
|
allLabel="egal (max)"
|
||||||
{LEVEL_FILTER_OPTS.map((o) => (
|
filterPlaceholder="Stufe suchen…"
|
||||||
<option key={`m-${o.value}`} value={String(o.level)}>
|
/>
|
||||||
{o.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Sichtbarkeit</label>
|
<label className="form-label">Sichtbarkeit</label>
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-input"
|
|
||||||
value={filters.visibility}
|
value={filters.visibility}
|
||||||
onChange={(e) => setFilters({ ...filters, visibility: e.target.value })}
|
onChange={(v) => setFilters({ ...filters, visibility: v })}
|
||||||
>
|
options={visibilityOptions}
|
||||||
<option value="">Alle</option>
|
allLabel="Alle"
|
||||||
<option value="private">Privat</option>
|
filterPlaceholder="Filtern…"
|
||||||
<option value="club">Verein</option>
|
/>
|
||||||
<option value="official">Offiziell</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Status</label>
|
<label className="form-label">Status</label>
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-input"
|
|
||||||
value={filters.status}
|
value={filters.status}
|
||||||
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
onChange={(v) => setFilters({ ...filters, status: v })}
|
||||||
>
|
options={statusOptions}
|
||||||
<option value="">Alle</option>
|
allLabel="Alle"
|
||||||
<option value="draft">Entwurf</option>
|
filterPlaceholder="Filtern…"
|
||||||
<option value="in_review">In Prüfung</option>
|
/>
|
||||||
<option value="approved">Freigegeben</option>
|
|
||||||
<option value="archived">Archiviert</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user