import { formatDurationDisplay, formatSessionDurationRange } from './trainingDurationUtils' import { frameworkSkillSummaryKey, maxSelectedSkillClubPercent, summaryHasSkill, } from './skillProfileListHelpers' export function frameworkSessionDurationLabel(row) { return formatSessionDurationRange(row?.session_duration_min, row?.session_duration_max, { empty: 'Dauer nicht angegeben', }) } /** Komma-getrennte Aggregat-Strings aus der Listen-API in Einträge zerlegen. */ export function splitFrameworkCommaAgg(value) { const s = (value ?? '').toString().trim() if (!s) return [] return s .split(',') .map((x) => x.trim()) .filter(Boolean) } /** Entwicklungsziele aus goal_titles_agg (Trenner „|“). */ export function splitFrameworkGoalsAgg(value) { const s = (value ?? '').toString().trim() if (!s) return [] return s .split('|') .map((x) => x.trim()) .filter(Boolean) } export function frameworkProgramHasCatalogMeta(row) { return ( splitFrameworkCommaAgg(row?.focus_area_names_agg).length > 0 || splitFrameworkCommaAgg(row?.style_direction_names_agg).length > 0 || splitFrameworkCommaAgg(row?.training_type_names_agg).length > 0 || splitFrameworkCommaAgg(row?.target_group_names_agg).length > 0 ) } 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 [] } /** 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) { 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 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, skillIds: [], skillSort: 'title', skillMinClubPercent: 0, skillDisplayLimit: 24, } 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 } if (f.durationMode === 'preset' && f.durationPresetMin != null) return true if ((f.skillIds || []).length) return true if (Number(f.skillMinClubPercent) > 0) return true if (f.skillSort === 'skill_strength' && (f.skillIds || []).length) 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.skillIds || []).length) { const names = f.skillIds.map((id) => nameById(catalogs.skills, id)) parts.push(`Fähigkeiten: ${names.join(', ')}`) } if (Number(f.skillMinClubPercent) > 0) { parts.push(`mind. ${f.skillMinClubPercent}% vom Rahmenprogramm-Maximum`) } if (f.skillSort === 'skill_strength' && (f.skillIds || []).length) { parts.push('Sortierung: Fähigkeiten-Stärke') } 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 } /** * Client-Filter für Rahmenprogramm-Auswahl (Liste / Import-Dialog). */ export function filterFrameworkPrograms(rows, filters = {}, skillSummaries = null) { 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)) const skillIds = f.skillIds || [] const minClubPct = Number(f.skillMinClubPercent) || 0 let list = (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 } 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 } return true }) if (skillIds.length && skillSummaries) { list = list.filter((r) => { const summary = skillSummaries[frameworkSkillSummaryKey(r.id)] if (!summary) return false return skillIds.some((sid) => summaryHasSkill(summary, sid, minClubPct)) }) } if (f.skillSort === 'skill_strength' && skillIds.length && skillSummaries) { list = [...list].sort((a, b) => { const sa = skillSummaries[frameworkSkillSummaryKey(a.id)] const sb = skillSummaries[frameworkSkillSummaryKey(b.id)] const pa = maxSelectedSkillClubPercent(sa, skillIds) ?? -1 const pb = maxSelectedSkillClubPercent(sb, skillIds) ?? -1 return pb - pa }) } return list } 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 }