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
- 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.
269 lines
9.1 KiB
JavaScript
269 lines
9.1 KiB
JavaScript
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
|
||
}
|