shinkan-jinkendo/frontend/src/utils/frameworkProgramListHelpers.js

195 lines
6.7 KiB
JavaScript
Raw Normal View History

import { formatDurationDisplay, formatSessionDurationRange } from './trainingDurationUtils'
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 []
}
/** 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,
}
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
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
}
/**
* Client-Filter für Rahmenprogramm-Auswahl (Liste / Import-Dialog).
*/
export function filterFrameworkPrograms(rows, filters = {}) {
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))
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
}
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
})
}
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
}