2026-05-20 15:06:03 +02:00
|
|
|
|
import { formatDurationDisplay, formatSessionDurationRange } from './trainingDurationUtils'
|
2026-05-20 13:19:40 +02:00
|
|
|
|
|
|
|
|
|
|
export function frameworkSessionDurationLabel(row) {
|
|
|
|
|
|
return formatSessionDurationRange(row?.session_duration_min, row?.session_duration_max, {
|
|
|
|
|
|
empty: 'Dauer nicht angegeben',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseIdList(raw) {
|
|
|
|
|
|
if (Array.isArray(raw)) {
|
|
|
|
|
|
return raw.map((x) => String(x)).filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof raw === 'string' && raw.trim().startsWith('[')) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const arr = JSON.parse(raw)
|
|
|
|
|
|
if (Array.isArray(arr)) return arr.map((x) => String(x))
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* ignore */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 15:06:03 +02:00
|
|
|
|
/** Eindeutige Session-Dauern (Minuten) aus allen Rahmen in der Liste. */
|
|
|
|
|
|
export function collectDistinctSessionDurationsMinutes(rows) {
|
|
|
|
|
|
const set = new Set()
|
|
|
|
|
|
for (const r of rows || []) {
|
|
|
|
|
|
const lo = r.session_duration_min
|
|
|
|
|
|
const hi = r.session_duration_max
|
|
|
|
|
|
if (lo != null && lo !== '' && Number.isFinite(Number(lo))) set.add(Number(lo))
|
|
|
|
|
|
if (hi != null && hi !== '' && Number.isFinite(Number(hi))) set.add(Number(hi))
|
|
|
|
|
|
}
|
|
|
|
|
|
return [...set].sort((a, b) => a - b)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function programDurationBounds(row) {
|
2026-05-20 13:19:40 +02:00
|
|
|
|
const lo = row.session_duration_min != null ? Number(row.session_duration_min) : null
|
|
|
|
|
|
const hi = row.session_duration_max != null ? Number(row.session_duration_max) : null
|
2026-05-20 15:06:03 +02:00
|
|
|
|
if (lo != null && Number.isFinite(lo) && lo > 0) {
|
|
|
|
|
|
const hiEff = hi != null && Number.isFinite(hi) && hi > 0 ? hi : lo
|
|
|
|
|
|
return { lo, hi: hiEff }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hi != null && Number.isFinite(hi) && hi > 0) {
|
|
|
|
|
|
return { lo: hi, hi }
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Überlappung Programm-Session-Spanne mit Filter-Spanne (Minuten). */
|
|
|
|
|
|
function rowMatchesDurationRange(row, fromMin, toMin) {
|
|
|
|
|
|
const hasFrom = fromMin != null && fromMin !== '' && !Number.isNaN(Number(fromMin))
|
|
|
|
|
|
const hasTo = toMin != null && toMin !== '' && !Number.isNaN(Number(toMin))
|
|
|
|
|
|
if (!hasFrom && !hasTo) return true
|
|
|
|
|
|
|
|
|
|
|
|
const bounds = programDurationBounds(row)
|
|
|
|
|
|
if (!bounds) return false
|
|
|
|
|
|
|
|
|
|
|
|
const fLo = hasFrom ? Number(fromMin) : bounds.lo
|
|
|
|
|
|
const fHi = hasTo ? Number(toMin) : hasFrom ? Number(fromMin) : bounds.hi
|
|
|
|
|
|
const filterLo = Math.min(fLo, fHi)
|
|
|
|
|
|
const filterHi = Math.max(fLo, fHi)
|
|
|
|
|
|
|
|
|
|
|
|
return bounds.lo <= filterHi && bounds.hi >= filterLo
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function rowMatchesDurationPreset(row, presetMin, toleranceMin = 10) {
|
|
|
|
|
|
if (presetMin == null || presetMin === '' || Number.isNaN(Number(presetMin))) return true
|
|
|
|
|
|
const t = Number(presetMin)
|
|
|
|
|
|
const bounds = programDurationBounds(row)
|
|
|
|
|
|
if (!bounds) return false
|
|
|
|
|
|
return t >= bounds.lo - toleranceMin && t <= bounds.hi + toleranceMin
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const EMPTY_FRAMEWORK_IMPORT_FILTERS = {
|
|
|
|
|
|
query: '',
|
|
|
|
|
|
focusAreaIds: [],
|
|
|
|
|
|
trainingTypeIds: [],
|
|
|
|
|
|
targetGroupIds: [],
|
|
|
|
|
|
durationMode: 'any',
|
|
|
|
|
|
durationRangeFrom: '',
|
|
|
|
|
|
durationRangeTo: '',
|
|
|
|
|
|
durationPresetMin: null,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function hasActiveFrameworkImportFilters(filters = {}) {
|
|
|
|
|
|
const f = { ...EMPTY_FRAMEWORK_IMPORT_FILTERS, ...filters }
|
|
|
|
|
|
if ((f.query || '').trim()) return true
|
|
|
|
|
|
if ((f.focusAreaIds || []).length) return true
|
|
|
|
|
|
if ((f.trainingTypeIds || []).length) return true
|
|
|
|
|
|
if ((f.targetGroupIds || []).length) return true
|
|
|
|
|
|
if (f.durationMode === 'range') {
|
|
|
|
|
|
if (String(f.durationRangeFrom || '').trim() !== '') return true
|
|
|
|
|
|
if (String(f.durationRangeTo || '').trim() !== '') return true
|
2026-05-20 13:19:40 +02:00
|
|
|
|
}
|
2026-05-20 15:06:03 +02:00
|
|
|
|
if (f.durationMode === 'preset' && f.durationPresetMin != null) return true
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Kurzbeschreibung aktiver Filter (für Zusammenfassung außerhalb des Panels).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function summarizeFrameworkImportFilters(filters = {}, catalogs = {}) {
|
|
|
|
|
|
const f = { ...EMPTY_FRAMEWORK_IMPORT_FILTERS, ...filters }
|
|
|
|
|
|
const parts = []
|
|
|
|
|
|
const q = (f.query || '').trim()
|
|
|
|
|
|
if (q) parts.push(`Suche: „${q}"`)
|
|
|
|
|
|
|
|
|
|
|
|
const nameById = (list, id) => list?.find((x) => String(x.id) === String(id))?.name || id
|
|
|
|
|
|
|
|
|
|
|
|
if ((f.focusAreaIds || []).length) {
|
|
|
|
|
|
const names = f.focusAreaIds.map((id) => nameById(catalogs.focusAreas, id))
|
|
|
|
|
|
parts.push(`Fokus: ${names.join(', ')}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if ((f.trainingTypeIds || []).length) {
|
|
|
|
|
|
const names = f.trainingTypeIds.map((id) => nameById(catalogs.trainingTypes, id))
|
|
|
|
|
|
parts.push(`Trainingsart: ${names.join(', ')}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if ((f.targetGroupIds || []).length) {
|
|
|
|
|
|
const names = f.targetGroupIds.map((id) => nameById(catalogs.targetGroups, id))
|
|
|
|
|
|
parts.push(`Zielgruppe: ${names.join(', ')}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (f.durationMode === 'range') {
|
|
|
|
|
|
const a = String(f.durationRangeFrom || '').trim()
|
|
|
|
|
|
const b = String(f.durationRangeTo || '').trim()
|
|
|
|
|
|
if (a || b) {
|
|
|
|
|
|
const fromLbl = a ? formatDurationDisplay(Number(a), { empty: a }) : '—'
|
|
|
|
|
|
const toLbl = b ? formatDurationDisplay(Number(b), { empty: b }) : '—'
|
|
|
|
|
|
parts.push(`Dauer: ${fromLbl} – ${toLbl}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (f.durationMode === 'preset' && f.durationPresetMin != null) {
|
|
|
|
|
|
parts.push(`Dauer: ${formatDurationDisplay(f.durationPresetMin)}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return parts
|
2026-05-20 13:19:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Client-Filter für Rahmenprogramm-Auswahl (Liste / Import-Dialog).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function filterFrameworkPrograms(rows, filters = {}) {
|
2026-05-20 15:06:03 +02:00
|
|
|
|
const f = { ...EMPTY_FRAMEWORK_IMPORT_FILTERS, ...filters }
|
|
|
|
|
|
const q = (f.query || '').trim().toLowerCase()
|
|
|
|
|
|
const focusIds = new Set((f.focusAreaIds || []).map(String))
|
|
|
|
|
|
const typeIds = new Set((f.trainingTypeIds || []).map(String))
|
|
|
|
|
|
const tgIds = new Set((f.targetGroupIds || []).map(String))
|
2026-05-20 13:19:40 +02:00
|
|
|
|
|
|
|
|
|
|
return (rows || []).filter((r) => {
|
|
|
|
|
|
if (q) {
|
|
|
|
|
|
const blob = [
|
|
|
|
|
|
r.title,
|
|
|
|
|
|
r.description,
|
|
|
|
|
|
r.goal_titles_agg,
|
|
|
|
|
|
r.focus_area_names_agg,
|
|
|
|
|
|
r.style_direction_names_agg,
|
|
|
|
|
|
r.training_type_names_agg,
|
|
|
|
|
|
r.target_group_names_agg,
|
|
|
|
|
|
]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(' ')
|
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
|
if (!blob.includes(q)) return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (focusIds.size) {
|
|
|
|
|
|
const fa = parseIdList(r.focus_area_ids)
|
|
|
|
|
|
if (!fa.some((id) => focusIds.has(id))) return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeIds.size) {
|
|
|
|
|
|
const tt = parseIdList(r.training_type_ids)
|
|
|
|
|
|
if (!tt.some((id) => typeIds.has(id))) return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tgIds.size) {
|
|
|
|
|
|
const tg = parseIdList(r.target_group_ids)
|
|
|
|
|
|
if (!tg.some((id) => tgIds.has(id))) return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 15:06:03 +02:00
|
|
|
|
if (f.durationMode === 'range') {
|
|
|
|
|
|
if (!rowMatchesDurationRange(r, f.durationRangeFrom, f.durationRangeTo)) return false
|
|
|
|
|
|
} else if (f.durationMode === 'preset') {
|
|
|
|
|
|
if (!rowMatchesDurationPreset(r, f.durationPresetMin)) return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 13:19:40 +02:00
|
|
|
|
return true
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function frameworkProgramOptionLabel(row) {
|
|
|
|
|
|
const title = (row?.title || '').trim() || `Rahmen #${row?.id}`
|
|
|
|
|
|
const dur = frameworkSessionDurationLabel(row)
|
|
|
|
|
|
const slots = row?.slots_count != null ? `${row.slots_count} Slot(s)` : ''
|
|
|
|
|
|
const bits = [dur !== 'Dauer nicht angegeben' ? dur : null, slots].filter(Boolean)
|
|
|
|
|
|
return bits.length ? `${title} · ${bits.join(' · ')}` : title
|
|
|
|
|
|
}
|