UX - Filter #12
|
|
@ -215,20 +215,35 @@ class ExerciseVariantsReorder(BaseModel):
|
||||||
|
|
||||||
_VALID_EXERCISE_STATUS_BULK = frozenset({"draft", "in_review", "approved", "archived"})
|
_VALID_EXERCISE_STATUS_BULK = frozenset({"draft", "in_review", "approved", "archived"})
|
||||||
_MAX_BULK_METADATA_IDS = 500
|
_MAX_BULK_METADATA_IDS = 500
|
||||||
|
_MAX_BULK_RELATION_IDS_PER_KIND = 80
|
||||||
|
|
||||||
|
|
||||||
class ExerciseBulkMetadataPatch(BaseModel):
|
class ExerciseBulkMetadataPatch(BaseModel):
|
||||||
"""Massenänderung von Sichtbarkeit und/oder Status (z. B. Private → Verein)."""
|
"""Massenänderung: Sichtbarkeit/Status und/oder Zuordnungen (Kataloge)."""
|
||||||
|
|
||||||
exercise_ids: list[int] = Field(..., min_length=1, max_length=_MAX_BULK_METADATA_IDS)
|
exercise_ids: list[int] = Field(..., min_length=1, max_length=_MAX_BULK_METADATA_IDS)
|
||||||
visibility: Optional[str] = Field(None, pattern="^(private|club|official)$")
|
visibility: Optional[str] = Field(None, pattern="^(private|club|official)$")
|
||||||
status: Optional[str] = None
|
status: Optional[str] = None
|
||||||
club_id: Optional[int] = Field(default=None, ge=1)
|
club_id: Optional[int] = Field(default=None, ge=1)
|
||||||
|
focus_area_ids: Optional[list[int]] = Field(default=None, max_length=_MAX_BULK_RELATION_IDS_PER_KIND)
|
||||||
|
style_direction_ids: Optional[list[int]] = Field(default=None, max_length=_MAX_BULK_RELATION_IDS_PER_KIND)
|
||||||
|
training_type_ids: Optional[list[int]] = Field(default=None, max_length=_MAX_BULK_RELATION_IDS_PER_KIND)
|
||||||
|
target_group_ids: Optional[list[int]] = Field(default=None, max_length=_MAX_BULK_RELATION_IDS_PER_KIND)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def at_least_visibility_or_status(self):
|
def at_least_one_patch_field(self):
|
||||||
if self.visibility is None and self.status is None:
|
if (
|
||||||
raise ValueError("Mindestens eines der Felder visibility oder status angeben")
|
self.visibility is None
|
||||||
|
and self.status is None
|
||||||
|
and self.focus_area_ids is None
|
||||||
|
and self.style_direction_ids is None
|
||||||
|
and self.training_type_ids is None
|
||||||
|
and self.target_group_ids is None
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"Mindestens eines der Felder visibility, status, focus_area_ids, style_direction_ids, "
|
||||||
|
"training_type_ids oder target_group_ids angeben"
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -456,7 +471,14 @@ def enrich_exercise_detail(exercise_id: int, cur) -> dict:
|
||||||
return exercise
|
return exercise
|
||||||
|
|
||||||
|
|
||||||
def assign_exercise_relations(cur, conn, exercise_id: int, data: dict):
|
def assign_exercise_relations(
|
||||||
|
cur,
|
||||||
|
conn,
|
||||||
|
exercise_id: int,
|
||||||
|
data: dict,
|
||||||
|
*,
|
||||||
|
do_commit: bool = True,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Weist M:N Relations für eine Übung zu.
|
Weist M:N Relations für eine Übung zu.
|
||||||
Löscht alte Zuordnungen und legt neue an (REPLACE-Logik).
|
Löscht alte Zuordnungen und legt neue an (REPLACE-Logik).
|
||||||
|
|
@ -532,6 +554,7 @@ def assign_exercise_relations(cur, conn, exercise_id: int, data: dict):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if do_commit:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -539,6 +562,51 @@ def assign_exercise_relations(cur, conn, exercise_id: int, data: dict):
|
||||||
# Endpoints
|
# Endpoints
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_bulk_id_list(raw: Optional[list]) -> list[int]:
|
||||||
|
"""Positive IDs, Reihenfolge beibehalten, Duplikate entfernen."""
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
seen: set[int] = set()
|
||||||
|
out: list[int] = []
|
||||||
|
for x in raw:
|
||||||
|
try:
|
||||||
|
xi = int(x)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if xi < 1 or xi in seen:
|
||||||
|
continue
|
||||||
|
seen.add(xi)
|
||||||
|
out.append(xi)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_catalog_ids_exist(cur, kind: str, ids: list[int]) -> None:
|
||||||
|
if not ids:
|
||||||
|
return
|
||||||
|
table_by_kind = {
|
||||||
|
"focus_areas": "focus_areas",
|
||||||
|
"style_directions": "style_directions",
|
||||||
|
"training_types": "training_types",
|
||||||
|
"target_groups": "target_groups",
|
||||||
|
}
|
||||||
|
table = table_by_kind.get(kind)
|
||||||
|
if not table:
|
||||||
|
raise HTTPException(status_code=500, detail="Interner Fehler: unbekannter Katalog")
|
||||||
|
ph = ",".join(["%s"] * len(ids))
|
||||||
|
cur.execute(f"SELECT id FROM {table} WHERE id IN ({ph})", tuple(ids))
|
||||||
|
found = {
|
||||||
|
int(r["id"]) if isinstance(r, dict) else int(r[0])
|
||||||
|
for r in cur.fetchall()
|
||||||
|
}
|
||||||
|
missing = [i for i in ids if i not in found]
|
||||||
|
if missing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Unbekannte {kind}-IDs (Beispiele): {missing[:12]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _merge_ids(multi: list[int], single: Optional[int]) -> list[int]:
|
def _merge_ids(multi: list[int], single: Optional[int]) -> list[int]:
|
||||||
"""Liste aus wiederholten Query-Parametern plus optional einem Legacy-Einzelfilter (ohne Duplikate)."""
|
"""Liste aus wiederholten Query-Parametern plus optional einem Legacy-Einzelfilter (ohne Duplikate)."""
|
||||||
seen: set[int] = set()
|
seen: set[int] = set()
|
||||||
|
|
@ -577,7 +645,11 @@ def bulk_patch_exercises_metadata(
|
||||||
tenant: TenantContext = Depends(get_tenant_context),
|
tenant: TenantContext = Depends(get_tenant_context),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Ändert Sichtbarkeit und/oder Status für viele Übungen auf einmal.
|
Ändert Sichtbarkeit, Status und/oder Katalog-Zuordnungen für viele Übungen auf einmal (REPLACE je Kategorie).
|
||||||
|
|
||||||
|
Zuordnung: Sind z. B. focus_area_ids im Body gesetzt, werden die Fokusbereiche bei den bearbeiteten
|
||||||
|
Übungen vollständig durch diese Liste ersetzt (leeres Array entfernt alle).
|
||||||
|
|
||||||
Berechtigt: Ersteller der jeweiligen Übung oder Plattform-Admin (admin/superadmin).
|
Berechtigt: Ersteller der jeweiligen Übung oder Plattform-Admin (admin/superadmin).
|
||||||
Governance wie bei Einzel-PUT (official nur Plattform-Admin; club mit Mitgliedschaft bzw. Admin).
|
Governance wie bei Einzel-PUT (official nur Plattform-Admin; club mit Mitgliedschaft bzw. Admin).
|
||||||
"""
|
"""
|
||||||
|
|
@ -603,6 +675,33 @@ def bulk_patch_exercises_metadata(
|
||||||
patch_visibility = body.visibility is not None
|
patch_visibility = body.visibility is not None
|
||||||
patch_status = status_val is not None
|
patch_status = status_val is not None
|
||||||
|
|
||||||
|
patch_focus_areas = body.focus_area_ids is not None
|
||||||
|
fa_ids = _normalize_bulk_id_list(body.focus_area_ids or []) if patch_focus_areas else []
|
||||||
|
patch_style_dirs = body.style_direction_ids is not None
|
||||||
|
sd_ids = _normalize_bulk_id_list(body.style_direction_ids or []) if patch_style_dirs else []
|
||||||
|
patch_training_types = body.training_type_ids is not None
|
||||||
|
tt_ids = _normalize_bulk_id_list(body.training_type_ids or []) if patch_training_types else []
|
||||||
|
patch_target_groups = body.target_group_ids is not None
|
||||||
|
tg_ids = _normalize_bulk_id_list(body.target_group_ids or []) if patch_target_groups else []
|
||||||
|
|
||||||
|
relation_data: Dict[str, Any] = {}
|
||||||
|
if patch_focus_areas:
|
||||||
|
relation_data["focus_areas_multi"] = [
|
||||||
|
{"focus_area_id": i, "is_primary": idx == 0} for idx, i in enumerate(fa_ids)
|
||||||
|
]
|
||||||
|
if patch_style_dirs:
|
||||||
|
relation_data["training_styles_multi"] = [
|
||||||
|
{"training_style_id": i, "is_primary": idx == 0} for idx, i in enumerate(sd_ids)
|
||||||
|
]
|
||||||
|
if patch_training_types:
|
||||||
|
relation_data["training_types_multi"] = [
|
||||||
|
{"training_type_id": i, "is_primary": idx == 0} for idx, i in enumerate(tt_ids)
|
||||||
|
]
|
||||||
|
if patch_target_groups:
|
||||||
|
relation_data["target_groups_multi"] = [
|
||||||
|
{"target_group_id": i, "is_primary": idx == 0} for idx, i in enumerate(tg_ids)
|
||||||
|
]
|
||||||
|
|
||||||
updated: List[int] = []
|
updated: List[int] = []
|
||||||
failed: List[Dict[str, Any]] = []
|
failed: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
|
@ -612,6 +711,16 @@ def bulk_patch_exercises_metadata(
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
if patch_focus_areas:
|
||||||
|
_assert_catalog_ids_exist(cur, "focus_areas", fa_ids)
|
||||||
|
if patch_style_dirs:
|
||||||
|
_assert_catalog_ids_exist(cur, "style_directions", sd_ids)
|
||||||
|
if patch_training_types:
|
||||||
|
_assert_catalog_ids_exist(cur, "training_types", tt_ids)
|
||||||
|
if patch_target_groups:
|
||||||
|
_assert_catalog_ids_exist(cur, "target_groups", tg_ids)
|
||||||
|
|
||||||
for ex_id in unique_ids:
|
for ex_id in unique_ids:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT id, created_by, visibility, club_id FROM exercises WHERE id = %s",
|
"SELECT id, created_by, visibility, club_id FROM exercises WHERE id = %s",
|
||||||
|
|
@ -681,6 +790,8 @@ def bulk_patch_exercises_metadata(
|
||||||
f"UPDATE exercises SET {', '.join(sets)} WHERE id = %s",
|
f"UPDATE exercises SET {', '.join(sets)} WHERE id = %s",
|
||||||
tuple(vals),
|
tuple(vals),
|
||||||
)
|
)
|
||||||
|
if relation_data:
|
||||||
|
assign_exercise_relations(cur, conn, ex_id, relation_data, do_commit=False)
|
||||||
updated.append(ex_id)
|
updated.append(ex_id)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ MODULE_VERSIONS = {
|
||||||
"groups": "0.1.0",
|
"groups": "0.1.0",
|
||||||
"skills": "0.1.0",
|
"skills": "0.1.0",
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.7.0", # PATCH /exercises/bulk-metadata — Massenänderung Sichtbarkeit/Status
|
"exercises": "2.8.0", # PATCH bulk-metadata: Sichtbarkeit/Status + Katalog-Zuordnungen (REPLACE je Feld)
|
||||||
"training_units": "0.2.0",
|
"training_units": "0.2.0",
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.8.1", # Vorlagen Leseprüfung library_content_visible_to_profile
|
"planning": "0.8.1", # Vorlagen Leseprüfung library_content_visible_to_profile
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,14 @@ function ExercisesListPage() {
|
||||||
const [bulkClubSelect, setBulkClubSelect] = useState('')
|
const [bulkClubSelect, setBulkClubSelect] = useState('')
|
||||||
const [bulkClubManual, setBulkClubManual] = useState('')
|
const [bulkClubManual, setBulkClubManual] = useState('')
|
||||||
const [bulkSubmitting, setBulkSubmitting] = useState(false)
|
const [bulkSubmitting, setBulkSubmitting] = useState(false)
|
||||||
|
const [bulkPatchFocusAreas, setBulkPatchFocusAreas] = useState(false)
|
||||||
|
const [bulkFocusAreaIds, setBulkFocusAreaIds] = useState([])
|
||||||
|
const [bulkPatchStyleDirections, setBulkPatchStyleDirections] = useState(false)
|
||||||
|
const [bulkStyleDirectionIds, setBulkStyleDirectionIds] = useState([])
|
||||||
|
const [bulkPatchTrainingTypes, setBulkPatchTrainingTypes] = useState(false)
|
||||||
|
const [bulkTrainingTypeIds, setBulkTrainingTypeIds] = useState([])
|
||||||
|
const [bulkPatchTargetGroups, setBulkPatchTargetGroups] = useState(false)
|
||||||
|
const [bulkTargetGroupIds, setBulkTargetGroupIds] = useState([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = setTimeout(() => setDebouncedSearch(searchInput.trim()), 400)
|
const t = setTimeout(() => setDebouncedSearch(searchInput.trim()), 400)
|
||||||
|
|
@ -421,12 +429,27 @@ function ExercisesListPage() {
|
||||||
setBulkStatus('')
|
setBulkStatus('')
|
||||||
setBulkClubSelect('')
|
setBulkClubSelect('')
|
||||||
setBulkClubManual('')
|
setBulkClubManual('')
|
||||||
|
setBulkPatchFocusAreas(false)
|
||||||
|
setBulkFocusAreaIds([])
|
||||||
|
setBulkPatchStyleDirections(false)
|
||||||
|
setBulkStyleDirectionIds([])
|
||||||
|
setBulkPatchTrainingTypes(false)
|
||||||
|
setBulkTrainingTypeIds([])
|
||||||
|
setBulkPatchTargetGroups(false)
|
||||||
|
setBulkTargetGroupIds([])
|
||||||
setBulkModalOpen(true)
|
setBulkModalOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBulkSubmit = async () => {
|
const handleBulkSubmit = async () => {
|
||||||
if (!bulkVisibility && !bulkStatus) {
|
const anyRelationPatch =
|
||||||
alert('Bitte mindestens Sichtbarkeit oder Status wählen (nicht „nicht ändern“ bei beiden).')
|
bulkPatchFocusAreas ||
|
||||||
|
bulkPatchStyleDirections ||
|
||||||
|
bulkPatchTrainingTypes ||
|
||||||
|
bulkPatchTargetGroups
|
||||||
|
if (!bulkVisibility && !bulkStatus && !anyRelationPatch) {
|
||||||
|
alert(
|
||||||
|
'Bitte mindestens eine Änderung wählen (Sichtbarkeit, Status oder eine der Zuordnungen mit gesetztem Häkchen „ersetzen“).'
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const ids = Array.from(selectedIds).filter((x) => Number.isFinite(x) && x > 0)
|
const ids = Array.from(selectedIds).filter((x) => Number.isFinite(x) && x > 0)
|
||||||
|
|
@ -441,6 +464,18 @@ function ExercisesListPage() {
|
||||||
const payload = { exercise_ids: ids }
|
const payload = { exercise_ids: ids }
|
||||||
if (bulkVisibility) payload.visibility = bulkVisibility
|
if (bulkVisibility) payload.visibility = bulkVisibility
|
||||||
if (bulkStatus) payload.status = bulkStatus
|
if (bulkStatus) payload.status = bulkStatus
|
||||||
|
if (bulkPatchFocusAreas) {
|
||||||
|
payload.focus_area_ids = bulkFocusAreaIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
||||||
|
}
|
||||||
|
if (bulkPatchStyleDirections) {
|
||||||
|
payload.style_direction_ids = bulkStyleDirectionIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
||||||
|
}
|
||||||
|
if (bulkPatchTrainingTypes) {
|
||||||
|
payload.training_type_ids = bulkTrainingTypeIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
||||||
|
}
|
||||||
|
if (bulkPatchTargetGroups) {
|
||||||
|
payload.target_group_ids = bulkTargetGroupIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
||||||
|
}
|
||||||
if (bulkVisibility === 'club') {
|
if (bulkVisibility === 'club') {
|
||||||
const manual = String(bulkClubManual || '').trim()
|
const manual = String(bulkClubManual || '').trim()
|
||||||
if (manual && /^\d+$/.test(manual)) payload.club_id = Number(manual)
|
if (manual && /^\d+$/.test(manual)) payload.club_id = Number(manual)
|
||||||
|
|
@ -460,6 +495,15 @@ function ExercisesListPage() {
|
||||||
const clubLabel =
|
const clubLabel =
|
||||||
resolvedClubId != null ? clubNameById[resolvedClubId] || `Verein #${resolvedClubId}` : null
|
resolvedClubId != null ? clubNameById[resolvedClubId] || `Verein #${resolvedClubId}` : null
|
||||||
|
|
||||||
|
let nextPrimaryFocusName = null
|
||||||
|
if (bulkPatchFocusAreas) {
|
||||||
|
const faNums = bulkFocusAreaIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
||||||
|
if (faNums.length > 0) {
|
||||||
|
const opt = focusOptions.find((o) => Number(o.id) === Number(faNums[0]))
|
||||||
|
nextPrimaryFocusName = String(opt?.label ?? '').trim() || String(faNums[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setExercises((prev) =>
|
setExercises((prev) =>
|
||||||
prev.map((e) => {
|
prev.map((e) => {
|
||||||
if (!updatedSet.has(Number(e.id))) return e
|
if (!updatedSet.has(Number(e.id))) return e
|
||||||
|
|
@ -470,6 +514,10 @@ function ExercisesListPage() {
|
||||||
next.club_name = bulkVisibility === 'club' ? clubLabel : null
|
next.club_name = bulkVisibility === 'club' ? clubLabel : null
|
||||||
}
|
}
|
||||||
if (bulkStatus) next.status = bulkStatus
|
if (bulkStatus) next.status = bulkStatus
|
||||||
|
if (bulkPatchFocusAreas) {
|
||||||
|
if (nextPrimaryFocusName == null) delete next.focus_area
|
||||||
|
else next.focus_area = nextPrimaryFocusName
|
||||||
|
}
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
@ -650,7 +698,7 @@ function ExercisesListPage() {
|
||||||
Auswahl aufheben
|
Auswahl aufheben
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn btn-primary" onClick={openBulkModal}>
|
<button type="button" className="btn btn-primary" onClick={openBulkModal}>
|
||||||
Sichtbarkeit / Status ändern…
|
Massenänderung…
|
||||||
</button>
|
</button>
|
||||||
<span style={{ fontSize: '12px', color: 'var(--text3)' }}>
|
<span style={{ fontSize: '12px', color: 'var(--text3)' }}>
|
||||||
Bis zu {BULK_MAX_IDS} pro Anfrage. Für „Verein“ ohne Auswahl: aktiver Vereinskontext (
|
Bis zu {BULK_MAX_IDS} pro Anfrage. Für „Verein“ ohne Auswahl: aktiver Vereinskontext (
|
||||||
|
|
@ -841,7 +889,7 @@ function ExercisesListPage() {
|
||||||
>
|
>
|
||||||
<div className="admin-modal-sheet__header">
|
<div className="admin-modal-sheet__header">
|
||||||
<h3 id="exercise-bulk-modal-title" className="admin-modal-sheet__title">
|
<h3 id="exercise-bulk-modal-title" className="admin-modal-sheet__title">
|
||||||
Massenänderung: Sichtbarkeit / Status
|
Massenänderung
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -857,6 +905,11 @@ function ExercisesListPage() {
|
||||||
höchstens {BULK_MAX_IDS}. Ohne Berechtigung bleiben Einzelübungen unverändert (siehe Hinweis nach dem
|
höchstens {BULK_MAX_IDS}. Ohne Berechtigung bleiben Einzelübungen unverändert (siehe Hinweis nach dem
|
||||||
Speichern).
|
Speichern).
|
||||||
</p>
|
</p>
|
||||||
|
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, marginBottom: '14px' }}>
|
||||||
|
Unter „Zuordnung ersetzen“: die gewählte Liste ersetzt die bisherige Zuordnung bei allen betroffenen
|
||||||
|
Übungen vollständig (leere Auswahl = alle Zuordnungen dieser Kategorie entfernen). Die erste Auswahl gilt
|
||||||
|
als Primärzuordnung.
|
||||||
|
</p>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-label">Sichtbarkeit</label>
|
<label className="form-label">Sichtbarkeit</label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -918,6 +971,100 @@ function ExercisesListPage() {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section className="exercise-filter-section" style={{ marginTop: '8px', paddingTop: '12px' }}>
|
||||||
|
<h4 className="exercise-filter-section-title">Zuordnung (optional)</h4>
|
||||||
|
<div className="exercise-filters-modal-grid">
|
||||||
|
<div>
|
||||||
|
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={bulkPatchFocusAreas}
|
||||||
|
onChange={(e) => {
|
||||||
|
const on = e.target.checked
|
||||||
|
setBulkPatchFocusAreas(on)
|
||||||
|
if (!on) setBulkFocusAreaIds([])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Fokusbereiche ersetzen
|
||||||
|
</label>
|
||||||
|
{bulkPatchFocusAreas ? (
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={bulkFocusAreaIds}
|
||||||
|
onChange={setBulkFocusAreaIds}
|
||||||
|
options={focusOptions}
|
||||||
|
placeholder="Fokusbereiche wählen …"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={bulkPatchStyleDirections}
|
||||||
|
onChange={(e) => {
|
||||||
|
const on = e.target.checked
|
||||||
|
setBulkPatchStyleDirections(on)
|
||||||
|
if (!on) setBulkStyleDirectionIds([])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Stilrichtungen ersetzen
|
||||||
|
</label>
|
||||||
|
{bulkPatchStyleDirections ? (
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={bulkStyleDirectionIds}
|
||||||
|
onChange={setBulkStyleDirectionIds}
|
||||||
|
options={styleOptions}
|
||||||
|
placeholder="Stilrichtungen wählen …"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={bulkPatchTrainingTypes}
|
||||||
|
onChange={(e) => {
|
||||||
|
const on = e.target.checked
|
||||||
|
setBulkPatchTrainingTypes(on)
|
||||||
|
if (!on) setBulkTrainingTypeIds([])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Trainingsstile ersetzen
|
||||||
|
</label>
|
||||||
|
{bulkPatchTrainingTypes ? (
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={bulkTrainingTypeIds}
|
||||||
|
onChange={setBulkTrainingTypeIds}
|
||||||
|
options={trainingTypeOptions}
|
||||||
|
placeholder="Trainingsstile wählen …"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={bulkPatchTargetGroups}
|
||||||
|
onChange={(e) => {
|
||||||
|
const on = e.target.checked
|
||||||
|
setBulkPatchTargetGroups(on)
|
||||||
|
if (!on) setBulkTargetGroupIds([])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Zielgruppen ersetzen
|
||||||
|
</label>
|
||||||
|
{bulkPatchTargetGroups ? (
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={bulkTargetGroupIds}
|
||||||
|
onChange={setBulkTargetGroupIds}
|
||||||
|
options={targetGroupOptions}
|
||||||
|
placeholder="Zielgruppen wählen …"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div className="exercise-filter-modal__footer" style={{ justifyContent: 'flex-end' }}>
|
<div className="exercise-filter-modal__footer" style={{ justifyContent: 'flex-end' }}>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -508,7 +508,7 @@ export async function updateExercise(id, data) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Massenänderung Sichtbarkeit / Status (`PATCH /api/exercises/bulk-metadata`). */
|
/** Massenänderung Übungen: Sichtbarkeit, Status, Katalog-Zuordnungen (`PATCH /api/exercises/bulk-metadata`). */
|
||||||
export async function bulkPatchExercisesMetadata(data) {
|
export async function bulkPatchExercisesMetadata(data) {
|
||||||
return request('/api/exercises/bulk-metadata', {
|
return request('/api/exercises/bulk-metadata', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export const PAGE_VERSIONS = {
|
||||||
LoginPage: "1.0.0",
|
LoginPage: "1.0.0",
|
||||||
Dashboard: "1.0.0",
|
Dashboard: "1.0.0",
|
||||||
AccountSettingsPage: "1.0.0",
|
AccountSettingsPage: "1.0.0",
|
||||||
ExercisesPage: "1.2.0", // Massenänderung Sichtbarkeit/Status auf der Liste
|
ExercisesPage: "1.3.0", // Massenänderung inkl. Fokusbereiche, Stilrichtungen, Trainingsstile, Zielgruppen
|
||||||
ClubsPage: "1.1.0",
|
ClubsPage: "1.1.0",
|
||||||
SkillsPage: "1.0.0",
|
SkillsPage: "1.0.0",
|
||||||
TrainingPlanningPage: "1.4.0",
|
TrainingPlanningPage: "1.4.0",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user