diff --git a/frontend/src/app.css b/frontend/src/app.css
index 9f93655..72415e5 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -5116,6 +5116,7 @@ a.analysis-split__nav-item {
}
.tu-planning-module-band {
+ /* Legacy — Ersetzt durch .tu-module-bundle-head; wird aus Kompatibilität vorerst beibehalten. */
margin-top: 0.85rem;
margin-bottom: 0.05rem;
padding: 0.35rem 0.65rem;
@@ -5128,6 +5129,180 @@ a.analysis-split__nav-item {
letter-spacing: 0.01em;
}
+.tu-module-bundle-head {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ margin: 0.65rem 0 0.15rem;
+ border-radius: 14px;
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--border2));
+ background: linear-gradient(
+ 134deg,
+ color-mix(in srgb, var(--accent-light) 55%, var(--surface)) 0%,
+ var(--surface) 68%
+ );
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
+}
+
+.tu-module-bundle-head__stripe {
+ width: 5px;
+ flex-shrink: 0;
+ background: linear-gradient(180deg, var(--accent) 0%, color-mix(in srgb, var(--accent-dark) 92%, black) 100%);
+}
+
+.tu-module-bundle-head__main {
+ flex: 1;
+ min-width: 0;
+ padding: 0.6rem 0.75rem 0.72rem;
+}
+
+.tu-module-bundle-head__kicker {
+ display: block;
+ font-size: 0.65rem;
+ font-weight: 800;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+ color: color-mix(in srgb, var(--accent-dark) 88%, var(--text3));
+ margin-bottom: 0.12rem;
+}
+
+.tu-module-bundle-head__title {
+ display: block;
+ font-size: 1.02rem;
+ font-weight: 700;
+ color: var(--text1);
+ line-height: 1.25;
+ margin-bottom: 0.5rem;
+}
+
+.tu-module-bundle-head__list {
+ margin: 0;
+ padding-left: 1.15rem;
+ font-size: 0.84rem;
+ line-height: 1.48;
+ color: var(--text2);
+}
+
+.tu-module-bundle-head__list li {
+ margin-bottom: 0.12rem;
+}
+
+.tu-module-bundle-head__list li::marker {
+ color: var(--accent-dark);
+}
+
+.tu-module-bundle-head__more,
+.tu-module-bundle-head__meta,
+.tu-module-bundle-head__empty {
+ margin: 0.25rem 0 0;
+ font-size: 0.79rem;
+ line-height: 1.42;
+ color: var(--text3);
+}
+
+.tu-module-bundle-head__empty {
+ margin-top: 0.15rem;
+}
+
+.tu-modulepick-search {
+ width: 100%;
+ margin-bottom: 0.55rem;
+}
+
+.tu-modulepick-list {
+ max-height: 240px;
+ overflow-y: auto;
+ padding: 2px;
+ margin: 0 -2px;
+ border-radius: 10px;
+ border: 1px solid var(--border2);
+ background: color-mix(in srgb, var(--surface2) 22%, var(--surface));
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.tu-modulepick-item {
+ text-align: left;
+ appearance: none;
+ margin: 0;
+ cursor: pointer;
+ width: 100%;
+ padding: 0.45rem 0.62rem;
+ border-radius: 8px;
+ border: 1px solid transparent;
+ background: var(--surface);
+ color: var(--text1);
+ transition:
+ border-color 0.12s ease,
+ box-shadow 0.12s ease;
+}
+
+.tu-modulepick-item:hover {
+ border-color: color-mix(in srgb, var(--accent) 28%, transparent);
+}
+
+.tu-modulepick-item--active {
+ border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
+ box-shadow:
+ 0 0 0 1px color-mix(in srgb, var(--accent) 20%, transparent),
+ 0 2px 10px rgba(29, 158, 117, 0.08);
+}
+
+.tu-modulepick-item__title {
+ display: block;
+ font-size: 0.9rem;
+ font-weight: 700;
+}
+
+.tu-modulepick-item__meta {
+ display: block;
+ margin-top: 0.08rem;
+ font-size: 0.74rem;
+ color: var(--text3);
+ line-height: 1.38;
+}
+
+.tu-modulepick-preview {
+ margin-top: 0.85rem;
+ padding: 0.55rem 0.72rem;
+ border-radius: 10px;
+ border: 1px dashed var(--border2);
+ background: color-mix(in srgb, var(--surface2) 38%, transparent);
+}
+
+.tu-modulepick-preview__title {
+ margin: 0 0 0.45rem;
+ font-size: 0.74rem;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--text3);
+}
+
+.tu-modulepick-preview__list {
+ margin: 0;
+ padding-left: 1.1rem;
+ font-size: 0.82rem;
+ color: var(--text2);
+ line-height: 1.42;
+}
+
+.tu-modulepick-preview__more {
+ margin: 0.35rem 0 0;
+ font-size: 0.78rem;
+ color: var(--text3);
+}
+
+.tu-module-apply-placement-details summary {
+ cursor: pointer;
+ font-weight: 600;
+ font-size: 0.82rem;
+ color: var(--accent-dark);
+ margin: 0.25rem 0 0;
+}
+
/* Einfügen zwischen Ablaufzeilen (Übung / Modul / Anmerkung) */
.tu-insert-slot {
display: flex;
@@ -5186,6 +5361,47 @@ a.analysis-split__nav-item {
opacity: 0.92;
}
+.training-unit-sections-editor .tu-item-row {
+ border-top: none;
+ margin-top: 0;
+}
+
+.training-unit-sections-editor .tu-item-row--exercise {
+ margin-top: 0.58rem;
+ padding: 0.55rem 0.72rem 0.55rem;
+ border-radius: 14px;
+ border: 1px solid color-mix(in srgb, var(--border2) 85%, var(--accent) 15%);
+ background: linear-gradient(160deg, var(--surface) 0%, color-mix(in srgb, var(--surface2) 35%, var(--surface)) 100%);
+ box-shadow:
+ 0 1px 0 rgba(255, 255, 255, 0.56) inset,
+ 0 2px 10px rgba(15, 23, 42, 0.05);
+}
+
+.training-unit-sections-editor .tu-item-row--note:not(.tu-item-row--separator-note) {
+ margin-top: 0.52rem;
+ padding: 0.48rem 0.72rem;
+ border-radius: 11px;
+ border: 1px solid color-mix(in srgb, var(--border2) 70%, transparent);
+ background: color-mix(in srgb, var(--surface2) 28%, var(--surface));
+}
+
+.training-unit-sections-editor .tu-item-row--separator-note {
+ margin-top: 0.42rem;
+ border-radius: 10px;
+ border: 1px dashed color-mix(in srgb, var(--border2) 75%, transparent);
+ background: color-mix(in srgb, var(--surface2) 15%, transparent);
+}
+
+.training-unit-sections-editor .tu-item-row--from-module.tu-item-row--exercise,
+.training-unit-sections-editor .tu-item-row--from-module.tu-item-row--note:not(.tu-item-row--separator-note) {
+ border-left: 4px solid var(--accent);
+ padding-left: 0.6rem;
+ box-shadow:
+ 0 1px 0 rgba(255, 255, 255, 0.52) inset,
+ 0 2px 12px rgba(29, 158, 117, 0.07),
+ inset 10px 0 0 color-mix(in srgb, var(--accent) 14%, transparent);
+}
+
.tu-item-row {
display: flex;
flex-wrap: wrap;
diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx
index fee6313..d12002d 100644
--- a/frontend/src/components/TrainingUnitSectionsEditor.jsx
+++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx
@@ -32,6 +32,63 @@ function truncatePreview(text, max = 160) {
return `${t.slice(0, max - 1)}…`
}
+/** Liest den zusammenhängenden Lauf eines Moduls im Abschnitt (ab erstem Item mit dieser Herkunfts-ID). */
+function gatherPlanningModuleOutline(items, startIdx, moduleId) {
+ const exercises = []
+ let notes = 0
+ for (let j = startIdx; j < (items?.length ?? 0); j++) {
+ const row = items[j]
+ if (normalizedPlanningModuleChainId(row.source_training_module_id) !== moduleId) break
+ if (row.item_type === 'note') {
+ const bod = (row.note_body || '').trim()
+ if (bod === SECTION_INSERT_SEPARATOR_BODY) continue
+ notes += 1
+ continue
+ }
+ const t =
+ (row.exercise_title || '').trim() ||
+ (row.exercise_id ? `Übung #${row.exercise_id}` : 'Übung')
+ exercises.push(t)
+ }
+ return { exercises, notes }
+}
+
+const MODULE_OUTLINE_PREVIEW_MAX = 8
+
+function renderModulePlanningHead(modBandTitle, modOutline, showModuleBand) {
+ if (!showModuleBand || !modOutline) return null
+ return (
+
+
+
+
Aus Modul
+
{modBandTitle}
+ {modOutline.exercises.length === 0 && modOutline.notes === 0 ? (
+
Ohne strukturierten Inhalt angezeigt.
+ ) : (
+
+ {modOutline.exercises.slice(0, MODULE_OUTLINE_PREVIEW_MAX).map((tx, ox) => (
+ {tx}
+ ))}
+
+ )}
+ {modOutline.exercises.length > MODULE_OUTLINE_PREVIEW_MAX ? (
+
+ … und noch {modOutline.exercises.length - MODULE_OUTLINE_PREVIEW_MAX}{' '}
+ {modOutline.exercises.length - MODULE_OUTLINE_PREVIEW_MAX === 1 ? 'Übung' : 'Übungen'}
+
+ ) : null}
+ {modOutline.notes > 0 ? (
+
+ sowie {modOutline.notes}{' '}
+ {modOutline.notes === 1 ? 'Zwischen-Hinweis' : 'Zwischen-Hinweise'}
+
+ ) : null}
+
+
+ )
+}
+
function reorderBlocksImmutable(blocks, fromI, toBeforeIdx) {
const b = [...blocks]
if (fromI < 0 || fromI >= b.length) return blocks
@@ -598,25 +655,22 @@ export default function TrainingUnitSectionsEditor({
(it.source_module_title || '').trim() ||
(curMn != null ? `Modul #${curMn}` : '')
+ const modOutline =
+ showModuleBand && curMn != null ? gatherPlanningModuleOutline(sec.items, iIdx, curMn) : null
+ const fromModClass = curMn != null ? ' tu-item-row--from-module' : ''
+
if (it.item_type === 'note') {
const isSepLine = (it.note_body || '').trim() === SECTION_INSERT_SEPARATOR_BODY
const notePv = truncatePreview(it.note_body || '', 260)
const noteHasText = Boolean((it.note_body || '').trim()) && !isSepLine
return (
- {showModuleBand ? (
-
- Baustein: {modBandTitle}
-
- ) : null}
+ {renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
@@ -714,16 +768,8 @@ export default function TrainingUnitSectionsEditor({
return (
- {showModuleBand ? (
-
- Baustein: {modBandTitle}
-
- ) : null}
-
+ {renderModulePlanningHead(modBandTitle, modOutline, showModuleBand)}
+
{enableItemDragReorder ? (
{
+ const q = moduleApplySearchQuery.trim().toLowerCase().replace(/\s+/g, ' ')
+ const words = q ? q.split(' ').filter(Boolean) : []
+ const list = Array.isArray(moduleApplyList) ? moduleApplyList : []
+ if (!words.length) return list
+ return list.filter((m) => {
+ const blob = [
+ m.title,
+ m.summary,
+ m.goal,
+ m.target_group_notes,
+ m.deployment_context_notes,
+ ]
+ .map((x) => String(x ?? '').toLowerCase())
+ .join('\n')
+ return words.every((w) => blob.includes(w))
+ })
+ }, [moduleApplySearchQuery, moduleApplyList])
+
+ const modulePlacementSummary = useMemo(() => {
+ const secs = Array.isArray(formData.sections) ? formData.sections : []
+ let si =
+ typeof moduleApplySectionIx === 'number'
+ ? moduleApplySectionIx
+ : parseInt(String(moduleApplySectionIx), 10)
+ if (!Number.isFinite(si)) si = 0
+ si = Math.max(0, Math.min(si, secs.length ? secs.length - 1 : 0))
+ const cap = secs[si]?.items?.length ?? 0
+ let beforeIx = cap
+ if (typeof moduleApplyInsertSlot === 'string' && moduleApplyInsertSlot.startsWith('before:')) {
+ const zi = parseInt(moduleApplyInsertSlot.slice('before:'.length), 10)
+ if (Number.isFinite(zi)) beforeIx = Math.min(Math.max(0, zi), cap)
+ }
+ const rawTitle = (secs[si]?.title || '').trim()
+ const secTitle = rawTitle || `Abschnitt ${si + 1}`
+ let positionDescription
+ if (cap <= 0) positionDescription = 'als erste Einträge dieses Abschnitts'
+ else if (beforeIx <= 0) positionDescription = 'vor dem ersten Eintrag dieses Abschnitts'
+ else if (beforeIx >= cap) positionDescription = 'nach dem letzten Eintrag dieses Abschnitts'
+ else positionDescription = `unmittelbar vor Eintrag ${beforeIx + 1} (${cap} Einträge im Abschnitt)`
+ return { secTitle, positionDescription }
+ }, [formData.sections, moduleApplySectionIx, moduleApplyInsertSlot])
+
+ useEffect(() => {
+ if (!moduleApplyOpen || !moduleApplyFilteredList.length) return
+ if (moduleApplyFilteredList.some((m) => String(m.id) === String(moduleApplyModuleId))) return
+ setModuleApplyModuleId(String(moduleApplyFilteredList[0].id))
+ }, [moduleApplyOpen, moduleApplyFilteredList, moduleApplyModuleId])
+
const planningModalClubId = useMemo(() => {
const gid = Number(formData.group_id)
if (!Number.isFinite(gid) || gid < 1) return null
@@ -686,6 +753,12 @@ function TrainingPlanningPage() {
const openModuleApplyModal = useCallback(async (placement) => {
setModuleApplyErr('')
+ setModuleApplySearchQuery('')
+ const placementLocked =
+ placement != null &&
+ typeof placement.sectionIndex === 'number' &&
+ typeof placement.insertBeforeIndex === 'number'
+ setModuleApplyPlacementLocked(placementLocked)
const secs = planningFormRef.current?.sections ?? []
let secIx = 0
let before = 0
@@ -754,6 +827,7 @@ function TrainingPlanningPage() {
nextSections = await enrichSectionsWithVariants(nextSections)
setFormData((fd) => ({ ...fd, sections: nextSections }))
setModuleApplyOpen(false)
+ setModuleApplyPlacementLocked(false)
} catch (e) {
setModuleApplyErr(e.message || 'Einfügen fehlgeschlagen')
} finally {
@@ -761,6 +835,98 @@ function TrainingPlanningPage() {
}
}, [moduleApplyModuleId, moduleApplySectionIx, moduleApplyInsertSlot])
+ useEffect(() => {
+ if (!moduleApplyOpen) {
+ setModulePickPreview({
+ loading: false,
+ moduleId: '',
+ exercises: [],
+ notes: 0,
+ err: '',
+ })
+ return undefined
+ }
+ const mid = parseInt(String(moduleApplyModuleId), 10)
+ if (!Number.isFinite(mid) || mid < 1) {
+ setModulePickPreview({
+ loading: false,
+ moduleId: '',
+ exercises: [],
+ notes: 0,
+ err: '',
+ })
+ return undefined
+ }
+ let cancelled = false
+ setModulePickPreview({
+ loading: true,
+ moduleId: String(mid),
+ exercises: [],
+ notes: 0,
+ err: '',
+ })
+ ;(async () => {
+ try {
+ const detail = await api.getTrainingModule(mid)
+ if (cancelled) return
+ const itemsSorted = [...(detail.items ?? [])].sort(
+ (a, b) => (a.order_index ?? 0) - (b.order_index ?? 0)
+ )
+ const uniqueEx = new Set()
+ let notes = 0
+ for (const row of itemsSorted) {
+ if ((row.item_type || '') !== 'note') {
+ const eid = row.exercise_id
+ if (eid) uniqueEx.add(Number(eid))
+ continue
+ }
+ const b = String(row.note_body ?? '').trim()
+ if (b === '---') continue
+ notes += 1
+ }
+ const titleById = new Map()
+ await Promise.all(
+ [...uniqueEx].map(async (eid) => {
+ try {
+ const ex = await api.getExercise(eid)
+ titleById.set(eid, (ex?.title || '').trim() || `Übung #${eid}`)
+ } catch {
+ titleById.set(eid, `Übung #${eid}`)
+ }
+ })
+ )
+ if (cancelled) return
+ const exTitlesInOrder = []
+ for (const row of itemsSorted) {
+ if ((row.item_type || '') !== 'exercise') continue
+ const eid = Number(row.exercise_id)
+ if (!Number.isFinite(eid)) continue
+ exTitlesInOrder.push(titleById.get(eid) || `Übung #${eid}`)
+ }
+ setModulePickPreview({
+ loading: false,
+ moduleId: String(mid),
+ exercises: exTitlesInOrder,
+ notes,
+ err: '',
+ })
+ } catch (e) {
+ if (!cancelled) {
+ setModulePickPreview({
+ loading: false,
+ moduleId: String(mid),
+ exercises: [],
+ notes: 0,
+ err: e?.message || 'Vorschau fehlgeschlagen',
+ })
+ }
+ }
+ })()
+ return () => {
+ cancelled = true
+ }
+ }, [moduleApplyOpen, moduleApplyModuleId])
+
const handleTakeLead = async (unit) => {
if (!user?.id) return
try {
@@ -1919,13 +2085,17 @@ function TrainingPlanningPage() {
overflowY: 'auto',
}}
role="presentation"
- onMouseDown={(ev) => ev.target === ev.currentTarget && !moduleApplyBusy && setModuleApplyOpen(false)}
+ onMouseDown={(ev) => {
+ if (ev.target !== ev.currentTarget || moduleApplyBusy) return
+ setModuleApplyOpen(false)
+ setModuleApplyPlacementLocked(false)
+ }}
>
- Modul einfügen
+ Trainingsmodul einfügen
-
- Übungen und Notizen des Moduls werden kopiert wie bei einer einzelnen Übung —
- ohne die Einheit vorher gespeichert zu haben (Speichern am Ende wie gewohnt). Die Herkunft bleibt
- am Block sichtbar; du kannst alles weiter anpassen.
+
+ Alle Positionen des gewählten Moduls werden als neue Zeilen eingefügt (Kopie, mit klarer
+ Herkunft im Ablauf). Die Einheit brauchst du dafür nicht vorher gespeichert zu haben — Speichern am Ende
+ wie gewohnt. Vollständige Textsuche oder Modulkategorien planen wir serverseitig für
+ eine spätere Iteration; vorerst steht hier eine{' '}
+ Schnellsuche über Titel und Freitext-Felder zur Verfügung.
{moduleApplyErr ? (
{moduleApplyErr}
) : null}
-
- Modul
- setModuleApplyModuleId(e.target.value)}
- disabled={moduleApplyBusy || !moduleApplyList.length}
- >
- {!moduleApplyList.length ? (
- Keine Module verfügbar
- ) : null}
- {moduleApplyList.map((m) => (
-
- {(m.title || '').trim() || `Modul #${m.id}`}
-
- ))}
-
-
+ {moduleApplyPlacementLocked ? (
+ <>
+
+ Aktuelle Einfügeposition: Abschnitt {modulePlacementSummary.secTitle} {' '}
+ / {modulePlacementSummary.positionDescription}
+
+
+ Abschnitt oder Position ändern
+
+
+ Abschnitt (Reihenfolge wie im Editor)
+ {
+ const newIx = parseInt(e.target.value, 10)
+ setModuleApplySectionIx(newIx)
+ const secsNow = planningFormRef.current?.sections ?? []
+ const len = Array.isArray(secsNow[newIx]?.items) ? secsNow[newIx].items.length : 0
+ setModuleApplyInsertSlot(`before:${len}`)
+ }}
+ disabled={moduleApplyBusy || !formData.sections?.length}
+ >
+ {(formData.sections || []).map((s, i) => (
+
+ {(s.title || `Abschnitt ${i + 1}`).trim()}
+
+ ))}
+
+
+
+ Position in diesem Abschnitt
+ setModuleApplyInsertSlot(e.target.value)}
+ disabled={moduleApplyBusy || !(formData.sections?.length > 0)}
+ >
+
+ Am Ende einfügen (nach allen Einträgen)
+
+ An den Anfang (vor dem ersten Eintrag)
+ {moduleApplyTargetItems.map((row, xi) => {
+ const labelPart =
+ row.item_type === 'note'
+ ? 'Zwischen-Anmerkung'
+ : (row.exercise_title || '').trim() || `Übung #${row.exercise_id || '—'}`
+ const clipped =
+ labelPart.length > 44 ? `${labelPart.slice(0, 43).trim()}…` : labelPart
+ return (
+
+ Vor Eintrag {xi + 1}: {clipped}
+
+ )
+ })}
+
+
+
+
+ >
+ ) : (
+ <>
+
+ Abschnitt (Reihenfolge wie im Editor)
+ {
+ const newIx = parseInt(e.target.value, 10)
+ setModuleApplySectionIx(newIx)
+ const secsNow = planningFormRef.current?.sections ?? []
+ const len = Array.isArray(secsNow[newIx]?.items) ? secsNow[newIx].items.length : 0
+ setModuleApplyInsertSlot(`before:${len}`)
+ }}
+ disabled={moduleApplyBusy || !formData.sections?.length}
+ >
+ {(formData.sections || []).map((s, i) => (
+
+ {(s.title || `Abschnitt ${i + 1}`).trim()}
+
+ ))}
+
+
-
- Ziel‑Abschnitt (Reihenfolge wie im Editor)
- {
- const newIx = parseInt(e.target.value, 10)
- setModuleApplySectionIx(newIx)
- const secsNow = planningFormRef.current?.sections ?? []
- const len = Array.isArray(secsNow[newIx]?.items) ? secsNow[newIx].items.length : 0
- setModuleApplyInsertSlot(`before:${len}`)
- }}
- disabled={moduleApplyBusy || !formData.sections?.length}
- >
- {(formData.sections || []).map((s, i) => (
-
- {(s.title || `Abschnitt ${i + 1}`).trim()}
-
- ))}
-
-
-
-
-
Position in diesem Abschnitt
-
setModuleApplyInsertSlot(e.target.value)}
- disabled={moduleApplyBusy || !(formData.sections?.length > 0)}
- >
-
- Ans Ende einfügen (nach allen Einträgen)
-
- An den Anfang (vor dem ersten Eintrag)
- {moduleApplyTargetItems.map((row, xi) => {
- const labelPart =
- row.item_type === 'note'
- ? 'Zwischen-Anmerkung'
- : (row.exercise_title || '').trim() || `Übung #${row.exercise_id || '—'}`
- const clipped =
- labelPart.length > 44 ? `${labelPart.slice(0, 43).trim()}…` : labelPart
- return (
-
- Vor Eintrag {xi + 1}: {clipped}
+
+ Position in diesem Abschnitt
+ setModuleApplyInsertSlot(e.target.value)}
+ disabled={moduleApplyBusy || !(formData.sections?.length > 0)}
+ >
+
+ Am Ende einfügen (nach allen Einträgen)
- )
- })}
-
+ An den Anfang (vor dem ersten Eintrag)
+ {moduleApplyTargetItems.map((row, xi) => {
+ const labelPart =
+ row.item_type === 'note'
+ ? 'Zwischen-Anmerkung'
+ : (row.exercise_title || '').trim() || `Übung #${row.exercise_id || '—'}`
+ const clipped =
+ labelPart.length > 44 ? `${labelPart.slice(0, 43).trim()}…` : labelPart
+ return (
+
+ Vor Eintrag {xi + 1}: {clipped}
+
+ )
+ })}
+
+
+ >
+ )}
+
+
+ Suche Module
+ setModuleApplySearchQuery(e.target.value)}
+ disabled={moduleApplyBusy}
+ aria-label="Module durch Freitext filtern"
+ />
+
+
+ Modulliste
+
+
+
+ {!moduleApplyFilteredList.length ? (
+
+ {!moduleApplyList.length ? 'Keine Module verfügbar oder keine Berechtigung.' : 'Kein Modul entspricht der Suche.'}
+
+ ) : (
+ moduleApplyFilteredList.map((m) => {
+ const title = ((m.title || '').trim() || `Modul #${m.id}`).trim()
+ const visLbl = trainingVisibilityShortDE(m.visibility)
+ const nPos = typeof m.items_count === 'number' ? m.items_count : '—'
+ const selected = String(m.id) === String(moduleApplyModuleId)
+ return (
+
setModuleApplyModuleId(String(m.id))}
+ >
+ {title}
+
+ {nPos} {typeof nPos === 'number' ? (nPos === 1 ? 'Position' : 'Positionen') : 'Position(en)'}
+ {visLbl ? <> · {visLbl}> : null}
+ {m.summary ? <> · {(m.summary || '').trim().slice(0, 72)}{(m.summary || '').trim().length > 72 ? '…' : ''}> : null}
+
+
+ )
+ })
+ )}
+
+
+ {moduleApplyModuleId ? (
+
+
Ablauf-Vorschau (Bibliotheksmodul)
+ {modulePickPreview.loading ? (
+
+ Übungen und Hinweise laden …
+
+ ) : modulePickPreview.err ? (
+
+ {modulePickPreview.err}
+
+ ) : !modulePickPreview.exercises.length && !modulePickPreview.notes ? (
+
+ Keine Übungspositionen in diesem Eintrag gefunden (prüfen, ob Übungen im Modul gültige IDs haben).
+
+ ) : (
+ <>
+
+ {(modulePickPreview.exercises.slice(0, 12)).map((t, qi) => (
+ {t}
+ ))}
+
+ {modulePickPreview.exercises.length > 12 ? (
+
+ … und noch {modulePickPreview.exercises.length - 12} weitere Übungen in genau dieser Modulreihenfolge.
+
+ ) : null}
+ {modulePickPreview.notes > 0 ? (
+
+ zusätzlich {modulePickPreview.notes}{' '}
+ {modulePickPreview.notes === 1 ? 'Position mit Hinweis' : 'Positionen mit Hinweisen'}{' '}
+ (ohne Aufzählung)
+
+ ) : null}
+ >
+ )}
+
+ ) : null}
+
!moduleApplyBusy && setModuleApplyOpen(false)}
+ onClick={() => {
+ if (moduleApplyBusy) return
+ setModuleApplyOpen(false)
+ setModuleApplyPlacementLocked(false)
+ }}
>
Abbrechen