shinkan-jinkendo/frontend/src/components/admin/SkillsCatalogAdmin.jsx
Lars 39b1fd04f0
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Has been cancelled
Enhance Skills Catalog Admin with Unassigned Skill Handling and Improved Selection Logic
- Introduced utility functions to count skills without main categories and categories, enhancing data management in the Skills Catalog Admin.
- Updated the SkillsCatalogAdmin component to handle unassigned main and category IDs, improving user experience when managing skills.
- Refactored skill selection logic to utilize new utility functions, ensuring accurate filtering of skills based on selected categories and main categories.
- Enhanced the UI to display unassigned skills clearly, improving overall usability and clarity in skill management.
2026-05-20 11:16:49 +02:00

1140 lines
41 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 React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useAuth } from '../../context/AuthContext'
import api from '../../utils/api'
import {
catalogSkillsForSelection,
countCatalogSkillsWithoutCategory,
countCatalogSkillsWithoutMain,
SKILL_CATEGORY_UNASSIGNED_KEY,
SKILL_MAIN_UNASSIGNED_KEY,
SKILL_UNASSIGNED_CATEGORY_LABEL,
SKILL_UNASSIGNED_MAIN_LABEL,
} from '../../utils/skillCatalogTree'
function bySortThenName(a, b) {
const sa = a.sort_order != null ? Number(a.sort_order) : 99999
const sb = b.sort_order != null ? Number(b.sort_order) : 99999
if (sa !== sb) return sa - sb
return String(a.name || '').localeCompare(String(b.name || ''), 'de')
}
/** Tauscht sort_order mit dem direkten Nachbarn in der aktuellen Sortierung (stabil per ID). */
async function swapNeighborSortById(sortedList, rowId, delta, updateId) {
const sorted = [...sortedList].sort(bySortThenName)
const i = sorted.findIndex((x) => x.id === rowId)
const j = i + delta
if (i < 0 || j < 0 || j >= sorted.length) return
const a = sorted[i]
const b = sorted[j]
let oa = a.sort_order
let ob = b.sort_order
if (oa == null || oa === '') oa = (i + 1) * 10
else oa = Number(oa)
if (ob == null || ob === '') ob = (j + 1) * 10
else ob = Number(ob)
await Promise.all([
updateId(a.id, { sort_order: ob }),
updateId(b.id, { sort_order: oa })
])
}
function stop(e) {
e.preventDefault()
e.stopPropagation()
}
export default function SkillsCatalogAdmin() {
const { user } = useAuth()
const isSuperadmin = user?.role === 'superadmin'
const [mains, setMains] = useState([])
const [categories, setCategories] = useState([])
const [allCategories, setAllCategories] = useState([])
const [catalog, setCatalog] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
const [selectedMainId, setSelectedMainId] = useState(null)
const [selectedCategoryId, setSelectedCategoryId] = useState(null)
const [selectedSkillId, setSelectedSkillId] = useState(null)
const [mainForm, setMainForm] = useState({
name: '',
slug: '',
description: '',
sort_order: ''
})
const [categoryForm, setCategoryForm] = useState({
name: '',
slug: '',
description: '',
main_category_id: '',
sort_order: ''
})
const [skillForm, setSkillForm] = useState({
name: '',
description: '',
category: '',
keywords: '',
status: 'active',
sort_order: '',
category_id: '',
main_category_id: '',
karate_relevance: '',
relevance_level: ''
})
const [newMainName, setNewMainName] = useState('')
const [newCategoryName, setNewCategoryName] = useState('')
const [newSkillName, setNewSkillName] = useState('')
/** Aktives Bearbeiten-Modal: Typ + Entitäts-ID */
const [editDialog, setEditDialog] = useState(null)
const refreshCategories = useCallback(async (mainId) => {
if (mainId == null || mainId === SKILL_MAIN_UNASSIGNED_KEY || typeof mainId !== 'number') {
setCategories([])
return
}
const c = await api.listSkillCategories({ main_category_id: mainId })
setCategories([...c].sort(bySortThenName))
}, [])
const bootstrap = useCallback(async () => {
setError('')
try {
const [m, s, ac] = await Promise.all([
api.listSkillMainCategories(),
api.listSkillsCatalog({ status: 'all' }),
api.listSkillCategories({})
])
setMains([...m].sort(bySortThenName))
setCatalog(s)
setAllCategories([...ac].sort(bySortThenName))
} catch (e) {
setError(e.message || String(e))
}
}, [])
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
await bootstrap()
if (!cancelled) setLoading(false)
})()
return () => {
cancelled = true
}
}, [bootstrap])
useEffect(() => {
refreshCategories(selectedMainId)
}, [selectedMainId, refreshCategories])
const sortedMains = useMemo(() => [...mains].sort(bySortThenName), [mains])
const sortedCategories = useMemo(() => [...categories].sort(bySortThenName), [categories])
const unassignedMainCount = useMemo(() => countCatalogSkillsWithoutMain(catalog), [catalog])
const unassignedCategoryCount = useMemo(
() => countCatalogSkillsWithoutCategory(catalog, selectedMainId),
[catalog, selectedMainId]
)
const skillsInSelection = useMemo(() => {
if (selectedCategoryId == null) return []
return [...catalogSkillsForSelection(catalog, selectedMainId, selectedCategoryId)].sort(
bySortThenName
)
}, [catalog, selectedMainId, selectedCategoryId])
useEffect(() => {
if (!editDialog) return
const onKey = (ev) => {
if (ev.key === 'Escape') setEditDialog(null)
}
document.addEventListener('keydown', onKey)
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', onKey)
document.body.style.overflow = prev
}
}, [editDialog])
function openEditMain(m) {
setMainForm({
name: m.name || '',
slug: m.slug || '',
description: m.description || '',
sort_order: m.sort_order ?? ''
})
setEditDialog({ type: 'main', id: m.id })
}
function openEditCategory(c) {
setCategoryForm({
name: c.name || '',
slug: c.slug || '',
description: c.description || '',
main_category_id: c.main_category_id ?? '',
sort_order: c.sort_order ?? ''
})
setEditDialog({ type: 'category', id: c.id })
}
function openEditSkill(s) {
setSkillForm({
name: s.name || '',
description: s.description || '',
category: s.category || '',
keywords: s.keywords || '',
status: s.status || 'active',
sort_order: s.sort_order ?? '',
category_id: s.category_id ?? '',
main_category_id: s.main_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 })
}
function closeEditDialog() {
setEditDialog(null)
}
/** @returns {Promise<boolean>} true bei Erfolg */
async function run(op) {
setBusy(true)
setMessage('')
try {
await op()
await bootstrap()
if (typeof selectedMainId === 'number') await refreshCategories(selectedMainId)
setMessage('Gespeichert.')
return true
} catch (e) {
setError(e.message || String(e))
return false
} finally {
setBusy(false)
}
}
function selectMain(id) {
setSelectedMainId(id)
setSelectedCategoryId(null)
setSelectedSkillId(null)
}
function selectCategory(id) {
setSelectedCategoryId(id)
setSelectedSkillId(null)
}
async function handleSwapMain(rowId, delta) {
await run(async () => {
await swapNeighborSortById(sortedMains, rowId, delta, (id, data) =>
api.updateSkillMainCategory(id, data)
)
})
}
async function handleSwapCategory(rowId, delta) {
await run(async () => {
await swapNeighborSortById(sortedCategories, rowId, delta, (id, data) =>
api.updateSkillCategory(id, data)
)
})
}
async function handleSwapSkill(rowId, delta) {
const sorted = [...skillsInSelection].sort(bySortThenName)
await run(async () => {
await swapNeighborSortById(sorted, rowId, delta, (id, data) => api.updateSkill(id, data))
})
}
async function handleSaveMain(e) {
e.preventDefault()
if (!editDialog || editDialog.type !== 'main') return
const ok = await run(async () => {
await api.updateSkillMainCategory(editDialog.id, {
name: mainForm.name.trim(),
slug: (mainForm.slug || '').trim() || undefined,
description: mainForm.description || null,
sort_order:
mainForm.sort_order === '' || mainForm.sort_order == null
? null
: Number(mainForm.sort_order)
})
})
if (ok) closeEditDialog()
}
async function handleSaveCategory(e) {
e.preventDefault()
if (!editDialog || editDialog.type !== 'category') return
const mid =
categoryForm.main_category_id === '' || categoryForm.main_category_id == null
? null
: Number(categoryForm.main_category_id)
const ok = await run(async () => {
await api.updateSkillCategory(editDialog.id, {
name: categoryForm.name.trim(),
slug: (categoryForm.slug || '').trim() || undefined,
description: categoryForm.description || null,
main_category_id: mid,
sort_order:
categoryForm.sort_order === '' || categoryForm.sort_order == null
? null
: Number(categoryForm.sort_order)
})
})
if (ok) {
if (mid != null && mid !== selectedMainId) {
setSelectedMainId(mid)
setSelectedSkillId(null)
}
closeEditDialog()
}
}
async function handleSaveSkill(e) {
e.preventDefault()
if (!editDialog || editDialog.type !== 'skill') return
const cid =
skillForm.category_id === '' || skillForm.category_id == null
? null
: Number(skillForm.category_id)
const mid =
cid == null &&
skillForm.main_category_id !== '' &&
skillForm.main_category_id != null
? Number(skillForm.main_category_id)
: null
const ok = await run(async () => {
const payload = {
name: skillForm.name.trim(),
description: skillForm.description || null,
category: skillForm.category || null,
keywords: skillForm.keywords || null,
status: skillForm.status,
sort_order:
skillForm.sort_order === '' || skillForm.sort_order == null
? null
: Number(skillForm.sort_order),
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 (cid == null) {
payload.main_category_id = mid
}
await api.updateSkill(editDialog.id, payload)
})
if (ok) {
if (cid != null) {
const cat = allCategories.find((c) => c.id === cid)
if (cat?.main_category_id) {
setSelectedMainId(cat.main_category_id)
setSelectedCategoryId(cid)
}
} else if (mid == null) {
setSelectedMainId(SKILL_MAIN_UNASSIGNED_KEY)
setSelectedCategoryId(SKILL_CATEGORY_UNASSIGNED_KEY)
} else {
setSelectedMainId(mid)
setSelectedCategoryId(SKILL_CATEGORY_UNASSIGNED_KEY)
}
closeEditDialog()
}
}
async function handleCreateMain(e) {
e.preventDefault()
const name = newMainName.trim()
if (!name) return
await run(async () => {
const created = await api.createSkillMainCategory({ name })
setNewMainName('')
selectMain(created.id)
})
}
async function handleCreateCategory(e) {
e.preventDefault()
if (selectedMainId == null || selectedMainId === SKILL_MAIN_UNASSIGNED_KEY) return
const name = newCategoryName.trim()
if (!name) return
await run(async () => {
const created = await api.createSkillCategory({
name,
main_category_id: selectedMainId
})
setNewCategoryName('')
setSelectedCategoryId(created.id)
setSelectedSkillId(null)
})
}
async function handleCreateSkill(e) {
e.preventDefault()
if (selectedCategoryId == null) return
const name = newSkillName.trim()
if (!name) return
await run(async () => {
const body = { name, status: 'active' }
if (typeof selectedCategoryId === 'number') {
body.category_id = selectedCategoryId
} else if (selectedCategoryId === SKILL_CATEGORY_UNASSIGNED_KEY) {
body.category_id = null
body.main_category_id =
typeof selectedMainId === 'number' ? selectedMainId : null
} else {
return
}
const created = await api.createSkill(body)
setNewSkillName('')
setSelectedSkillId(created.id)
})
}
async function handleDeleteMain() {
if (!editDialog || editDialog.type !== 'main' || !isSuperadmin) return
if (!window.confirm('Hauptkategorie wirklich löschen?')) return
const id = editDialog.id
const ok = await run(async () => {
await api.deleteSkillMainCategory(id)
if (selectedMainId === id) {
setSelectedMainId(null)
setSelectedCategoryId(null)
setSelectedSkillId(null)
}
})
if (ok) closeEditDialog()
}
async function handleDeleteCategory() {
if (!editDialog || editDialog.type !== 'category' || !isSuperadmin) return
if (!window.confirm('Kategorie wirklich löschen?')) return
const id = editDialog.id
const ok = await run(async () => {
await api.deleteSkillCategory(id)
if (selectedCategoryId === id) {
setSelectedCategoryId(null)
setSelectedSkillId(null)
}
})
if (ok) closeEditDialog()
}
async function handleDeleteSkill() {
if (!editDialog || editDialog.type !== 'skill' || !isSuperadmin) return
if (!window.confirm('Fähigkeit wirklich löschen?')) return
const id = editDialog.id
const ok = await run(async () => {
await api.deleteSkill(id)
if (selectedSkillId === id) setSelectedSkillId(null)
})
if (ok) closeEditDialog()
}
if (loading) {
return (
<div className="skills-catalog-admin">
<p className="muted">Lade Katalog</p>
</div>
)
}
return (
<div className="skills-catalog-admin">
<p className="skills-catalog-admin__intro muted">
Struktur: <strong>Hauptkategorie</strong> <strong>Kategorie</strong> {' '}
<strong>Fähigkeit</strong>. Fähigkeiten ohne Zuordnung finden Sie unter{' '}
<strong>{SKILL_UNASSIGNED_MAIN_LABEL}</strong> bzw. <strong>{SKILL_UNASSIGNED_CATEGORY_LABEL}</strong>{' '}
(wie in der Auswahlbox). Reihenfolge mit Pfeilen; Details über das Stift-Symbol.
</p>
{error ? (
<div className="skills-catalog-admin__error" role="alert">
{error}
<button
type="button"
className="btn btn-secondary"
style={{ marginLeft: 12 }}
onClick={() => {
setError('')
bootstrap()
}}
>
Erneut laden
</button>
</div>
) : null}
{message ? <p className="muted skills-catalog-admin__msg">{message}</p> : null}
<div className="skills-catalog-layout">
<section className="skills-catalog-column" aria-label="Hauptkategorien">
<h3 className="skills-catalog-column__title">Hauptkategorien</h3>
<ul className="skills-catalog-list">
{unassignedMainCount > 0 ? (
<li>
<div
className={
'skills-catalog-row' +
(selectedMainId === SKILL_MAIN_UNASSIGNED_KEY
? ' skills-catalog-row--active'
: '')
}
>
<button
type="button"
className="skills-catalog-row__label"
onClick={() => selectMain(SKILL_MAIN_UNASSIGNED_KEY)}
>
{SKILL_UNASSIGNED_MAIN_LABEL} ({unassignedMainCount})
</button>
</div>
</li>
) : null}
{sortedMains.map((m) => (
<li key={m.id}>
<div
className={
'skills-catalog-row' +
(selectedMainId === m.id ? ' skills-catalog-row--active' : '')
}
>
<button
type="button"
className="skills-catalog-row__label"
onClick={() => selectMain(m.id)}
>
{m.name}
</button>
<span className="skills-catalog-row__actions">
<button
type="button"
className="btn btn-secondary btn-tiny"
disabled={busy}
title="Nach oben"
aria-label="Nach oben sortieren"
onClick={(e) => {
stop(e)
handleSwapMain(m.id, -1)
}}
>
</button>
<button
type="button"
className="btn btn-secondary btn-tiny"
disabled={busy}
title="Nach unten"
aria-label="Nach unten sortieren"
onClick={(e) => {
stop(e)
handleSwapMain(m.id, 1)
}}
>
</button>
<button
type="button"
className="btn btn-ghost btn-icon-touch"
disabled={busy}
title="Bearbeiten"
aria-label={`Hauptkategorie ${m.name} bearbeiten`}
onClick={(e) => {
stop(e)
openEditMain(m)
}}
>
</button>
</span>
</div>
</li>
))}
</ul>
<form className="skills-catalog-quick" onSubmit={handleCreateMain}>
<label className="form-label">Neue Hauptkategorie</label>
<input
className="form-input"
value={newMainName}
onChange={(e) => setNewMainName(e.target.value)}
placeholder="Name"
disabled={busy}
/>
<button type="submit" className="btn btn-primary btn-full" disabled={busy}>
Anlegen
</button>
</form>
</section>
<section className="skills-catalog-column" aria-label="Kategorien">
<h3 className="skills-catalog-column__title">Kategorien</h3>
{selectedMainId == null ? (
<p className="muted skills-catalog-placeholder">Zuerst eine Hauptkategorie wählen.</p>
) : selectedMainId === SKILL_MAIN_UNASSIGNED_KEY ? (
<>
<ul className="skills-catalog-list">
{unassignedCategoryCount > 0 ? (
<li>
<div
className={
'skills-catalog-row' +
(selectedCategoryId === SKILL_CATEGORY_UNASSIGNED_KEY
? ' skills-catalog-row--active'
: '')
}
>
<button
type="button"
className="skills-catalog-row__label"
onClick={() => selectCategory(SKILL_CATEGORY_UNASSIGNED_KEY)}
>
{SKILL_UNASSIGNED_CATEGORY_LABEL} ({unassignedCategoryCount})
</button>
</div>
</li>
) : null}
</ul>
<p className="muted skills-catalog-placeholder">
Unter {SKILL_UNASSIGNED_MAIN_LABEL} gibt es keine Unterkategorien.
</p>
</>
) : (
<>
<ul className="skills-catalog-list">
{unassignedCategoryCount > 0 ? (
<li>
<div
className={
'skills-catalog-row' +
(selectedCategoryId === SKILL_CATEGORY_UNASSIGNED_KEY
? ' skills-catalog-row--active'
: '')
}
>
<button
type="button"
className="skills-catalog-row__label"
onClick={() => selectCategory(SKILL_CATEGORY_UNASSIGNED_KEY)}
>
{SKILL_UNASSIGNED_CATEGORY_LABEL} ({unassignedCategoryCount})
</button>
</div>
</li>
) : null}
{sortedCategories.map((c) => (
<li key={c.id}>
<div
className={
'skills-catalog-row' +
(selectedCategoryId === c.id ? ' skills-catalog-row--active' : '')
}
>
<button
type="button"
className="skills-catalog-row__label"
onClick={() => selectCategory(c.id)}
>
{c.name}
</button>
<span className="skills-catalog-row__actions">
<button
type="button"
className="btn btn-secondary btn-tiny"
disabled={busy}
title="Nach oben"
aria-label="Nach oben sortieren"
onClick={(e) => {
stop(e)
handleSwapCategory(c.id, -1)
}}
>
</button>
<button
type="button"
className="btn btn-secondary btn-tiny"
disabled={busy}
title="Nach unten"
aria-label="Nach unten sortieren"
onClick={(e) => {
stop(e)
handleSwapCategory(c.id, 1)
}}
>
</button>
<button
type="button"
className="btn btn-ghost btn-icon-touch"
disabled={busy}
title="Bearbeiten"
aria-label={`Kategorie ${c.name} bearbeiten`}
onClick={(e) => {
stop(e)
openEditCategory(c)
}}
>
</button>
</span>
</div>
</li>
))}
</ul>
<form className="skills-catalog-quick" onSubmit={handleCreateCategory}>
<label className="form-label">Neue Kategorie</label>
<input
className="form-input"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
placeholder="Name"
disabled={busy}
/>
<button type="submit" className="btn btn-primary btn-full" disabled={busy}>
Anlegen
</button>
</form>
</>
)}
</section>
<section className="skills-catalog-column" aria-label="Fähigkeiten">
<h3 className="skills-catalog-column__title">Fähigkeiten</h3>
{selectedCategoryId == null ? (
<p className="muted skills-catalog-placeholder">
Zuerst eine Kategorie oder {SKILL_UNASSIGNED_CATEGORY_LABEL} wählen.
</p>
) : (
<>
<ul className="skills-catalog-list">
{skillsInSelection.map((s) => (
<li key={s.id}>
<div
className={
'skills-catalog-row' +
(selectedSkillId === s.id ? ' skills-catalog-row--active' : '')
}
>
<button
type="button"
className="skills-catalog-row__label"
onClick={() => setSelectedSkillId(s.id)}
>
{s.name}
{s.status && s.status !== 'active' ? (
<span className="skills-catalog-row__badge">{s.status}</span>
) : null}
</button>
<span className="skills-catalog-row__actions">
<button
type="button"
className="btn btn-secondary btn-tiny"
disabled={busy}
title="Nach oben"
aria-label="Nach oben sortieren"
onClick={(e) => {
stop(e)
handleSwapSkill(s.id, -1)
}}
>
</button>
<button
type="button"
className="btn btn-secondary btn-tiny"
disabled={busy}
title="Nach unten"
aria-label="Nach unten sortieren"
onClick={(e) => {
stop(e)
handleSwapSkill(s.id, 1)
}}
>
</button>
<button
type="button"
className="btn btn-ghost btn-icon-touch"
disabled={busy}
title="Bearbeiten"
aria-label={`Fähigkeit ${s.name} bearbeiten`}
onClick={(e) => {
stop(e)
openEditSkill(s)
}}
>
</button>
</span>
</div>
</li>
))}
</ul>
<form className="skills-catalog-quick" onSubmit={handleCreateSkill}>
<label className="form-label">Neue Fähigkeit</label>
<input
className="form-input"
value={newSkillName}
onChange={(e) => setNewSkillName(e.target.value)}
placeholder="Name"
disabled={busy}
/>
<button type="submit" className="btn btn-primary btn-full" disabled={busy}>
Anlegen
</button>
</form>
</>
)}
</section>
</div>
{editDialog ? (
<div
className="admin-modal-backdrop"
role="presentation"
onClick={closeEditDialog}
>
<div
className="admin-modal-sheet"
role="dialog"
aria-modal="true"
aria-labelledby="skills-catalog-edit-title"
onClick={(e) => e.stopPropagation()}
>
<div className="admin-modal-sheet__header">
<h3 id="skills-catalog-edit-title" className="admin-modal-sheet__title">
{editDialog.type === 'main'
? 'Hauptkategorie bearbeiten'
: editDialog.type === 'category'
? 'Kategorie bearbeiten'
: 'Fähigkeit bearbeiten'}
</h3>
<button
type="button"
className="btn btn-secondary admin-modal-sheet__close"
onClick={closeEditDialog}
aria-label="Schließen"
>
</button>
</div>
<div className="admin-modal-sheet__body">
{editDialog.type === 'main' ? (
<form className="skills-catalog-form" onSubmit={handleSaveMain}>
<label className="form-label">Name</label>
<input
className="form-input"
value={mainForm.name}
onChange={(e) => setMainForm((f) => ({ ...f, name: e.target.value }))}
disabled={busy}
autoComplete="off"
/>
<label className="form-label">Slug</label>
<input
className="form-input"
value={mainForm.slug}
onChange={(e) => setMainForm((f) => ({ ...f, slug: e.target.value }))}
disabled={busy}
/>
<label className="form-label">Beschreibung</label>
<textarea
className="form-input"
rows={3}
value={mainForm.description}
onChange={(e) => setMainForm((f) => ({ ...f, description: e.target.value }))}
disabled={busy}
/>
<label className="form-label">Sortierung (Zahl, optional)</label>
<input
className="form-input"
type="number"
inputMode="numeric"
value={mainForm.sort_order}
onChange={(e) => setMainForm((f) => ({ ...f, sort_order: e.target.value }))}
disabled={busy}
/>
<div className="skills-catalog-form__actions">
<button type="submit" className="btn btn-primary" disabled={busy}>
Speichern
</button>
<button
type="button"
className="btn btn-secondary"
disabled={busy}
onClick={closeEditDialog}
>
Abbrechen
</button>
{isSuperadmin ? (
<button
type="button"
className="btn btn-secondary"
disabled={busy}
onClick={handleDeleteMain}
>
Löschen
</button>
) : null}
</div>
</form>
) : null}
{editDialog.type === 'category' ? (
<form className="skills-catalog-form" onSubmit={handleSaveCategory}>
<label className="form-label">Name</label>
<input
className="form-input"
value={categoryForm.name}
onChange={(e) => setCategoryForm((f) => ({ ...f, name: e.target.value }))}
disabled={busy}
/>
<label className="form-label">Slug</label>
<input
className="form-input"
value={categoryForm.slug}
onChange={(e) => setCategoryForm((f) => ({ ...f, slug: e.target.value }))}
disabled={busy}
/>
<label className="form-label">Hauptkategorie (verschieben)</label>
<select
className="form-input"
value={
categoryForm.main_category_id === '' ? '' : String(categoryForm.main_category_id)
}
onChange={(e) =>
setCategoryForm((f) => ({
...f,
main_category_id: e.target.value === '' ? '' : e.target.value
}))
}
disabled={busy}
>
<option value=""> keine </option>
{sortedMains.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
<label className="form-label">Beschreibung</label>
<textarea
className="form-input"
rows={3}
value={categoryForm.description}
onChange={(e) => setCategoryForm((f) => ({ ...f, description: e.target.value }))}
disabled={busy}
/>
<label className="form-label">Sortierung (optional)</label>
<input
className="form-input"
type="number"
inputMode="numeric"
value={categoryForm.sort_order}
onChange={(e) => setCategoryForm((f) => ({ ...f, sort_order: e.target.value }))}
disabled={busy}
/>
<div className="skills-catalog-form__actions">
<button type="submit" className="btn btn-primary" disabled={busy}>
Speichern
</button>
<button
type="button"
className="btn btn-secondary"
disabled={busy}
onClick={closeEditDialog}
>
Abbrechen
</button>
{isSuperadmin ? (
<button
type="button"
className="btn btn-secondary"
disabled={busy}
onClick={handleDeleteCategory}
>
Löschen
</button>
) : null}
</div>
</form>
) : null}
{editDialog.type === 'skill' ? (
<form className="skills-catalog-form" onSubmit={handleSaveSkill}>
<label className="form-label">Name</label>
<input
className="form-input"
value={skillForm.name}
onChange={(e) => setSkillForm((f) => ({ ...f, name: e.target.value }))}
disabled={busy}
/>
<label className="form-label">Kategorie (verschieben)</label>
<select
className="form-input"
value={
skillForm.category_id === '' || skillForm.category_id == null
? ''
: String(skillForm.category_id)
}
onChange={(e) =>
setSkillForm((f) => ({
...f,
category_id: e.target.value === '' ? '' : e.target.value
}))
}
disabled={busy}
>
<option value="">{SKILL_UNASSIGNED_CATEGORY_LABEL}</option>
{allCategories.map((c) => (
<option key={c.id} value={c.id}>
{(c.main_category_name ? c.main_category_name + ' · ' : '') + c.name}
</option>
))}
</select>
{skillForm.category_id === '' || skillForm.category_id == null ? (
<>
<label className="form-label">Hauptkategorie</label>
<select
className="form-input"
value={
skillForm.main_category_id === '' || skillForm.main_category_id == null
? ''
: String(skillForm.main_category_id)
}
onChange={(e) =>
setSkillForm((f) => ({
...f,
main_category_id: e.target.value === '' ? '' : e.target.value
}))
}
disabled={busy}
>
<option value="">{SKILL_UNASSIGNED_MAIN_LABEL}</option>
{sortedMains.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
</>
) : null}
<label className="form-label">Legacy-Kurzlabel category (optional)</label>
<input
className="form-input"
value={skillForm.category}
onChange={(e) => setSkillForm((f) => ({ ...f, category: e.target.value }))}
disabled={busy}
/>
<label className="form-label">Stichwörter</label>
<input
className="form-input"
value={skillForm.keywords}
onChange={(e) => setSkillForm((f) => ({ ...f, keywords: e.target.value }))}
disabled={busy}
/>
<label className="form-label">Status</label>
<select
className="form-input"
value={skillForm.status}
onChange={(e) => setSkillForm((f) => ({ ...f, status: e.target.value }))}
disabled={busy}
>
<option value="active">active</option>
<option value="inactive">inactive</option>
</select>
<label className="form-label">Beschreibung</label>
<textarea
className="form-input"
rows={4}
value={skillForm.description}
onChange={(e) => setSkillForm((f) => ({ ...f, description: e.target.value }))}
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, 13)</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>
<input
className="form-input"
type="number"
inputMode="numeric"
value={skillForm.sort_order}
onChange={(e) => setSkillForm((f) => ({ ...f, sort_order: e.target.value }))}
disabled={busy}
/>
<div className="skills-catalog-form__actions">
<button type="submit" className="btn btn-primary" disabled={busy}>
Speichern
</button>
<button
type="button"
className="btn btn-secondary"
disabled={busy}
onClick={closeEditDialog}
>
Abbrechen
</button>
{isSuperadmin ? (
<button
type="button"
className="btn btn-secondary"
disabled={busy}
onClick={handleDeleteSkill}
>
Löschen
</button>
) : null}
</div>
</form>
) : null}
</div>
</div>
</div>
) : null}
</div>
)
}