shinkan-jinkendo/frontend/src/components/admin/SkillsCatalogAdmin.jsx

1006 lines
35 KiB
React
Raw Normal View History

import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useAuth } from '../../context/AuthContext'
import api from '../../utils/api'
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: '',
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) {
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 skillsInCategory = useMemo(() => {
if (selectedCategoryId == null) return []
return catalog
.filter((s) => s.category_id === selectedCategoryId)
.sort(bySortThenName)
}, [catalog, 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 ?? '',
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 (selectedMainId) 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 = [...skillsInCategory].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
let cid =
skillForm.category_id === '' || skillForm.category_id == null
? null
: Number(skillForm.category_id)
const skillRow = catalog.find((s) => s.id === editDialog.id)
if (cid == null && skillRow?.category_id) {
cid = skillRow.category_id
}
const ok = await run(async () => {
await api.updateSkill(editDialog.id, {
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 (ok) {
if (cid != null && cid !== selectedCategoryId) {
const cat = allCategories.find((c) => c.id === cid)
if (cat?.main_category_id) {
setSelectedMainId(cat.main_category_id)
setSelectedCategoryId(cid)
}
}
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) 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 created = await api.createSkill({
name,
category_id: selectedCategoryId,
status: 'active'
})
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>. Reihenfolge mit Pfeilen; Details über das Stift-Symbol (öffnet
ein Fenster — auf dem iPhone nach unten scrollen, falls nötig).
</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">
{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>
) : (
<>
<ul className="skills-catalog-list">
{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 wählen.</p>
) : (
<>
<ul className="skills-catalog-list">
{skillsInCategory.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}
required
>
{allCategories.map((c) => (
<option key={c.id} value={c.id}>
{(c.main_category_name ? c.main_category_name + ' · ' : '') + c.name}
</option>
))}
</select>
<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, 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>
<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>
)
}