shinkan-jinkendo/frontend/src/utils/frameworkProgramListHelpers.js
Lars 2de4c0b7c9
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
Refactor Skill Scoring Functions and Enhance Corpus Handling
- Introduced new helper functions for managing artifact type corpus, improving code organization and readability.
- Updated the `compute_club_corpus_reference` function to utilize the new corpus handling methods, enhancing clarity and maintainability.
- Refactored skill profile functions to leverage the new corpus structure, ensuring consistent data retrieval across different artifact types.
- Improved the handling of visibility clauses for library content, streamlining database queries for skill profiles.
- Enhanced the batch skill profile summary function to aggregate reference data by artifact type, improving performance and accuracy.
2026-05-21 10:17:22 +02:00

269 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}