Enhance MediaWiki import functionality with category normalization and skill attributes
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
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
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m8s
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
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
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m8s
- Introduced `_normalize_mw_category` function to clean category names for API calls, ensuring consistent handling of category prefixes. - Updated `SmwClient` methods to utilize normalized category names, improving data retrieval accuracy. - Added `_wiki_category_or_default` function to provide default categories based on import type, enhancing user experience during imports. - Integrated new fields `karate_relevance` and `relevance_level` into various admin components, allowing for better skill management. - Incremented app version to 0.8.145 and updated changelog to reflect these changes.
This commit is contained in:
parent
949a77fe38
commit
623af621b4
|
|
@ -30,6 +30,17 @@ CATEGORY_METHODS = os.getenv("MEDIAWIKI_CATEGORY_METHODS", "Methodenbeschrei
|
||||||
CATEGORY_MODELS = os.getenv("MEDIAWIKI_CATEGORY_MODELS", "Reifegradmodelle")
|
CATEGORY_MODELS = os.getenv("MEDIAWIKI_CATEGORY_MODELS", "Reifegradmodelle")
|
||||||
|
|
||||||
|
|
||||||
|
def _wiki_category_or_default(category: Optional[str], import_type: str) -> str:
|
||||||
|
"""Leeres category ⇒ Standard je import_type."""
|
||||||
|
if (category or "").strip():
|
||||||
|
return category.strip()
|
||||||
|
if import_type == "skill":
|
||||||
|
return CATEGORY_SKILLS
|
||||||
|
if import_type == "method":
|
||||||
|
return CATEGORY_METHODS
|
||||||
|
return CATEGORY_EXERCISES
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Pydantic Models #
|
# Pydantic Models #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
@ -59,7 +70,7 @@ def require_admin(session: dict = Depends(require_auth)) -> dict:
|
||||||
|
|
||||||
@router.get("/import/mediawiki/preview")
|
@router.get("/import/mediawiki/preview")
|
||||||
async def preview_import(
|
async def preview_import(
|
||||||
category: str = Query(default=CATEGORY_EXERCISES),
|
category: Optional[str] = Query(default=None),
|
||||||
import_type: str = Query(default="exercise"),
|
import_type: str = Query(default="exercise"),
|
||||||
limit: int = Query(default=10, ge=1, le=500),
|
limit: int = Query(default=10, ge=1, le=500),
|
||||||
session: dict = Depends(require_admin),
|
session: dict = Depends(require_admin),
|
||||||
|
|
@ -68,9 +79,10 @@ async def preview_import(
|
||||||
Zeigt Vorschau: Welche Seiten würden importiert werden?
|
Zeigt Vorschau: Welche Seiten würden importiert werden?
|
||||||
Überprüft Duplikate und mapped Felder ohne zu speichern.
|
Überprüft Duplikate und mapped Felder ohne zu speichern.
|
||||||
"""
|
"""
|
||||||
|
resolved_category = _wiki_category_or_default(category, import_type)
|
||||||
client = SmwClient()
|
client = SmwClient()
|
||||||
try:
|
try:
|
||||||
members = await client.get_category_members(category, limit=limit)
|
members = await client.get_category_members(resolved_category, limit=limit)
|
||||||
except SmwClientError as e:
|
except SmwClientError as e:
|
||||||
raise HTTPException(status_code=502, detail=f"Wiki-API nicht erreichbar: {e}")
|
raise HTTPException(status_code=502, detail=f"Wiki-API nicht erreichbar: {e}")
|
||||||
|
|
||||||
|
|
@ -129,7 +141,7 @@ async def preview_import(
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"category": category,
|
"category": resolved_category,
|
||||||
"import_type": import_type,
|
"import_type": import_type,
|
||||||
"total_found": len(members),
|
"total_found": len(members),
|
||||||
"preview": preview,
|
"preview": preview,
|
||||||
|
|
@ -151,6 +163,7 @@ async def execute_import(
|
||||||
Gibt sofort log_id zurück – Status via GET /import/mediawiki/status/{log_id}.
|
Gibt sofort log_id zurück – Status via GET /import/mediawiki/status/{log_id}.
|
||||||
"""
|
"""
|
||||||
profile_id = session["profile_id"]
|
profile_id = session["profile_id"]
|
||||||
|
resolved_category = _wiki_category_or_default(body.category, body.import_type)
|
||||||
|
|
||||||
# Log-Eintrag anlegen
|
# Log-Eintrag anlegen
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
|
|
@ -160,7 +173,7 @@ async def execute_import(
|
||||||
(import_type, import_status, category, dry_run, reimport, imported_by)
|
(import_type, import_status, category, dry_run, reimport, imported_by)
|
||||||
VALUES (%s, 'running', %s, %s, %s, %s)
|
VALUES (%s, 'running', %s, %s, %s, %s)
|
||||||
RETURNING id""",
|
RETURNING id""",
|
||||||
(body.import_type, body.category, body.dry_run, body.reimport_existing, profile_id)
|
(body.import_type, resolved_category, body.dry_run, body.reimport_existing, profile_id)
|
||||||
)
|
)
|
||||||
log_id = cur.fetchone()['id']
|
log_id = cur.fetchone()['id']
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
@ -169,7 +182,7 @@ async def execute_import(
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
_run_import,
|
_run_import,
|
||||||
log_id=log_id,
|
log_id=log_id,
|
||||||
category=body.category,
|
category=resolved_category,
|
||||||
import_type=body.import_type,
|
import_type=body.import_type,
|
||||||
reimport=body.reimport_existing,
|
reimport=body.reimport_existing,
|
||||||
dry_run=body.dry_run,
|
dry_run=body.dry_run,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,26 @@ class SmwClientError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_mw_category(category: str) -> str:
|
||||||
|
"""
|
||||||
|
Bereinigt Kategorienamen für API cmtitle=Kategorie:…
|
||||||
|
Erlaubt z. B. 'Fähigkeitsbeschreibung', ' Kategorie:X ', 'kategorie:X' ohne Doppel-Prefix.
|
||||||
|
"""
|
||||||
|
c = (category or "").strip()
|
||||||
|
if not c:
|
||||||
|
raise SmwClientError("Kategorie (Seitenlisten-Name ohne Präfix) darf nicht leer sein")
|
||||||
|
|
||||||
|
pref = "kategorie:"
|
||||||
|
while c.lower().startswith(pref):
|
||||||
|
c = c[len(pref) :].lstrip()
|
||||||
|
|
||||||
|
remaining = (c or "").strip()
|
||||||
|
if not remaining:
|
||||||
|
raise SmwClientError("Kategorie (Seitenlisten-Name ohne Präfix) darf nicht leer sein")
|
||||||
|
|
||||||
|
return remaining
|
||||||
|
|
||||||
|
|
||||||
class SmwClient:
|
class SmwClient:
|
||||||
"""Stateless MediaWiki/SMW API Client mit Session-Login."""
|
"""Stateless MediaWiki/SMW API Client mit Session-Login."""
|
||||||
|
|
||||||
|
|
@ -110,12 +130,13 @@ class SmwClient:
|
||||||
"""
|
"""
|
||||||
members = []
|
members = []
|
||||||
cmcontinue = None
|
cmcontinue = None
|
||||||
|
cat = _normalize_mw_category(category)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
params = {
|
params = {
|
||||||
"action": "query",
|
"action": "query",
|
||||||
"list": "categorymembers",
|
"list": "categorymembers",
|
||||||
"cmtitle": f"Kategorie:{category}",
|
"cmtitle": f"Kategorie:{cat}",
|
||||||
"cmlimit": min(limit, 500),
|
"cmlimit": min(limit, 500),
|
||||||
"cmtype": "page", # Nur Seiten, keine Unterkategorien
|
"cmtype": "page", # Nur Seiten, keine Unterkategorien
|
||||||
"cmprop": "ids|title",
|
"cmprop": "ids|title",
|
||||||
|
|
@ -134,8 +155,8 @@ class SmwClient:
|
||||||
|
|
||||||
# Rekursiv durch Unterkategorien gehen
|
# Rekursiv durch Unterkategorien gehen
|
||||||
if recursive:
|
if recursive:
|
||||||
subcats = await self._get_subcategories(category)
|
subcats = await self._get_subcategories(cat)
|
||||||
logger.info(f"Kategorie '{category}': {len(members)} direkte Seiten, {len(subcats)} Unterkategorien")
|
logger.info(f"Kategorie '{cat}': {len(members)} direkte Seiten, {len(subcats)} Unterkategorien")
|
||||||
|
|
||||||
for subcat in subcats:
|
for subcat in subcats:
|
||||||
if len(members) >= limit:
|
if len(members) >= limit:
|
||||||
|
|
@ -150,12 +171,13 @@ class SmwClient:
|
||||||
"""Gibt alle Unterkategorien einer Kategorie zurück."""
|
"""Gibt alle Unterkategorien einer Kategorie zurück."""
|
||||||
subcats = []
|
subcats = []
|
||||||
cmcontinue = None
|
cmcontinue = None
|
||||||
|
cat = _normalize_mw_category(category)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
params = {
|
params = {
|
||||||
"action": "query",
|
"action": "query",
|
||||||
"list": "categorymembers",
|
"list": "categorymembers",
|
||||||
"cmtitle": f"Kategorie:{category}",
|
"cmtitle": f"Kategorie:{cat}",
|
||||||
"cmlimit": 500,
|
"cmlimit": 500,
|
||||||
"cmtype": "subcat", # Nur Unterkategorien
|
"cmtype": "subcat", # Nur Unterkategorien
|
||||||
"cmprop": "ids|title",
|
"cmprop": "ids|title",
|
||||||
|
|
|
||||||
|
|
@ -408,9 +408,9 @@ def map_wiki_to_skill(
|
||||||
first_value = values[0] if isinstance(values, list) else values
|
first_value = values[0] if isinstance(values, list) else values
|
||||||
|
|
||||||
if target == "description":
|
if target == "description":
|
||||||
description_text = wikitext_to_plaintext(first_value)
|
description_text = wikitext_to_plaintext(str(first_value))
|
||||||
elif target == "karate_relevance":
|
elif target == "karate_relevance":
|
||||||
mapped["karate_relevance"] = wikitext_to_plaintext(first_value)
|
mapped["karate_relevance"] = wikitext_to_plaintext(str(first_value))
|
||||||
elif target == "relevance_level":
|
elif target == "relevance_level":
|
||||||
parsed = parse_wiki_relevance_level(first_value if isinstance(first_value, str) else str(first_value))
|
parsed = parse_wiki_relevance_level(first_value if isinstance(first_value, str) else str(first_value))
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
|
|
|
||||||
18
backend/tests/test_import_wiki_category_default.py
Normal file
18
backend/tests/test_import_wiki_category_default.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""Resolver für leere/fehlende Kategorie je import_type."""
|
||||||
|
|
||||||
|
from routers.import_wiki import (
|
||||||
|
CATEGORY_EXERCISES,
|
||||||
|
CATEGORY_METHODS,
|
||||||
|
CATEGORY_SKILLS,
|
||||||
|
_wiki_category_or_default,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wiki_category_or_default_explicit():
|
||||||
|
assert _wiki_category_or_default(" MeineKat ", "skill") == "MeineKat"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wiki_category_or_default_falls_through():
|
||||||
|
assert _wiki_category_or_default(None, "exercise") == CATEGORY_EXERCISES
|
||||||
|
assert _wiki_category_or_default("", "skill") == CATEGORY_SKILLS
|
||||||
|
assert _wiki_category_or_default(" ", "method") == CATEGORY_METHODS
|
||||||
21
backend/tests/test_smw_client_category_normalize.py
Normal file
21
backend/tests/test_smw_client_category_normalize.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from smw_client import _normalize_mw_category, SmwClientError
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalize_strips_optional_kategorie_prefix():
|
||||||
|
assert _normalize_mw_category("Fähigkeitsbeschreibung") == "Fähigkeitsbeschreibung"
|
||||||
|
assert _normalize_mw_category(" Kategorie:Fähigkeitsbeschreibung ") == "Fähigkeitsbeschreibung"
|
||||||
|
assert _normalize_mw_category("kategorie:test") == "test"
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalize_strips_duplicate_prefix_chain():
|
||||||
|
assert _normalize_mw_category("Kategorie:Kategorie:Foo") == "Foo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalize_rejects_empty():
|
||||||
|
with pytest.raises(SmwClientError, match="leer"):
|
||||||
|
_normalize_mw_category("")
|
||||||
|
with pytest.raises(SmwClientError, match="leer"):
|
||||||
|
_normalize_mw_category(" Kategorie: ")
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.143"
|
APP_VERSION = "0.8.145"
|
||||||
BUILD_DATE = "2026-05-16"
|
BUILD_DATE = "2026-05-16"
|
||||||
DB_SCHEMA_VERSION = "20260516065"
|
DB_SCHEMA_VERSION = "20260516065"
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ MODULE_VERSIONS = {
|
||||||
"planning": "0.13.0", # Vorlagen/Framework/Module/Graphs: RBAC wie Übungen (edit/delete/governance transition); Planungs-UI Sichtbarkeit neue Vorlage
|
"planning": "0.13.0", # Vorlagen/Framework/Module/Graphs: RBAC wie Übungen (edit/delete/governance transition); Planungs-UI Sichtbarkeit neue Vorlage
|
||||||
"dashboard": "1.1.0", # GET /api/dashboard/kpis inkl. training_home (ein Client-Roundtrip für KPIs + nächste Termine)
|
"dashboard": "1.1.0", # GET /api/dashboard/kpis inkl. training_home (ein Client-Roundtrip für KPIs + nächste Termine)
|
||||||
"training_modules": "1.1.0", # PUT/DELETE: assert_library_content_* (Vereinsadmin löscht Vereins-Inhalt, Trainer bearbeitet club wie Übungen)
|
"training_modules": "1.1.0", # PUT/DELETE: assert_library_content_* (Vereinsadmin löscht Vereins-Inhalt, Trainer bearbeitet club wie Übungen)
|
||||||
"import_wiki": "1.0.1", # Skills: KarateRelevanz + RelevanzLevel → DB-Spalten
|
"import_wiki": "1.0.3", # Default-Kategorie Fähigkeiten: Fähigkeitsbeschreibung; cmtitle-Normalisierung; UI Preview/Execute Defaults je Typ
|
||||||
"admin": "1.0.0",
|
"admin": "1.0.0",
|
||||||
"membership": "1.0.0",
|
"membership": "1.0.0",
|
||||||
"catalogs": "1.5.0", # Updated: Trainer Contexts API (Migration 012)
|
"catalogs": "1.5.0", # Updated: Trainer Contexts API (Migration 012)
|
||||||
|
|
@ -36,6 +36,21 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.145",
|
||||||
|
"date": "2026-05-16",
|
||||||
|
"changes": [
|
||||||
|
"Wiki-Import: Standard-Kategorie Fähigkeiten korrigiert auf „Fähigkeitsbeschreibung“ (korrekter Wiki-Name).",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.8.144",
|
||||||
|
"date": "2026-05-16",
|
||||||
|
"changes": [
|
||||||
|
"Wiki-Import: Default-Kategorie Fähigkeiten; SMW-Client entfernt versehentliches „Kategorie:“-Prefix; Vorschau/Ausführung nutzen Default-Kategorie je import_type wenn leer.",
|
||||||
|
"Admin UI Wiki-Import: Wechsel Import-Typ setzt passende Standard-Kategorie.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.143",
|
"version": "0.8.143",
|
||||||
"date": "2026-05-16",
|
"date": "2026-05-16",
|
||||||
|
|
|
||||||
|
|
@ -3342,6 +3342,12 @@ a.analysis-split__nav-item {
|
||||||
.admin-matrix-skill-list__name {
|
.admin-matrix-skill-list__name {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
.admin-matrix-skill-list__buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.admin-matrix-skill-list__path {
|
.admin-matrix-skill-list__path {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
|
@ -3388,6 +3394,16 @@ a.analysis-split__nav-item {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
.admin-matrix-matrix__skill-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.admin-matrix-matrix__skill-head .btn-tiny {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
.admin-matrix-matrix__cell {
|
.admin-matrix-matrix__cell {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@ export default function MaturityModelsAdminPanel() {
|
||||||
const [levelsForm, setLevelsForm] = useState([])
|
const [levelsForm, setLevelsForm] = useState([])
|
||||||
const [cellDraft, setCellDraft] = useState({})
|
const [cellDraft, setCellDraft] = useState({})
|
||||||
const [skillToAdd, setSkillToAdd] = useState('')
|
const [skillToAdd, setSkillToAdd] = useState('')
|
||||||
|
/** Modal: Stammdaten Fähigkeit (Wiki-Felder) aus Matrix-Kontext */
|
||||||
|
const [skillWikiModal, setSkillWikiModal] = useState(null)
|
||||||
|
const [skillWikiForm, setSkillWikiForm] = useState({
|
||||||
|
karate_relevance: '',
|
||||||
|
relevance_level: ''
|
||||||
|
})
|
||||||
|
const [skillWikiLoading, setSkillWikiLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
@ -219,6 +226,57 @@ export default function MaturityModelsAdminPanel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openSkillWikiModal(skillId, skillName) {
|
||||||
|
setSkillWikiModal({ skill_id: skillId, skill_name: skillName })
|
||||||
|
setSkillWikiLoading(true)
|
||||||
|
setSkillWikiForm({ karate_relevance: '', relevance_level: '' })
|
||||||
|
try {
|
||||||
|
const s = await api.getSkill(skillId)
|
||||||
|
setSkillWikiForm({
|
||||||
|
karate_relevance: s.karate_relevance || '',
|
||||||
|
relevance_level:
|
||||||
|
s.relevance_level != null && s.relevance_level !== ''
|
||||||
|
? String(s.relevance_level)
|
||||||
|
: ''
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message || String(e))
|
||||||
|
setSkillWikiModal(null)
|
||||||
|
} finally {
|
||||||
|
setSkillWikiLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSkillWikiModal() {
|
||||||
|
setSkillWikiModal(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveSkillWikiFields(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!skillWikiModal) return
|
||||||
|
setError('')
|
||||||
|
setSkillWikiLoading(true)
|
||||||
|
try {
|
||||||
|
await api.updateSkill(skillWikiModal.skill_id, {
|
||||||
|
karate_relevance:
|
||||||
|
skillWikiForm.karate_relevance && skillWikiForm.karate_relevance.trim()
|
||||||
|
? skillWikiForm.karate_relevance.trim()
|
||||||
|
: null,
|
||||||
|
relevance_level:
|
||||||
|
skillWikiForm.relevance_level === '' || skillWikiForm.relevance_level == null
|
||||||
|
? null
|
||||||
|
: Number(skillWikiForm.relevance_level)
|
||||||
|
})
|
||||||
|
const sk = await api.listSkills({ status: 'active' })
|
||||||
|
setAllSkills(sk)
|
||||||
|
closeSkillWikiModal()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message || String(e))
|
||||||
|
} finally {
|
||||||
|
setSkillWikiLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDeleteModel() {
|
async function handleDeleteModel() {
|
||||||
if (!selectedId || !isSuperadmin) return
|
if (!selectedId || !isSuperadmin) return
|
||||||
if (!confirm('Reifegradmodell dauerhaft löschen?')) return
|
if (!confirm('Reifegradmodell dauerhaft löschen?')) return
|
||||||
|
|
@ -552,13 +610,24 @@ export default function MaturityModelsAdminPanel() {
|
||||||
<li key={ms.skill_id} className="admin-matrix-skill-list__item">
|
<li key={ms.skill_id} className="admin-matrix-skill-list__item">
|
||||||
<div className="admin-matrix-skill-list__row">
|
<div className="admin-matrix-skill-list__row">
|
||||||
<strong className="admin-matrix-skill-list__name">{ms.skill_name}</strong>
|
<strong className="admin-matrix-skill-list__name">{ms.skill_name}</strong>
|
||||||
<button
|
<span className="admin-matrix-skill-list__buttons">
|
||||||
type="button"
|
<button
|
||||||
className="btn btn-secondary btn-small"
|
type="button"
|
||||||
onClick={() => handleRemoveSkill(ms.skill_id)}
|
className="btn btn-secondary btn-small"
|
||||||
>
|
disabled={saving || skillWikiLoading}
|
||||||
Entfernen
|
title="Karate-Relevanz und Relevanzgrad bearbeiten"
|
||||||
</button>
|
onClick={() => openSkillWikiModal(ms.skill_id, ms.skill_name)}
|
||||||
|
>
|
||||||
|
Wiki-Felder
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-small"
|
||||||
|
onClick={() => handleRemoveSkill(ms.skill_id)}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{(ms.skill_main_category_name || ms.skill_subcategory_name) ? (
|
{(ms.skill_main_category_name || ms.skill_subcategory_name) ? (
|
||||||
<div className="admin-matrix-skill-list__path muted">
|
<div className="admin-matrix-skill-list__path muted">
|
||||||
|
|
@ -595,13 +664,26 @@ export default function MaturityModelsAdminPanel() {
|
||||||
{(detail.model_skills || []).map((ms) => (
|
{(detail.model_skills || []).map((ms) => (
|
||||||
<tr key={ms.skill_id}>
|
<tr key={ms.skill_id}>
|
||||||
<td className="admin-matrix-matrix__skill-cell">
|
<td className="admin-matrix-matrix__skill-cell">
|
||||||
<div>{ms.skill_name}</div>
|
<div className="admin-matrix-matrix__skill-head">
|
||||||
{(ms.skill_main_category_name || ms.skill_subcategory_name) ? (
|
<div>
|
||||||
<div className="admin-matrix-matrix__skill-path muted">
|
<div>{ms.skill_name}</div>
|
||||||
{ms.skill_main_category_name || '—'}
|
{(ms.skill_main_category_name || ms.skill_subcategory_name) ? (
|
||||||
{ms.skill_subcategory_name ? ` › ${ms.skill_subcategory_name}` : ''}
|
<div className="admin-matrix-matrix__skill-path muted">
|
||||||
|
{ms.skill_main_category_name || '—'}
|
||||||
|
{ms.skill_subcategory_name ? ` › ${ms.skill_subcategory_name}` : ''}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-tiny"
|
||||||
|
title="Karate-Relevanz und Relevanzgrad bearbeiten"
|
||||||
|
disabled={saving || skillWikiLoading}
|
||||||
|
onClick={() => openSkillWikiModal(ms.skill_id, ms.skill_name)}
|
||||||
|
>
|
||||||
|
✎
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{(detail.levels || []).map((l) => {
|
{(detail.levels || []).map((l) => {
|
||||||
const key = `${ms.skill_id}-${l.level_number}`
|
const key = `${ms.skill_id}-${l.level_number}`
|
||||||
|
|
@ -643,6 +725,90 @@ export default function MaturityModelsAdminPanel() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{skillWikiModal ? (
|
||||||
|
<div
|
||||||
|
className="admin-modal-backdrop"
|
||||||
|
role="presentation"
|
||||||
|
onClick={closeSkillWikiModal}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="admin-modal-sheet"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="matrix-skill-wiki-title"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="admin-modal-sheet__header">
|
||||||
|
<h3 id="matrix-skill-wiki-title" className="admin-modal-sheet__title">
|
||||||
|
Fähigkeit: {skillWikiModal.skill_name}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary admin-modal-sheet__close"
|
||||||
|
disabled={skillWikiLoading}
|
||||||
|
onClick={closeSkillWikiModal}
|
||||||
|
aria-label="Schließen"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-modal-sheet__body">
|
||||||
|
{skillWikiLoading ? (
|
||||||
|
<p className="muted">Lade Daten…</p>
|
||||||
|
) : (
|
||||||
|
<form className="skills-catalog-form" onSubmit={handleSaveSkillWikiFields}>
|
||||||
|
<label className="form-label">Karate-Relevanz (Wiki)</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows={4}
|
||||||
|
value={skillWikiForm.karate_relevance}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSkillWikiForm((f) => ({ ...f, karate_relevance: e.target.value }))
|
||||||
|
}
|
||||||
|
disabled={skillWikiLoading}
|
||||||
|
placeholder="Freitext (Wiki-Eigenschaft KarateRelevanz)"
|
||||||
|
/>
|
||||||
|
<label className="form-label">Relevanzgrad (Wiki, 1–3)</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={
|
||||||
|
skillWikiForm.relevance_level === ''
|
||||||
|
? ''
|
||||||
|
: String(skillWikiForm.relevance_level)
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSkillWikiForm((f) => ({ ...f, relevance_level: e.target.value }))
|
||||||
|
}
|
||||||
|
disabled={skillWikiLoading}
|
||||||
|
>
|
||||||
|
<option value="">– nicht gesetzt –</option>
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
</select>
|
||||||
|
<p className="muted admin-matrix-hint">
|
||||||
|
Änderungen gelten für die Fähigkeit im gesamten Katalog (gemeinsame Stammdaten).
|
||||||
|
</p>
|
||||||
|
<div className="skills-catalog-form__actions">
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={skillWikiLoading}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
disabled={skillWikiLoading}
|
||||||
|
onClick={closeSkillWikiModal}
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,9 @@ export default function SkillsCatalogAdmin() {
|
||||||
keywords: '',
|
keywords: '',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
sort_order: '',
|
sort_order: '',
|
||||||
category_id: ''
|
category_id: '',
|
||||||
|
karate_relevance: '',
|
||||||
|
relevance_level: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const [newMainName, setNewMainName] = useState('')
|
const [newMainName, setNewMainName] = useState('')
|
||||||
|
|
@ -176,7 +178,12 @@ export default function SkillsCatalogAdmin() {
|
||||||
keywords: s.keywords || '',
|
keywords: s.keywords || '',
|
||||||
status: s.status || 'active',
|
status: s.status || 'active',
|
||||||
sort_order: s.sort_order ?? '',
|
sort_order: s.sort_order ?? '',
|
||||||
category_id: s.category_id ?? ''
|
category_id: s.category_id ?? '',
|
||||||
|
karate_relevance: s.karate_relevance || '',
|
||||||
|
relevance_level:
|
||||||
|
s.relevance_level != null && s.relevance_level !== ''
|
||||||
|
? String(s.relevance_level)
|
||||||
|
: ''
|
||||||
})
|
})
|
||||||
setEditDialog({ type: 'skill', id: s.id })
|
setEditDialog({ type: 'skill', id: s.id })
|
||||||
}
|
}
|
||||||
|
|
@ -304,7 +311,15 @@ export default function SkillsCatalogAdmin() {
|
||||||
skillForm.sort_order === '' || skillForm.sort_order == null
|
skillForm.sort_order === '' || skillForm.sort_order == null
|
||||||
? null
|
? null
|
||||||
: Number(skillForm.sort_order),
|
: Number(skillForm.sort_order),
|
||||||
category_id: cid
|
category_id: cid,
|
||||||
|
karate_relevance:
|
||||||
|
typeof skillForm.karate_relevance === 'string' && skillForm.karate_relevance.trim()
|
||||||
|
? skillForm.karate_relevance.trim()
|
||||||
|
: null,
|
||||||
|
relevance_level:
|
||||||
|
skillForm.relevance_level === '' || skillForm.relevance_level == null
|
||||||
|
? null
|
||||||
|
: Number(skillForm.relevance_level)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
|
@ -922,6 +937,31 @@ export default function SkillsCatalogAdmin() {
|
||||||
onChange={(e) => setSkillForm((f) => ({ ...f, description: e.target.value }))}
|
onChange={(e) => setSkillForm((f) => ({ ...f, description: e.target.value }))}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
/>
|
/>
|
||||||
|
<label className="form-label">Karate-Relevanz (Wiki)</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows={3}
|
||||||
|
value={skillForm.karate_relevance}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSkillForm((f) => ({ ...f, karate_relevance: e.target.value }))
|
||||||
|
}
|
||||||
|
disabled={busy}
|
||||||
|
placeholder="Freitext aus Wiki / eigene Erläuterung"
|
||||||
|
/>
|
||||||
|
<label className="form-label">Relevanzgrad (Wiki, 1–3)</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={skillForm.relevance_level === '' ? '' : String(skillForm.relevance_level)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSkillForm((f) => ({ ...f, relevance_level: e.target.value }))
|
||||||
|
}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
<option value="">– nicht gesetzt –</option>
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
</select>
|
||||||
<label className="form-label">Sortierung (optional)</label>
|
<label className="form-label">Sortierung (optional)</label>
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,13 @@ const WIKI_IMPORT_TABS = [
|
||||||
{ id: 'history', label: 'Historie', icon: History },
|
{ id: 'history', label: 'Historie', icon: History },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** MediaWiki-Kategorienamen ohne Präfix „Kategorie:“ — karatetrainer.net aktueller Stand */
|
||||||
|
function defaultWikiCategory(importType) {
|
||||||
|
if (importType === 'skill') return 'Fähigkeitsbeschreibung'
|
||||||
|
if (importType === 'method') return 'Methodenbeschreibung'
|
||||||
|
return 'Übungen'
|
||||||
|
}
|
||||||
|
|
||||||
export default function MediaWikiImportPage() {
|
export default function MediaWikiImportPage() {
|
||||||
const [activeTab, setActiveTab] = useState('preview')
|
const [activeTab, setActiveTab] = useState('preview')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
@ -169,7 +176,11 @@ export default function MediaWikiImportPage() {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={previewType}
|
value={previewType}
|
||||||
onChange={(e) => setPreviewType(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const t = e.target.value
|
||||||
|
setPreviewType(t)
|
||||||
|
setPreviewCategory(defaultWikiCategory(t))
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
|
|
@ -315,7 +326,11 @@ export default function MediaWikiImportPage() {
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={executeType}
|
value={executeType}
|
||||||
onChange={(e) => setExecuteType(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const t = e.target.value
|
||||||
|
setExecuteType(t)
|
||||||
|
setExecuteCategory(defaultWikiCategory(t))
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,9 @@ function SkillsPage() {
|
||||||
description: '',
|
description: '',
|
||||||
importance: 3,
|
importance: 3,
|
||||||
keywords: [],
|
keywords: [],
|
||||||
status: 'active'
|
status: 'active',
|
||||||
|
karate_relevance: '',
|
||||||
|
relevance_level: ''
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|
@ -102,10 +104,20 @@ function SkillsPage() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (modalType === 'skill') {
|
if (modalType === 'skill') {
|
||||||
|
const raw = { ...formData }
|
||||||
|
raw.karate_relevance =
|
||||||
|
typeof raw.karate_relevance === 'string' && raw.karate_relevance.trim()
|
||||||
|
? raw.karate_relevance.trim()
|
||||||
|
: null
|
||||||
|
raw.relevance_level =
|
||||||
|
raw.relevance_level === '' || raw.relevance_level == null || raw.relevance_level === undefined
|
||||||
|
? null
|
||||||
|
: Number(raw.relevance_level)
|
||||||
|
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await api.updateSkill(editing.id, formData)
|
await api.updateSkill(editing.id, raw)
|
||||||
} else {
|
} else {
|
||||||
await api.createSkill(formData)
|
await api.createSkill(raw)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
|
|
@ -394,6 +406,41 @@ function SkillsPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{modalType === 'skill' && (
|
||||||
|
<>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Karate-Relevanz (Wiki)</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows={3}
|
||||||
|
value={formData.karate_relevance || ''}
|
||||||
|
onChange={(e) => updateFormField('karate_relevance', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Relevanzgrad (Wiki, 1–3)</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={
|
||||||
|
formData.relevance_level === null ||
|
||||||
|
formData.relevance_level === undefined ||
|
||||||
|
formData.relevance_level === ''
|
||||||
|
? ''
|
||||||
|
: String(formData.relevance_level)
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateFormField('relevance_level', e.target.value === '' ? '' : e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">– nicht gesetzt –</option>
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{modalType === 'skill' && (
|
{modalType === 'skill' && (
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-label">Wichtigkeit (1-5)</label>
|
<label className="form-label">Wichtigkeit (1-5)</label>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user