import React, { useState, useEffect, useMemo } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels' const PAGE_SIZE = 100 const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null) function ExercisesListPage() { const [exercises, setExercises] = useState([]) const [catalogs, setCatalogs] = useState({ focusAreas: [], styleDirections: [], trainingTypes: [], targetGroups: [], skills: [], }) const [catalogsReady, setCatalogsReady] = useState(false) const [loading, setLoading] = useState(true) const [loadingMore, setLoadingMore] = useState(false) const [offset, setOffset] = useState(0) const [hasMore, setHasMore] = useState(false) const [searchInput, setSearchInput] = useState('') const [aiSearchInput, setAiSearchInput] = useState('') const [debouncedSearch, setDebouncedSearch] = useState('') const [debouncedAiSearch, setDebouncedAiSearch] = useState('') const [filters, setFilters] = useState({ focus_area: '', style_direction_id: '', training_type_id: '', target_group_id: '', skill_id: '', skill_min_level: '', skill_max_level: '', visibility: '', status: '', }) useEffect(() => { const t = setTimeout(() => setDebouncedSearch(searchInput.trim()), 400) return () => clearTimeout(t) }, [searchInput]) useEffect(() => { const t = setTimeout(() => setDebouncedAiSearch(aiSearchInput.trim()), 400) return () => clearTimeout(t) }, [aiSearchInput]) const queryBase = useMemo(() => { const q = {} const n = (v) => (v === '' || v == null ? undefined : Number(v)) if (filters.focus_area) q.focus_area = n(filters.focus_area) if (filters.style_direction_id) q.style_direction_id = n(filters.style_direction_id) if (filters.training_type_id) q.training_type_id = n(filters.training_type_id) if (filters.target_group_id) q.target_group_id = n(filters.target_group_id) if (filters.skill_id) q.skill_id = n(filters.skill_id) if (filters.skill_min_level) q.skill_min_level = n(filters.skill_min_level) if (filters.skill_max_level) q.skill_max_level = n(filters.skill_max_level) if (filters.visibility) q.visibility = filters.visibility if (filters.status) q.status = filters.status if (debouncedSearch) q.search = debouncedSearch if (debouncedAiSearch) q.ai_search = debouncedAiSearch return q }, [filters, debouncedSearch, debouncedAiSearch]) useEffect(() => { let cancelled = false ;(async () => { try { const [fa, sd, tt, tg, sk] = await Promise.all([ api.listFocusAreas(), api.listStyleDirections(), api.listTrainingTypes(), api.listTargetGroups(), api.listSkills(), ]) if (!cancelled) { setCatalogs({ focusAreas: fa, styleDirections: sd, trainingTypes: tt, targetGroups: tg, skills: sk, }) setCatalogsReady(true) } } catch (err) { if (!cancelled) { console.error(err) alert('Kataloge konnten nicht geladen werden: ' + err.message) setCatalogsReady(true) } } })() return () => { cancelled = true } }, []) useEffect(() => { if (!catalogsReady) return let cancelled = false const run = async () => { setLoading(true) setOffset(0) try { const batch = await api.listExercises({ ...queryBase, limit: PAGE_SIZE, offset: 0 }) if (cancelled) return setExercises(batch) setHasMore(batch.length === PAGE_SIZE) setOffset(batch.length) } catch (err) { if (!cancelled) { console.error('Failed to load data:', err) alert('Fehler beim Laden: ' + err.message) } } finally { if (!cancelled) setLoading(false) } } run() return () => { cancelled = true } }, [queryBase, catalogsReady]) const loadMore = async () => { if (loadingMore || !hasMore) return setLoadingMore(true) try { const batch = await api.listExercises({ ...queryBase, limit: PAGE_SIZE, offset }) setExercises((prev) => [...prev, ...batch]) setHasMore(batch.length === PAGE_SIZE) setOffset((o) => o + batch.length) } catch (err) { alert('Fehler: ' + err.message) } finally { setLoadingMore(false) } } const handleDelete = async (exercise) => { if (!confirm(`Übung "${exercise.title}" wirklich löschen?`)) return try { await api.deleteExercise(exercise.id) setExercises((prev) => prev.filter((e) => e.id !== exercise.id)) } catch (err) { alert('Fehler beim Löschen: ' + err.message) } } if (!catalogsReady || loading) { return (
Laden...
Standardfilter aus Verein und Trainerrolle folgen später; die KI-Nutzung der zweiten Zeile kommt mit einem eigenen Endpunkt.
Keine Übungen gefunden.
{exercises.length} angezeigt {hasMore ? ' · es gibt weitere Einträge' : ''}
{exercise.summary.length > 160 ? `${exercise.summary.slice(0, 160)}…` : exercise.summary}
)}