toggleSelect(exercise.id)}
+ onChange={() => toggleSelect(exercise)}
aria-label={`„${titleText}“ auswählen`}
className="exercise-card-layout__check"
/>
@@ -174,6 +189,11 @@ export default function ExerciseListCard({ exercise, user, selectedIds, toggleSe
e.stopPropagation()}>
{exercise.title}
+ {selectionPinned ? (
+
{focusNames.map((name) => (
diff --git a/frontend/src/components/exercises/ExercisesListPageRoot.jsx b/frontend/src/components/exercises/ExercisesListPageRoot.jsx
index 8a20a46..5bdfc84 100644
--- a/frontend/src/components/exercises/ExercisesListPageRoot.jsx
+++ b/frontend/src/components/exercises/ExercisesListPageRoot.jsx
@@ -14,6 +14,11 @@ import ExercisePeekModal from '../ExercisePeekModal'
import { buildExerciseListFilterChips } from '../../utils/exerciseListFilterChips'
import { applyDashboardExerciseListUrl, buildExerciseListQueryBase } from '../../utils/exerciseListQuery'
import { readExerciseListSessionState, writeExerciseListSessionState } from '../../utils/exerciseListSessionState'
+import {
+ mergeSelectedWithListEntries,
+ normalizeSelectedEntries,
+ snapshotExerciseForSelection,
+} from '../../utils/exerciseListSelection'
import { useExerciseListCatalogsAndQuery } from '../../hooks/useExerciseListCatalogsAndQuery'
import {
INITIAL_EXERCISE_LIST_FILTERS,
@@ -54,7 +59,7 @@ function ExercisesListPageRoot() {
const [pageTab, setPageTab] = useState('list')
const prefsAppliedRef = useRef(false)
- const [selectedIds, setSelectedIds] = useState(() => new Set())
+ const [selectedEntries, setSelectedEntries] = useState(() => [])
const [bulkModalOpen, setBulkModalOpen] = useState(false)
const [bulkVisibility, setBulkVisibility] = useState('')
const [bulkStatus, setBulkStatus] = useState('')
@@ -83,6 +88,7 @@ function ExercisesListPageRoot() {
setSearchInput(session.searchInput || '')
setAiSearchInput(session.aiSearchInput || '')
setMineOnly(session.mineOnly)
+ setSelectedEntries(normalizeSelectedEntries(session.selectedEntries))
}
try {
const sp = new URLSearchParams(window.location.search)
@@ -100,8 +106,9 @@ function ExercisesListPageRoot() {
searchInput,
aiSearchInput,
mineOnly,
+ selectedEntries,
})
- }, [filters, searchInput, aiSearchInput, mineOnly])
+ }, [filters, searchInput, aiSearchInput, mineOnly, selectedEntries])
useEffect(() => {
if (!user?.id) prefsAppliedRef.current = false
@@ -142,9 +149,20 @@ function ExercisesListPageRoot() {
loadMore,
} = useExerciseListCatalogsAndQuery({ queryBase, pageTab, tenantClubDepKey })
- useEffect(() => {
- setSelectedIds(new Set())
- }, [queryBase])
+ const selectedIds = useMemo(
+ () => new Set(selectedEntries.map((e) => Number(e.id)).filter((id) => Number.isFinite(id) && id > 0)),
+ [selectedEntries]
+ )
+
+ const selectedExercisesDisplay = useMemo(
+ () => mergeSelectedWithListEntries(selectedEntries, exercises),
+ [selectedEntries, exercises]
+ )
+
+ const filterResultExercises = useMemo(
+ () => exercises.filter((e) => !selectedIds.has(Number(e.id))),
+ [exercises, selectedIds]
+ )
const focusOptions = useMemo(
() =>
@@ -237,42 +255,46 @@ function ExercisesListPageRoot() {
? Number(user.active_club_id)
: null
- const toggleSelect = useCallback((id) => {
- setSelectedIds((prev) => {
- const n = new Set(prev)
- const nid = Number(id)
- if (Number.isNaN(nid)) return prev
- if (n.has(nid)) n.delete(nid)
- else n.add(nid)
- return n
+ const toggleSelect = useCallback((exercise) => {
+ const snap = snapshotExerciseForSelection(exercise)
+ if (!snap) return
+ setSelectedEntries((prev) => {
+ const idx = prev.findIndex((e) => Number(e.id) === snap.id)
+ if (idx >= 0) return prev.filter((_, i) => i !== idx)
+ return [...prev, snap]
})
}, [])
- const clearSelection = useCallback(() => setSelectedIds(new Set()), [])
+ const clearSelection = useCallback(() => setSelectedEntries([]), [])
const toggleSelectAllPage = useCallback(() => {
- setSelectedIds((prev) => {
- const n = new Set(prev)
- const allSel =
- exercises.length > 0 && exercises.every((e) => n.has(Number(e.id)))
+ setSelectedEntries((prev) => {
+ const ids = new Set(prev.map((e) => Number(e.id)))
+ const pageIds = filterResultExercises.map((e) => Number(e.id))
+ const allSel = pageIds.length > 0 && pageIds.every((id) => ids.has(id))
if (allSel) {
- exercises.forEach((e) => n.delete(Number(e.id)))
- } else {
- exercises.forEach((e) => n.add(Number(e.id)))
+ const remove = new Set(pageIds)
+ return prev.filter((e) => !remove.has(Number(e.id)))
}
- return n
+ const next = [...prev]
+ for (const ex of filterResultExercises) {
+ const snap = snapshotExerciseForSelection(ex)
+ if (!snap || ids.has(snap.id)) continue
+ ids.add(snap.id)
+ next.push(snap)
+ }
+ return next
})
- }, [exercises])
+ }, [filterResultExercises])
const allOnPageSelected = useMemo(
- () => exercises.length > 0 && exercises.every((e) => selectedIds.has(Number(e.id))),
- [exercises, selectedIds]
+ () =>
+ filterResultExercises.length > 0 &&
+ filterResultExercises.every((e) => selectedIds.has(Number(e.id))),
+ [filterResultExercises, selectedIds]
)
- const selectedExercisesInListOrder = useMemo(
- () => exercises.filter((e) => selectedIds.has(Number(e.id))),
- [exercises, selectedIds]
- )
+ const selectedExercisesInListOrder = selectedExercisesDisplay
const bulkVisibilityOptions = useMemo(() => {
const base = [
@@ -289,6 +311,7 @@ function ExercisesListPageRoot() {
try {
await api.deleteExercise(exercise.id)
setExercises((prev) => prev.filter((e) => e.id !== exercise.id))
+ setSelectedEntries((prev) => prev.filter((e) => Number(e.id) !== Number(exercise.id)))
} catch (err) {
alert('Fehler beim Löschen: ' + err.message)
}
@@ -582,45 +605,81 @@ function ExercisesListPageRoot() {
onClose={() => setPeekExercise(null)}
/>
- {listFetching && exercises.length === 0 ? (
+ {listFetching && exercises.length === 0 && selectedEntries.length === 0 ? (
- ) : exercises.length === 0 ? (
+ ) : exercises.length === 0 && selectedEntries.length === 0 ? (
) : (
<>
- {listFetching ? (
-
Aktualisiere Treffer…
+ {selectedEntries.length > 0 ? (
+
+
+
Auswahl ({selectedEntries.length})
+
+ Bleibt sichtbar, auch wenn du den Filter wechselst — ideal für die Modul-Zusammenstellung.
+
+
+
+ {selectedExercisesDisplay.map((exercise) => (
+ setPeekExercise({ id: ex.id, title: ex.title })}
+ selectionPinned
+ />
+ ))}
+
+
) : null}
-
- {exercises.length} angezeigt
- {hasMore ? ' · es gibt weitere Einträge' : ''}
-
-
- {exercises.map((exercise) => (
- setPeekExercise({ id: ex.id, title: ex.title })}
- />
- ))}
-
- {hasMore && (
-
-
-
+
+ {filterResultExercises.length === 0 ? (
+ selectedEntries.length > 0 ? (
+
+ Keine weiteren Treffer für den aktuellen Filter.
+
+ ) : null
+ ) : (
+ <>
+ {listFetching ? (
+
Aktualisiere Treffer…
+ ) : null}
+
+ {filterResultExercises.length} Treffer
+ {selectedEntries.length > 0 ? ' (ohne bereits Ausgewählte)' : ''}
+ {hasMore ? ' · es gibt weitere Einträge' : ''}
+
+
+ {filterResultExercises.map((exercise) => (
+ setPeekExercise({ id: ex.id, title: ex.title })}
+ />
+ ))}
+
+ {hasMore && (
+
+
+
+ )}
+ >
)}
>
)}
diff --git a/frontend/src/components/exercises/SaveSelectedExercisesAsModuleModal.jsx b/frontend/src/components/exercises/SaveSelectedExercisesAsModuleModal.jsx
index 1fddf41..15dc269 100644
--- a/frontend/src/components/exercises/SaveSelectedExercisesAsModuleModal.jsx
+++ b/frontend/src/components/exercises/SaveSelectedExercisesAsModuleModal.jsx
@@ -7,9 +7,11 @@ import { useToast } from '../../context/ToastContext'
import { useAuth } from '../../context/AuthContext'
import { activeClubMemberships, getDefaultClubIdForGovernanceForms } from '../../utils/activeClub'
import { hydrateExercisePlanningRow } from '../../utils/trainingUnitSectionsForm'
+import { buildRowsPayload, moduleItemToPayload } from '../../utils/exerciseListSelection'
/**
- * Erstellt ein Trainingsmodul aus per Checkbox ausgewählten Übungen (Reihenfolge = Listenreihenfolge).
+ * Erstellt ein Trainingsmodul aus per Checkbox ausgewählten Übungen (Reihenfolge = Auswahlreihenfolge).
+ * Optional: Positionen an ein bestehendes Modul anfügen.
*/
export default function SaveSelectedExercisesAsModuleModal({
open,
@@ -30,6 +32,11 @@ export default function SaveSelectedExercisesAsModuleModal({
const [loadErr, setLoadErr] = useState('')
/** @type {[Array