diff --git a/frontend/src/components/admin/SkillsCatalogAdmin.jsx b/frontend/src/components/admin/SkillsCatalogAdmin.jsx index 83cca0a..3225d01 100644 --- a/frontend/src/components/admin/SkillsCatalogAdmin.jsx +++ b/frontend/src/components/admin/SkillsCatalogAdmin.jsx @@ -1,6 +1,15 @@ 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 @@ -72,6 +81,7 @@ export default function SkillsCatalogAdmin() { status: 'active', sort_order: '', category_id: '', + main_category_id: '', karate_relevance: '', relevance_level: '' }) @@ -84,7 +94,7 @@ export default function SkillsCatalogAdmin() { const [editDialog, setEditDialog] = useState(null) const refreshCategories = useCallback(async (mainId) => { - if (mainId == null) { + if (mainId == null || mainId === SKILL_MAIN_UNASSIGNED_KEY || typeof mainId !== 'number') { setCategories([]) return } @@ -128,12 +138,19 @@ export default function SkillsCatalogAdmin() { const sortedCategories = useMemo(() => [...categories].sort(bySortThenName), [categories]) - const skillsInCategory = useMemo(() => { + const unassignedMainCount = useMemo(() => countCatalogSkillsWithoutMain(catalog), [catalog]) + + const unassignedCategoryCount = useMemo( + () => countCatalogSkillsWithoutCategory(catalog, selectedMainId), + [catalog, selectedMainId] + ) + + const skillsInSelection = useMemo(() => { if (selectedCategoryId == null) return [] - return catalog - .filter((s) => s.category_id === selectedCategoryId) - .sort(bySortThenName) - }, [catalog, selectedCategoryId]) + return [...catalogSkillsForSelection(catalog, selectedMainId, selectedCategoryId)].sort( + bySortThenName + ) + }, [catalog, selectedMainId, selectedCategoryId]) useEffect(() => { if (!editDialog) return @@ -179,6 +196,7 @@ export default function SkillsCatalogAdmin() { 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 !== '' @@ -199,7 +217,7 @@ export default function SkillsCatalogAdmin() { try { await op() await bootstrap() - if (selectedMainId) await refreshCategories(selectedMainId) + if (typeof selectedMainId === 'number') await refreshCategories(selectedMainId) setMessage('Gespeichert.') return true } catch (e) { @@ -238,7 +256,7 @@ export default function SkillsCatalogAdmin() { } async function handleSwapSkill(rowId, delta) { - const sorted = [...skillsInCategory].sort(bySortThenName) + const sorted = [...skillsInSelection].sort(bySortThenName) await run(async () => { await swapNeighborSortById(sorted, rowId, delta, (id, data) => api.updateSkill(id, data)) }) @@ -292,16 +310,18 @@ export default function SkillsCatalogAdmin() { async function handleSaveSkill(e) { e.preventDefault() if (!editDialog || editDialog.type !== 'skill') return - let cid = + const 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 mid = + cid == null && + skillForm.main_category_id !== '' && + skillForm.main_category_id != null + ? Number(skillForm.main_category_id) + : null const ok = await run(async () => { - await api.updateSkill(editDialog.id, { + const payload = { name: skillForm.name.trim(), description: skillForm.description || null, category: skillForm.category || null, @@ -319,16 +339,26 @@ export default function SkillsCatalogAdmin() { relevance_level: skillForm.relevance_level === '' || skillForm.relevance_level == null ? null - : Number(skillForm.relevance_level) - }) + : Number(skillForm.relevance_level), + } + if (cid == null) { + payload.main_category_id = mid + } + await api.updateSkill(editDialog.id, payload) }) if (ok) { - if (cid != null && cid !== selectedCategoryId) { + 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() } @@ -347,7 +377,7 @@ export default function SkillsCatalogAdmin() { async function handleCreateCategory(e) { e.preventDefault() - if (selectedMainId == null) return + if (selectedMainId == null || selectedMainId === SKILL_MAIN_UNASSIGNED_KEY) return const name = newCategoryName.trim() if (!name) return await run(async () => { @@ -367,11 +397,17 @@ export default function SkillsCatalogAdmin() { const name = newSkillName.trim() if (!name) return await run(async () => { - const created = await api.createSkill({ - name, - category_id: selectedCategoryId, - status: 'active' - }) + 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) }) @@ -429,8 +465,9 @@ export default function SkillsCatalogAdmin() {

Struktur: HauptkategorieKategorie →{' '} - Fähigkeit. Reihenfolge mit Pfeilen; Details über das Stift-Symbol (öffnet - ein Fenster — auf dem iPhone nach unten scrollen, falls nötig). + Fähigkeit. Fähigkeiten ohne Zuordnung finden Sie unter{' '} + {SKILL_UNASSIGNED_MAIN_LABEL} bzw. {SKILL_UNASSIGNED_CATEGORY_LABEL}{' '} + (wie in der Auswahlbox). Reihenfolge mit Pfeilen; Details über das Stift-Symbol.

{error ? ( @@ -455,6 +492,26 @@ export default function SkillsCatalogAdmin() {

Hauptkategorien