import React, { useEffect, useMemo, useState } from 'react' import api from '../../utils/api' function downloadJson(obj, filename) { const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename a.click() URL.revokeObjectURL(url) } function groupModelSkills(model) { if (!model?.model_skills?.length) return [] const groups = new Map() for (const ms of model.model_skills) { const main = ms.skill_main_category_name || '—' const sub = ms.skill_subcategory_name || '—' const k = `${main}\t${sub}` if (!groups.has(k)) groups.set(k, { main, sub, skills: [] }) groups.get(k).skills.push(ms) } return Array.from(groups.values()) } function cellMapForSkill(model, skillId) { const m = {} for (const sl of model.skill_levels || []) { if (Number(sl.skill_id) === Number(skillId)) { m[sl.level_number] = sl } } return m } export default function MaturityMatrixToolsAdmin() { const [focusAreas, setFocusAreas] = useState([]) const [styles, setStyles] = useState([]) const [trainingTypes, setTrainingTypes] = useState([]) const [models, setModels] = useState([]) const [focusId, setFocusId] = useState('') const [styleId, setStyleId] = useState('') const [typeId, setTypeId] = useState('') const [matrix, setMatrix] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [message, setMessage] = useState('') const [importMode, setImportMode] = useState('create') const [replaceModelId, setReplaceModelId] = useState('') const [exportModelId, setExportModelId] = useState('') const [importBindings, setImportBindings] = useState(true) const [stackWipe, setStackWipe] = useState(false) const [stackConfirmText, setStackConfirmText] = useState('') const [stackLoading, setStackLoading] = useState(false) const [stackImportLoading, setStackImportLoading] = useState(false) useEffect(() => { let cancelled = false ;(async () => { try { const [fa, sd, tt, m] = await Promise.all([ api.listFocusAreas({}), api.listStyleDirections({}), api.listTrainingTypes({}), api.listMaturityModels({}) ]) if (!cancelled) { setFocusAreas(fa) setStyles(sd) setTrainingTypes(tt) setModels(m) } } catch (e) { if (!cancelled) setError(e.message || String(e)) } })() return () => { cancelled = true } }, []) const groups = useMemo(() => (matrix ? groupModelSkills(matrix) : []), [matrix]) const levels = matrix?.levels || [] async function handleResolve() { if (!focusId) { setError('Fokusbereich wählen.') return } setLoading(true) setError('') setMessage('') setMatrix(null) try { const params = { focus_area_id: focusId } if (styleId) params.style_direction_id = styleId if (typeId) params.training_type_id = typeId const m = await api.resolveMaturityModel(params) setMatrix(m) if (!m) setError('Keine Matrix für diesen Kontext.') else setMessage('Matrix geladen.') } catch (e) { setError(e.message || String(e)) } finally { setLoading(false) } } async function handleExportResolved() { if (!focusId) { setError('Fokusbereich wählen.') return } setError('') setMessage('') try { const params = { focus_area_id: focusId } if (styleId) params.style_direction_id = styleId if (typeId) params.training_type_id = typeId const bundle = await api.exportResolvedMaturityBundle(params) downloadJson(bundle, 'faehigkeitsmatrix-aufgeloest.json') setMessage('Export gestartet (Download).') } catch (e) { setError(e.message || String(e)) } } async function handleExportStored() { if (!exportModelId) { setError('Modell für Export auswählen.') return } setError('') setMessage('') try { const bundle = await api.exportMaturityModelBundle(parseInt(exportModelId, 10)) downloadJson(bundle, `reifegradmodell-${exportModelId}.json`) setMessage('Export gestartet (Download).') } catch (e) { setError(e.message || String(e)) } } async function handleExportStack() { setError('') setMessage('') setStackLoading(true) try { const bundle = await api.exportMatrixStackBundle() const name = `matrix-stack-${(bundle.bundle_export_id || 'export').slice(0, 8)}.json` downloadJson(bundle, name) setMessage('Komplett-Stack exportiert (Download).') } catch (err) { setError(err.message || String(err)) } finally { setStackLoading(false) } } async function handleImportStackFile(e) { const file = e.target.files?.[0] if (!file) return setError('') setMessage('') setStackImportLoading(true) try { const data = JSON.parse(await file.text()) if (data.kind !== 'shinkan.matrix_stack.v1') { setError('Erwartet wird eine Datei mit kind: shinkan.matrix_stack.v1 (Komplett-Stack-Export).') e.target.value = '' return } if (stackWipe && stackConfirmText !== 'DELETE_MATURITY_STACK') { setError('Vollständiges Ersetzen: Bestätigung exakt „DELETE_MATURITY_STACK“ eintragen (Superadmin).') e.target.value = '' return } const payload = { ...data, replace_all_maturity_models: stackWipe, confirm_replace_all: stackWipe ? stackConfirmText : undefined } const res = await api.importMatrixStackBundle(payload) const w = res.warnings || [] setMessage( `Stack-Import OK. Modell-Zuordnung: ${Object.keys(res.model_id_map || {}).length} Modell(e), Skills gemappt: ${Object.keys(res.skill_id_map || {}).length}.` + (w.length ? ` ${w.length} Hinweis(e) (Konsole).` : '') ) if (w.length) console.warn('matrix_stack import warnings', w) setModels(await api.listMaturityModels({})) } catch (err) { setError(err.message || String(err)) } finally { setStackImportLoading(false) e.target.value = '' } } async function handleImportFile(e) { const file = e.target.files?.[0] if (!file) return setError('') setMessage('') try { const data = JSON.parse(await file.text()) if (data.kind === 'shinkan.matrix_stack.v1') { setError('Komplett-Stack: bitte die Datei im Abschnitt „Komplett-Stack“ importieren (eigenes Dateifeld).') e.target.value = '' return } else { const payload = { ...data, mode: importMode, import_bindings: importBindings } if (importMode === 'replace') { if (!replaceModelId) { setError('Bei „Ersetzen“ die Ziel-Modell-ID angeben.') e.target.value = '' return } payload.replace_model_id = parseInt(replaceModelId, 10) } const res = await api.importMaturityModelBundle(payload) setMessage(`Import erfolgreich. Modell-ID: ${res.id}`) setModels(await api.listMaturityModels({})) } } catch (err) { setError(err.message || String(err)) } finally { e.target.value = '' } } return (
Matrix nach Kontext auflösen, hierarchisch nach Hauptkategorie und Kategorie darstellen, sowie JSON exportieren oder importieren (gespeichertes Modell inkl. optional Kontext-Bindings, aufgelöste Matrix, oder{' '} Komplett-Stack mit Fähigkeitskatalog und allen Reifegradmodellen für Test → Prod).
{error ? ({message}
: null}Quelle:{' '} {matrix.resolution.merged ? `zusammengeführt aus Modell-IDs ${(matrix.resolution.source_model_ids || []).join(', ')}` : `Modell-ID ${matrix.id}`}
) : null}| Fähigkeit | {levels.map((lv) => ({lv.level_number} {lv.name} | ))}
|---|---|
| {ms.skill_name} | {levels.map((lv) => { const c = cells[lv.level_number] return (
{c ? (
<>
{c.description || '—'}
{c.observable_criteria ? (
Beobachtung: {c.observable_criteria}
) : null}
>
) : (
—
)}
|
)
})}
Export enthält skill_main_categories,{' '}
skill_categories, skills,{' '}
skill_level_definitions, alle Reifegradmodelle (Stufen, Matrix-Zellen,
Legacy-Kontext M:N) sowie context_bindings mit{' '}
Fokus-/Stil-/Trainingsstil-Namen für die Ziel-DB. Auf Prod müssen dieselben
Katalognamen für Fokusbereiche, Stilrichtungen und Trainingsstile existieren.
JSON vom Export dieses Dialogs (shinkan.matrix_stack.v1). Katalog
wird per Slug zusammengeführt; Skills per Kategorie + Name. Reifegradmodelle werden neu angelegt; Kontext-Bindings
über Namen der Fokus-/Stil-/Trainingsstil-Kataloge auf der Ziel-DB.
Import läuft…
: null}Enthält Stufen, Fähigkeiten, Zelltexte und die dem Modell zugeordneten Kontext-Bindings.
shinkan.maturity_model.v1 oder{' '}
shinkan.maturity_matrix_resolved.v1. Komplett-Stack bitte im
Abschnitt Komplett-Stack oben importieren. Aufgelöste Matrizen legen ein neues Modell an bzw.
ersetzen den Inhalt des Zielmodells (ohne Bindings).