Refactor Progression Path Evaluation and Comparison Logic
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
- Updated `suggest_progression_path` to utilize `evaluate_steps` for improved validation, ensuring at least one evaluation step is provided. - Modified frontend components to enhance user experience in the comparison process, including clearer messaging and improved dialog handling. - Adjusted `ProgressionGraphEditor` to streamline the comparison flow and integrate new evaluation parameters. - Enhanced `ProgressionOptimizeCompareModal` to reflect changes in comparison logic, allowing for better user interaction with proposed path suggestions. - Bumped version to reflect the new features and improvements.
This commit is contained in:
parent
69ce3f6975
commit
3f130aa8ad
|
|
@ -2448,18 +2448,16 @@ def suggest_progression_path(
|
||||||
raise HTTPException(status_code=403, detail="Nur Trainer dürfen Pfad-Vorschläge abrufen")
|
raise HTTPException(status_code=403, detail="Nur Trainer dürfen Pfad-Vorschläge abrufen")
|
||||||
|
|
||||||
if body.compare_with_assignments:
|
if body.compare_with_assignments:
|
||||||
assignments = _slot_assignments_by_major_index(body.slot_assignments)
|
eval_source = list(body.evaluate_steps or body.slot_assignments or [])
|
||||||
if len(assignments) < 1:
|
if len(eval_source) < 1:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="compare_with_assignments erfordert mindestens ein slot_assignment",
|
detail="compare_with_assignments erfordert evaluate_steps",
|
||||||
)
|
)
|
||||||
baseline_body = body.model_copy(
|
baseline_body = body.model_copy(
|
||||||
update={
|
update={
|
||||||
"evaluate_only": True,
|
"evaluate_only": True,
|
||||||
"evaluate_steps": list(
|
"evaluate_steps": eval_source,
|
||||||
body.evaluate_steps or body.slot_assignments or []
|
|
||||||
),
|
|
||||||
"compare_with_assignments": False,
|
"compare_with_assignments": False,
|
||||||
"preserve_slot_assignments": False,
|
"preserve_slot_assignments": False,
|
||||||
# Gleiche QS-Pipeline wie „Graph bewerten“ (kein Match/Rematch-Schönung)
|
# Gleiche QS-Pipeline wie „Graph bewerten“ (kein Match/Rematch-Schönung)
|
||||||
|
|
@ -2472,7 +2470,7 @@ def suggest_progression_path(
|
||||||
proposed_body = body.model_copy(
|
proposed_body = body.model_copy(
|
||||||
update={
|
update={
|
||||||
"compare_with_assignments": False,
|
"compare_with_assignments": False,
|
||||||
"preserve_slot_assignments": True,
|
"preserve_slot_assignments": False,
|
||||||
"evaluate_only": False,
|
"evaluate_only": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -350,8 +350,8 @@ export default function ProgressionFindingsPanel({
|
||||||
</strong>
|
</strong>
|
||||||
{pathQa.assignments_preserved ? (
|
{pathQa.assignments_preserved ? (
|
||||||
<p style={{ margin: '6px 0 0', color: 'var(--text2)', fontSize: '11px' }}>
|
<p style={{ margin: '6px 0 0', color: 'var(--text2)', fontSize: '11px' }}>
|
||||||
Bewertung des aktuellen Pfads — keine automatische Umzuordnung.
|
Bewertung des aktuellen Pfads. „Übungen matchen“ öffnet einen Dialog mit Vorschlägen für
|
||||||
{showOptimizeCompare ? ' Match prüft zusätzlich Optimierungsvorschläge im Vergleich.' : ''}
|
alle Slots — Übernahme nur nach deiner Auswahl.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{strongResult ? (
|
{strongResult ? (
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,11 @@ import {
|
||||||
addSlotToDraft,
|
addSlotToDraft,
|
||||||
applyEvaluateResponseToDraft,
|
applyEvaluateResponseToDraft,
|
||||||
applyGapOfferToDraft,
|
applyGapOfferToDraft,
|
||||||
applyMatchResponseToDraft,
|
|
||||||
applySelectedCompareSteps,
|
applySelectedCompareSteps,
|
||||||
compareResponseHasActionableSlotChanges,
|
|
||||||
compareResponseHadRematchWithoutActionableDiffs,
|
|
||||||
compareSlotDiffs,
|
compareSlotDiffs,
|
||||||
|
collectGapOffersFromApiResponse,
|
||||||
|
dedupeGapOffersBySlot,
|
||||||
|
filterGapOffersForUnfilledSlots,
|
||||||
pathQaQualityPercent,
|
pathQaQualityPercent,
|
||||||
applyResolvedStructuredToDraft,
|
applyResolvedStructuredToDraft,
|
||||||
buildPlanningArtifactFromDraft,
|
buildPlanningArtifactFromDraft,
|
||||||
|
|
@ -120,6 +120,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
const [activePlanningContextLines, setActivePlanningContextLines] = useState([])
|
const [activePlanningContextLines, setActivePlanningContextLines] = useState([])
|
||||||
const [compareOpen, setCompareOpen] = useState(false)
|
const [compareOpen, setCompareOpen] = useState(false)
|
||||||
const [comparePayload, setComparePayload] = useState(null)
|
const [comparePayload, setComparePayload] = useState(null)
|
||||||
|
const [compareSource, setCompareSource] = useState('manual')
|
||||||
const [comparing, setComparing] = useState(false)
|
const [comparing, setComparing] = useState(false)
|
||||||
const [compareApplying, setCompareApplying] = useState(false)
|
const [compareApplying, setCompareApplying] = useState(false)
|
||||||
const [proposedPathQa, setProposedPathQa] = useState(null)
|
const [proposedPathQa, setProposedPathQa] = useState(null)
|
||||||
|
|
@ -487,11 +488,10 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchOptimizeCompare = async (synced) => {
|
const fetchMatchCompare = async (synced) => {
|
||||||
const res = await api.suggestProgressionPath({
|
const res = await api.suggestProgressionPath({
|
||||||
...buildMatchRequestBase(synced),
|
...buildMatchRequestBase(synced),
|
||||||
evaluate_steps: slotsToEvaluateSteps(synced),
|
evaluate_steps: slotsToEvaluateSteps(synced),
|
||||||
preserve_slot_assignments: true,
|
|
||||||
compare_with_assignments: true,
|
compare_with_assignments: true,
|
||||||
})
|
})
|
||||||
if (!res?.comparison_mode) {
|
if (!res?.comparison_mode) {
|
||||||
|
|
@ -500,44 +500,43 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
const presentOptimizeCompare = (res, { source = 'manual' } = {}) => {
|
const gapOffersFromMatchResponse = (synced, res) =>
|
||||||
|
filterGapOffersForUnfilledSlots(
|
||||||
|
synced,
|
||||||
|
dedupeGapOffersBySlot(collectGapOffersFromApiResponse(res), synced),
|
||||||
|
)
|
||||||
|
|
||||||
|
const presentMatchCompare = (res, { source = 'manual' } = {}) => {
|
||||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||||
setTargetSummary(res?.target_profile_summary || null)
|
setTargetSummary(res?.target_profile_summary || null)
|
||||||
|
setComparePayload(res)
|
||||||
|
setCompareSource(source)
|
||||||
|
setProposedPathQa(res?.proposed_path_qa_pipeline || null)
|
||||||
|
setCompareOpen(true)
|
||||||
|
|
||||||
const baselineQa = res?.baseline_path_qa || null
|
const baselineQa = res?.baseline_path_qa || null
|
||||||
const proposedQa = res?.proposed_path_qa || res?.path_qa || null
|
const proposedQa = res?.proposed_path_qa || res?.path_qa || null
|
||||||
const actionableCount =
|
const diffCount =
|
||||||
res?.slot_diff_count ?? compareSlotDiffs(res, { actionableOnly: true }).length
|
res?.slot_diff_count ?? compareSlotDiffs(res, { actionableOnly: true }).length
|
||||||
|
const bPct = pathQaQualityPercent(baselineQa)
|
||||||
const openCompareDialog = (diffCount, noticePrefix) => {
|
const pPct = pathQaQualityPercent(proposedQa)
|
||||||
setComparePayload(res)
|
let notice =
|
||||||
setProposedPathQa(res?.proposed_path_qa_pipeline || null)
|
diffCount > 0
|
||||||
setCompareOpen(true)
|
? `Match: ${diffCount} Slot-Vorschlag/Vorschläge — bitte im Dialog prüfen und auswählen.`
|
||||||
const bPct = pathQaQualityPercent(baselineQa)
|
: 'Match: Keine abweichenden Slot-Vorschläge — Dialog zur Kontrolle geöffnet.'
|
||||||
const pPct = pathQaQualityPercent(proposedQa)
|
if (bPct != null && pPct != null && pPct !== bPct) {
|
||||||
let notice = diffCount > 0
|
notice += ` Pfad-QS Vorschlag fair bewertet: ${bPct} % → ${pPct} %.`
|
||||||
? `${noticePrefix}${diffCount} Slot-Anpassung(en) — Vergleichsdialog geöffnet.`
|
|
||||||
: 'Vergleichsdialog geöffnet — keine übernehmbaren Slot-Änderungen im End-Pfad.'
|
|
||||||
if (bPct != null && pPct != null && pPct !== bPct) {
|
|
||||||
notice += ` Vorschlag fair bewertet: ${bPct} % → ${pPct} %.`
|
|
||||||
}
|
|
||||||
setMatchNotice(notice)
|
|
||||||
}
|
}
|
||||||
|
setMatchNotice(notice)
|
||||||
|
}
|
||||||
|
|
||||||
if (source === 'match') {
|
const runMatchCompareFlow = async (synced, { source = 'match' } = {}) => {
|
||||||
if (compareResponseHasActionableSlotChanges(res)) {
|
const res = await fetchMatchCompare(synced)
|
||||||
openCompareDialog(actionableCount, 'KI schlägt ')
|
setGapFillOffers(gapOffersFromMatchResponse(synced, res))
|
||||||
return { opened: true, res }
|
presentMatchCompare(res, { source })
|
||||||
}
|
const evalRes = await fetchPathEvaluate(synced)
|
||||||
setProposedPathQa(null)
|
setPathQa(evalRes?.path_qa || res?.baseline_path_qa || null)
|
||||||
setComparePayload(null)
|
return res
|
||||||
return { opened: false, res }
|
|
||||||
}
|
|
||||||
|
|
||||||
openCompareDialog(
|
|
||||||
actionableCount,
|
|
||||||
compareResponseHasActionableSlotChanges(res) ? 'KI schlägt ' : '',
|
|
||||||
)
|
|
||||||
return { opened: true, res }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const runMatch = async () => {
|
const runMatch = async () => {
|
||||||
|
|
@ -555,94 +554,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
setMatchNotice('')
|
setMatchNotice('')
|
||||||
try {
|
try {
|
||||||
const synced = syncProgressionRoadmapFromSlots(draft)
|
const synced = syncProgressionRoadmapFromSlots(draft)
|
||||||
const hasAssignments = draftHasLibrarySlotAssignments(synced)
|
|
||||||
setProposedPathQa(null)
|
setProposedPathQa(null)
|
||||||
|
await runMatchCompareFlow(synced, { source: 'match' })
|
||||||
if (hasAssignments) {
|
|
||||||
const res = await fetchOptimizeCompare(synced)
|
|
||||||
const { opened, res: compareRes } = presentOptimizeCompare(res, { source: 'match' })
|
|
||||||
if (opened) {
|
|
||||||
const evalRes = await fetchPathEvaluate(synced)
|
|
||||||
setPathQa(evalRes?.path_qa || compareRes?.baseline_path_qa || null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const evalRes = await fetchPathEvaluate(synced)
|
|
||||||
const { draft: evaluated, remainingOffers } = applyEvaluateResult(synced, evalRes)
|
|
||||||
setDraft(evaluated)
|
|
||||||
setGapFillOffers(remainingOffers)
|
|
||||||
setProposedPathQa(null)
|
|
||||||
setComparePayload(null)
|
|
||||||
const evalPct = pathQaQualityPercent(evalRes?.path_qa)
|
|
||||||
if (compareResponseHadRematchWithoutActionableDiffs(compareRes)) {
|
|
||||||
setMatchNotice(
|
|
||||||
`Match: Auto-Rematch ohne übernehmbare End-Änderung — dein Pfad bleibt unverändert${
|
|
||||||
evalPct != null ? ` (Pfad-QS ${evalPct} %).` : '.'
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setMatchNotice(
|
|
||||||
`Match: Kein inhaltlicher Optimierungsvorschlag — Pfad unverändert${
|
|
||||||
evalPct != null ? ` (${evalPct} %).` : '.'
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await saveProgressionGraphDraft(api, graphId, {
|
|
||||||
...evaluated,
|
|
||||||
lastFindings: evalRes?.path_qa || null,
|
|
||||||
})
|
|
||||||
setDraft((prev) => (prev ? { ...prev, dirty: false } : prev))
|
|
||||||
} catch (saveErr) {
|
|
||||||
console.warn('Match-Artefakt konnte nicht gespeichert werden', saveErr)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await api.suggestProgressionPath({
|
|
||||||
...buildMatchRequestBase(synced),
|
|
||||||
preserve_slot_assignments: false,
|
|
||||||
})
|
|
||||||
const { draft: matched, remainingOffers } = applyMatchResponseToDraft(
|
|
||||||
{
|
|
||||||
...synced,
|
|
||||||
progressionRoadmap: res?.progression_roadmap || synced.progressionRoadmap,
|
|
||||||
pathSkillExpectations: res?.path_skill_expectations || synced.pathSkillExpectations,
|
|
||||||
},
|
|
||||||
res,
|
|
||||||
)
|
|
||||||
setDraft(matched)
|
|
||||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
|
||||||
setTargetSummary(res?.target_profile_summary || null)
|
|
||||||
setPathQa(res?.path_qa || null)
|
|
||||||
setGapFillOffers(remainingOffers)
|
|
||||||
const ms = res?.match_summary
|
|
||||||
const rematchLog = res?.path_qa?.rematch_log
|
|
||||||
const rematchRounds = res?.path_qa?.rematch_rounds
|
|
||||||
if (ms) {
|
|
||||||
const parts = [
|
|
||||||
`Match: ${ms.library_matches ?? 0}/${ms.slot_count ?? '?'} Slots aus Bibliothek, ${ms.gap_fill_offer_count ?? 0} KI-Angebote.`,
|
|
||||||
]
|
|
||||||
if (rematchRounds > 0 && Array.isArray(rematchLog) && rematchLog.length > 0) {
|
|
||||||
parts.push(
|
|
||||||
`Auto-Rematch: ${rematchLog.length} Anpassung(en) in ${rematchRounds} Runde(n).`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const refineLog = res?.path_qa?.refine_log
|
|
||||||
if (Array.isArray(refineLog) && refineLog.length > 0) {
|
|
||||||
parts.push(`Stufen-Spec: ${refineLog.length} Slot(s) verfeinert.`)
|
|
||||||
}
|
|
||||||
setMatchNotice(parts.join(' '))
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await saveProgressionGraphDraft(api, graphId, {
|
|
||||||
...matched,
|
|
||||||
lastFindings: res?.path_qa || null,
|
|
||||||
})
|
|
||||||
setDraft((prev) => (prev ? { ...prev, dirty: false } : prev))
|
|
||||||
} catch (saveErr) {
|
|
||||||
console.warn('Match-Artefakt konnte nicht gespeichert werden', saveErr)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setActionErr(e.message || 'Übungs-Match fehlgeschlagen')
|
setActionErr(e.message || 'Übungs-Match fehlgeschlagen')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -656,8 +569,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
alert('Ziel-Anfrage: mindestens 3 Zeichen.')
|
alert('Ziel-Anfrage: mindestens 3 Zeichen.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!draftHasLibrarySlotAssignments(draft)) {
|
if (validMajorSteps.length < 2) {
|
||||||
alert('Mindestens ein Slot mit Bibliotheks-Übung nötig für einen Vergleich.')
|
alert('Mindestens zwei Slots mit Lernziel (je 3+ Zeichen) nötig.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setComparing(true)
|
setComparing(true)
|
||||||
|
|
@ -665,10 +578,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
setMatchNotice('')
|
setMatchNotice('')
|
||||||
try {
|
try {
|
||||||
const synced = syncProgressionRoadmapFromSlots(draft)
|
const synced = syncProgressionRoadmapFromSlots(draft)
|
||||||
const res = await fetchOptimizeCompare(synced)
|
setProposedPathQa(null)
|
||||||
presentOptimizeCompare(res, { source: 'manual' })
|
await runMatchCompareFlow(synced, { source: 'manual' })
|
||||||
const evalRes = await fetchPathEvaluate(synced)
|
|
||||||
setPathQa(evalRes?.path_qa || res?.baseline_path_qa || null)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setActionErr(e.message || 'Optimierungs-Vergleich fehlgeschlagen')
|
setActionErr(e.message || 'Optimierungs-Vergleich fehlgeschlagen')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -1217,7 +1128,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
onGenerateGapAi={openGapFillPrep}
|
onGenerateGapAi={openGapFillPrep}
|
||||||
onRematchSlots={runMatch}
|
onRematchSlots={runMatch}
|
||||||
onOptimizeCompare={runOptimizeCompare}
|
onOptimizeCompare={runOptimizeCompare}
|
||||||
canOptimizeCompare={draftHasLibrarySlotAssignments(draft)}
|
canOptimizeCompare={validMajorSteps.length >= 2}
|
||||||
optimizeCompareBusy={comparing}
|
optimizeCompareBusy={comparing}
|
||||||
rematchBusy={matching}
|
rematchBusy={matching}
|
||||||
generatingOfferId={generatingOfferId}
|
generatingOfferId={generatingOfferId}
|
||||||
|
|
@ -1261,6 +1172,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
<ProgressionOptimizeCompareModal
|
<ProgressionOptimizeCompareModal
|
||||||
open={compareOpen}
|
open={compareOpen}
|
||||||
comparison={comparePayload}
|
comparison={comparePayload}
|
||||||
|
mode={compareSource}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
if (compareApplying) return
|
if (compareApplying) return
|
||||||
setCompareOpen(false)
|
setCompareOpen(false)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ function qaLabel(pathQa) {
|
||||||
export default function ProgressionOptimizeCompareModal({
|
export default function ProgressionOptimizeCompareModal({
|
||||||
open,
|
open,
|
||||||
comparison,
|
comparison,
|
||||||
|
mode = 'manual',
|
||||||
onClose,
|
onClose,
|
||||||
onApplySelected,
|
onApplySelected,
|
||||||
applying = false,
|
applying = false,
|
||||||
|
|
@ -63,12 +64,16 @@ export default function ProgressionOptimizeCompareModal({
|
||||||
setSelected(on ? new Set(allKeys) : new Set())
|
setSelected(on ? new Set(allKeys) : new Set())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const title =
|
||||||
|
mode === 'match' ? 'Übungs-Match — Vorschläge prüfen' : 'Optimierung vergleichen'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="modal-overlay"
|
className="modal-overlay"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="optimize-compare-title"
|
aria-labelledby="optimize-compare-title"
|
||||||
|
style={{ zIndex: 2200 }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget && !applying) onClose()
|
if (e.target === e.currentTarget && !applying) onClose()
|
||||||
}}
|
}}
|
||||||
|
|
@ -79,12 +84,11 @@ export default function ProgressionOptimizeCompareModal({
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<h3 id="optimize-compare-title" style={{ marginTop: 0 }}>
|
<h3 id="optimize-compare-title" style={{ marginTop: 0 }}>
|
||||||
Optimierung vergleichen
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}>
|
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}>
|
||||||
Vergleicht deinen Pfad mit dem Match-Vorschlag — beide Seiten mit derselben Bewertungslogik
|
KI matcht alle Lernziel-Slots neu (auch bereits belegte). Du wählst im Dialog, welche
|
||||||
wie „Graph bewerten“. Prozentwerte beziehen sich auf den übernehmbaren End-Stand, nicht auf
|
Vorschläge übernommen werden — der Graph ändert sich erst nach „Auswahl übernehmen“.
|
||||||
das Auto-Rematch-Protokoll.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{noMeaningfulDiffs || proposedNotBetter ? (
|
{noMeaningfulDiffs || proposedNotBetter ? (
|
||||||
|
|
|
||||||
|
|
@ -906,7 +906,7 @@ export function slotsToSlotAssignments(draft) {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mindestens ein Bibliotheks-Slot belegt → kuratierter Stand, Match prüft Abweichungen. */
|
/** Mindestens ein Bibliotheks-Slot belegt. */
|
||||||
export function draftHasLibrarySlotAssignments(draft) {
|
export function draftHasLibrarySlotAssignments(draft) {
|
||||||
return slotsToSlotAssignments(draft).length >= 1
|
return slotsToSlotAssignments(draft).length >= 1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user