feat: enhance admin UI for maturity models and skills catalog
- Added new styles and layout for the maturity models admin panel, improving user experience. - Removed unused multi-selection component for focus areas, style directions, and target groups. - Updated skills catalog functionality to support editing and sorting of main categories and skills. - Introduced a modal for editing main categories, categories, and skills with improved accessibility. - Enhanced responsiveness and visual consistency across admin components.
This commit is contained in:
parent
e8b7e62832
commit
469ec93074
|
|
@ -1248,6 +1248,426 @@ a.analysis-split__nav-item {
|
|||
min-width: 2rem;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text2);
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
background: var(--surface2);
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
/* Touch-Ziel mind. ca. 44×44 (Apple HIG) */
|
||||
.btn-icon-touch {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.15rem;
|
||||
line-height: 1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.admin-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.admin-modal-backdrop {
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
padding-bottom: max(24px, env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
.admin-modal-sheet {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
max-height: min(92vh, 100dvh);
|
||||
background: var(--surface);
|
||||
border-radius: 16px 16px 0 0;
|
||||
border: 1px solid var(--border);
|
||||
border-bottom: none;
|
||||
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.admin-modal-sheet {
|
||||
border-radius: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
max-height: min(88vh, 900px);
|
||||
}
|
||||
}
|
||||
|
||||
.admin-modal-sheet__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 16px 12px;
|
||||
padding-top: max(16px, env(safe-area-inset-top, 0px));
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-modal-sheet__title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.admin-modal-sheet__close {
|
||||
flex-shrink: 0;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.admin-modal-sheet__body {
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* Reifegradmodell-Admin: klare Schritte, responsives Raster */
|
||||
.admin-matrix-alert {
|
||||
border: 1px solid var(--danger);
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 12px;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.admin-matrix-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 300px) 1fr;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.admin-matrix-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-matrix-sidebar {
|
||||
position: sticky;
|
||||
top: 12px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.admin-matrix-sidebar {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-matrix-sidebar__title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.admin-matrix-sidebar__subtitle {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-matrix-divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.admin-matrix-model-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: min(40vh, 320px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.admin-matrix-model-list li + li {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.admin-matrix-model-btn {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border2);
|
||||
background: var(--surface2);
|
||||
color: var(--text1);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-height: 48px;
|
||||
}
|
||||
.admin-matrix-model-btn:hover {
|
||||
background: var(--surface);
|
||||
}
|
||||
.admin-matrix-model-btn--active {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-light);
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.admin-matrix-model-btn--active {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
.admin-matrix-model-btn__name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.admin-matrix-model-btn__meta {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-matrix-new-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-matrix-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.admin-matrix-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.admin-matrix-empty {
|
||||
padding: 24px;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.admin-matrix-section__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.admin-matrix-section__title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.admin-matrix-step {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.admin-matrix-hint {
|
||||
font-size: 14px;
|
||||
margin: 0 0 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.admin-matrix-meta-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.admin-matrix-meta-grid__row2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.admin-matrix-meta-grid__row2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-matrix-context-readonly {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: var(--surface2);
|
||||
border: 1px dashed var(--border2);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.admin-matrix-context-readonly strong {
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.admin-matrix-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.admin-matrix-actions--mt {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.admin-matrix-level-count {
|
||||
max-width: 220px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-matrix-table-wrap {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.admin-matrix-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
.admin-matrix-table th,
|
||||
.admin-matrix-table td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.admin-matrix-table thead th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: var(--text2);
|
||||
font-size: 13px;
|
||||
}
|
||||
.admin-matrix-table__narrow {
|
||||
width: 88px;
|
||||
}
|
||||
|
||||
.admin-matrix-skill-add {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.admin-matrix-skill-add__select {
|
||||
flex: 1 1 220px;
|
||||
min-width: 0;
|
||||
}
|
||||
.admin-matrix-skill-add__btn {
|
||||
flex-shrink: 0;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.admin-matrix-skill-list {
|
||||
margin: 12px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.admin-matrix-skill-list__item {
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.admin-matrix-skill-list__item:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
.admin-matrix-skill-list__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.admin-matrix-skill-list__name {
|
||||
font-size: 15px;
|
||||
}
|
||||
.admin-matrix-skill-list__path {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.admin-matrix-matrix-scroll {
|
||||
overflow: auto;
|
||||
max-height: min(70vh, 720px);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.admin-matrix-table--matrix th,
|
||||
.admin-matrix-table--matrix td {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.admin-matrix-matrix__corner {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: var(--surface);
|
||||
min-width: 140px;
|
||||
padding: 8px;
|
||||
}
|
||||
.admin-matrix-matrix__level-head {
|
||||
padding: 8px;
|
||||
min-width: 160px;
|
||||
background: var(--surface2);
|
||||
font-size: 13px;
|
||||
}
|
||||
.admin-matrix-matrix__skill-cell {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background: var(--surface);
|
||||
font-weight: 600;
|
||||
max-width: 220px;
|
||||
vertical-align: top;
|
||||
padding: 8px;
|
||||
}
|
||||
.admin-matrix-matrix__skill-path {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
margin-top: 6px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.admin-matrix-matrix__cell {
|
||||
vertical-align: top;
|
||||
padding: 6px;
|
||||
min-width: 148px;
|
||||
}
|
||||
.admin-matrix-matrix__ta {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
min-width: 120px;
|
||||
}
|
||||
.admin-matrix-matrix__ta--criteria {
|
||||
font-size: 11px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.muted { color: var(--text3); font-size: 13px; }
|
||||
.empty-state { text-align: center; padding: 48px 16px; color: var(--text3); }
|
||||
.empty-state h3 { font-size: 16px; color: var(--text2); margin-bottom: 6px; }
|
||||
|
|
|
|||
|
|
@ -2,31 +2,6 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useAuth } from '../../context/AuthContext'
|
||||
import api from '../../utils/api'
|
||||
|
||||
function MultiIdSelect({ label, options, valueIds, onChange, hint }) {
|
||||
return (
|
||||
<div>
|
||||
<label className="form-label">{label}</label>
|
||||
<select
|
||||
multiple
|
||||
className="form-input"
|
||||
style={{ minHeight: 100, width: '100%' }}
|
||||
value={(valueIds || []).map(String)}
|
||||
onChange={(e) => {
|
||||
const v = Array.from(e.target.selectedOptions, (o) => parseInt(o.value, 10))
|
||||
onChange(v)
|
||||
}}
|
||||
>
|
||||
{options.map((o) => (
|
||||
<option key={o.id} value={o.id}>{o.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{hint ? (
|
||||
<div style={{ fontSize: 12, color: 'var(--text3)', marginTop: 4 }}>{hint}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MaturityModelsAdminPanel() {
|
||||
const { user } = useAuth()
|
||||
const isSuperadmin = user?.role === 'superadmin'
|
||||
|
|
@ -37,17 +12,11 @@ export default function MaturityModelsAdminPanel() {
|
|||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [focusAreas, setFocusAreas] = useState([])
|
||||
const [styles, setStyles] = useState([])
|
||||
const [targetGroups, setTargetGroups] = useState([])
|
||||
const [allSkills, setAllSkills] = useState([])
|
||||
|
||||
const [newModel, setNewModel] = useState({
|
||||
name: '',
|
||||
level_count: 5,
|
||||
focus_area_ids: [],
|
||||
style_direction_ids: [],
|
||||
target_group_ids: [],
|
||||
status: 'draft'
|
||||
})
|
||||
|
||||
|
|
@ -61,18 +30,12 @@ export default function MaturityModelsAdminPanel() {
|
|||
let cancelled = false
|
||||
;(async () => {
|
||||
try {
|
||||
const [m, fa, sd, tg, sk] = await Promise.all([
|
||||
const [m, sk] = await Promise.all([
|
||||
api.listMaturityModels({}),
|
||||
api.listFocusAreas({}),
|
||||
api.listStyleDirections({}),
|
||||
api.listTargetGroups({}),
|
||||
api.listSkills({ status: 'active' })
|
||||
])
|
||||
if (!cancelled) {
|
||||
setModels(m)
|
||||
setFocusAreas(fa)
|
||||
setStyles(sd)
|
||||
setTargetGroups(tg)
|
||||
setAllSkills(sk)
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -97,9 +60,6 @@ export default function MaturityModelsAdminPanel() {
|
|||
setMeta({
|
||||
name: d.name,
|
||||
description: d.description || '',
|
||||
focus_area_ids: (d.focus_areas || []).map((x) => x.id),
|
||||
style_direction_ids: (d.style_directions || []).map((x) => x.id),
|
||||
target_group_ids: (d.target_groups || []).map((x) => x.id),
|
||||
status: d.status,
|
||||
version: d.version || '1.0'
|
||||
})
|
||||
|
|
@ -138,10 +98,7 @@ export default function MaturityModelsAdminPanel() {
|
|||
const payload = {
|
||||
name: newModel.name.trim(),
|
||||
level_count: parseInt(String(newModel.level_count), 10),
|
||||
status: newModel.status,
|
||||
focus_area_ids: newModel.focus_area_ids,
|
||||
style_direction_ids: newModel.style_direction_ids,
|
||||
target_group_ids: newModel.target_group_ids
|
||||
status: newModel.status
|
||||
}
|
||||
const created = await api.createMaturityModel(payload)
|
||||
await refreshModels()
|
||||
|
|
@ -149,9 +106,6 @@ export default function MaturityModelsAdminPanel() {
|
|||
setNewModel({
|
||||
name: '',
|
||||
level_count: 5,
|
||||
focus_area_ids: [],
|
||||
style_direction_ids: [],
|
||||
target_group_ids: [],
|
||||
status: 'draft'
|
||||
})
|
||||
} catch (e) {
|
||||
|
|
@ -169,9 +123,6 @@ export default function MaturityModelsAdminPanel() {
|
|||
await api.updateMaturityModel(selectedId, {
|
||||
name: meta.name,
|
||||
description: meta.description || null,
|
||||
focus_area_ids: meta.focus_area_ids,
|
||||
style_direction_ids: meta.style_direction_ids,
|
||||
target_group_ids: meta.target_group_ids,
|
||||
status: meta.status,
|
||||
version: meta.version
|
||||
})
|
||||
|
|
@ -321,58 +272,45 @@ export default function MaturityModelsAdminPanel() {
|
|||
return (
|
||||
<div className="admin-matrix-panel">
|
||||
<p className="admin-matrix-panel__intro muted">
|
||||
Reifegradmodelle: Kontext (Fokus / Stil / Zielgruppe), Stufen, Matrix-Zelltexte. Die Fähigkeiten selbst pflegen Sie im Tab „Katalog und Hierarchie“.
|
||||
Ablauf: Modell wählen → Stammdaten → Stufen definieren → Fähigkeiten zuordnen → Matrix-Zelltexte pflegen.
|
||||
Fähigkeiten legen Sie im Tab „Katalog und Hierarchie“ an.{' '}
|
||||
<strong>Zuordnung zu Fokusbereich / Stil / Zielgruppe</strong> ist hier vorerst deaktiviert; bestehende
|
||||
Einträge in der Datenbank bleiben unverändert, bis wir das Konzept (Überschneidungen) geklärt haben.
|
||||
</p>
|
||||
|
||||
{error ? (
|
||||
<div className="card" style={{ borderColor: 'var(--danger)', marginBottom: 16 }}>
|
||||
<div className="card admin-matrix-alert" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(240px, 320px) 1fr',
|
||||
gap: 20,
|
||||
alignItems: 'start'
|
||||
}}
|
||||
>
|
||||
<div className="card" style={{ padding: 16 }}>
|
||||
<h2 style={{ marginTop: 0, fontSize: '1.1rem' }}>Modelle</h2>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
||||
<div className="admin-matrix-layout">
|
||||
<aside className="admin-matrix-sidebar card">
|
||||
<h2 className="admin-matrix-sidebar__title">Reifegradmodelle</h2>
|
||||
<ul className="admin-matrix-model-list">
|
||||
{models.map((m) => (
|
||||
<li key={m.id} style={{ marginBottom: 8 }}>
|
||||
<li key={m.id}>
|
||||
<button
|
||||
type="button"
|
||||
className={selectedId === m.id ? 'btn btn-primary' : 'btn btn-secondary'}
|
||||
style={{ width: '100%', textAlign: 'left' }}
|
||||
className={
|
||||
'admin-matrix-model-btn' +
|
||||
(selectedId === m.id ? ' admin-matrix-model-btn--active' : '')
|
||||
}
|
||||
onClick={() => selectModel(m.id)}
|
||||
>
|
||||
<div style={{ fontWeight: 600 }}>{m.name}</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.85, lineHeight: 1.35 }}>
|
||||
{(m.focus_areas || []).length
|
||||
? `Fokus: ${m.focus_areas.map((f) => f.name).join(', ')}`
|
||||
: 'Fokus: alle'}
|
||||
<br />
|
||||
{(m.style_directions || []).length
|
||||
? `Stil: ${m.style_directions.map((s) => s.name).join(', ')}`
|
||||
: 'Stil: alle'}
|
||||
<br />
|
||||
{(m.target_groups || []).length
|
||||
? `Zielgr.: ${m.target_groups.map((t) => t.name).join(', ')}`
|
||||
: 'Zielgr.: alle'}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, opacity: 0.75 }}>{m.status} · {m.level_count} Stufen</div>
|
||||
<span className="admin-matrix-model-btn__name">{m.name}</span>
|
||||
<span className="admin-matrix-model-btn__meta muted">
|
||||
{m.status} · {m.level_count} Stufen
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<hr style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '16px 0' }} />
|
||||
<hr className="admin-matrix-divider" />
|
||||
|
||||
<h3 style={{ fontSize: '1rem' }}>Neues Modell</h3>
|
||||
<form onSubmit={handleCreate} style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<h3 className="admin-matrix-sidebar__subtitle">Neues Modell</h3>
|
||||
<form className="admin-matrix-new-form" onSubmit={handleCreate}>
|
||||
<label className="form-label">Name</label>
|
||||
<input
|
||||
className="form-input"
|
||||
|
|
@ -386,6 +324,7 @@ export default function MaturityModelsAdminPanel() {
|
|||
type="number"
|
||||
min={3}
|
||||
max={10}
|
||||
inputMode="numeric"
|
||||
value={newModel.level_count}
|
||||
onChange={(e) => setNewModel((s) => ({ ...s, level_count: e.target.value }))}
|
||||
/>
|
||||
|
|
@ -399,47 +338,34 @@ export default function MaturityModelsAdminPanel() {
|
|||
<option value="active">Aktiv</option>
|
||||
<option value="archived">Archiviert</option>
|
||||
</select>
|
||||
<MultiIdSelect
|
||||
label="Fokusbereiche (leer = alle)"
|
||||
options={focusAreas}
|
||||
valueIds={newModel.focus_area_ids}
|
||||
onChange={(ids) => setNewModel((s) => ({ ...s, focus_area_ids: ids }))}
|
||||
hint="Strg/Cmd gedrückt halten für Mehrfachauswahl."
|
||||
/>
|
||||
<MultiIdSelect
|
||||
label="Stilrichtungen (leer = alle)"
|
||||
options={styles}
|
||||
valueIds={newModel.style_direction_ids}
|
||||
onChange={(ids) => setNewModel((s) => ({ ...s, style_direction_ids: ids }))}
|
||||
hint="Strg/Cmd gedrückt halten für Mehrfachauswahl."
|
||||
/>
|
||||
<MultiIdSelect
|
||||
label="Zielgruppen (leer = alle)"
|
||||
options={targetGroups}
|
||||
valueIds={newModel.target_group_ids}
|
||||
onChange={(ids) => setNewModel((s) => ({ ...s, target_group_ids: ids }))}
|
||||
hint="Strg/Cmd gedrückt halten für Mehrfachauswahl."
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={saving}>
|
||||
Anlegen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div>
|
||||
{loading ? <div className="spinner" /> : null}
|
||||
<div className="admin-matrix-main">
|
||||
{loading ? (
|
||||
<div className="admin-matrix-loading" aria-busy="true">
|
||||
<div className="spinner" />
|
||||
<span className="muted">Lade Modell…</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!loading && !detail && (
|
||||
<div className="card" style={{ padding: 24, color: 'var(--text2)' }}>
|
||||
Modell links wählen oder neu anlegen.
|
||||
<div className="card admin-matrix-empty">
|
||||
Modell in der linken Spalte wählen oder neu anlegen.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && detail && meta && (
|
||||
<>
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||
<h2 style={{ marginTop: 0, fontSize: '1.1rem' }}>Kontext & Metadaten</h2>
|
||||
<div className="form-row" style={{ display: 'grid', gap: 12 }}>
|
||||
<section className="card admin-matrix-section">
|
||||
<div className="admin-matrix-section__head">
|
||||
<span className="admin-matrix-step">1</span>
|
||||
<h2 className="admin-matrix-section__title">Stammdaten</h2>
|
||||
</div>
|
||||
<div className="admin-matrix-meta-grid">
|
||||
<div>
|
||||
<label className="form-label">Name</label>
|
||||
<input
|
||||
|
|
@ -457,28 +383,7 @@ export default function MaturityModelsAdminPanel() {
|
|||
onChange={(e) => setMeta((m) => ({ ...m, description: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<MultiIdSelect
|
||||
label="Fokusbereiche (keine Auswahl = alle)"
|
||||
options={focusAreas}
|
||||
valueIds={meta.focus_area_ids}
|
||||
onChange={(ids) => setMeta((m) => ({ ...m, focus_area_ids: ids }))}
|
||||
hint="Strg/Cmd + Klick für mehrere. Alle abwählen = gilt in jedem Fokusbereich."
|
||||
/>
|
||||
<MultiIdSelect
|
||||
label="Stilrichtungen (keine Auswahl = alle)"
|
||||
options={styles}
|
||||
valueIds={meta.style_direction_ids}
|
||||
onChange={(ids) => setMeta((m) => ({ ...m, style_direction_ids: ids }))}
|
||||
hint="Strg/Cmd + Klick für mehrere."
|
||||
/>
|
||||
<MultiIdSelect
|
||||
label="Zielgruppen (keine Auswahl = alle)"
|
||||
options={targetGroups}
|
||||
valueIds={meta.target_group_ids}
|
||||
onChange={(ids) => setMeta((m) => ({ ...m, target_group_ids: ids }))}
|
||||
hint="Strg/Cmd + Klick für mehrere."
|
||||
/>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div className="admin-matrix-meta-grid__row2">
|
||||
<div>
|
||||
<label className="form-label">Status</label>
|
||||
<select
|
||||
|
|
@ -501,9 +406,26 @@ export default function MaturityModelsAdminPanel() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12, flexWrap: 'wrap' }}>
|
||||
{((detail.focus_areas || []).length > 0 ||
|
||||
(detail.style_directions || []).length > 0 ||
|
||||
(detail.target_groups || []).length > 0) && (
|
||||
<div className="admin-matrix-context-readonly muted">
|
||||
<strong>Hinweis (nur Anzeige):</strong> In der Datenbank sind noch Kontext-Zuordnungen
|
||||
hinterlegt (Fokus / Stil / Zielgruppe). Diese können Sie hier derzeit nicht ändern.
|
||||
{(detail.focus_areas || []).length ? (
|
||||
<div>Fokusbereiche: {(detail.focus_areas || []).map((f) => f.name).join(', ')}</div>
|
||||
) : null}
|
||||
{(detail.style_directions || []).length ? (
|
||||
<div>Stilrichtungen: {(detail.style_directions || []).map((s) => s.name).join(', ')}</div>
|
||||
) : null}
|
||||
{(detail.target_groups || []).length ? (
|
||||
<div>Zielgruppen: {(detail.target_groups || []).map((t) => t.name).join(', ')}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="admin-matrix-actions">
|
||||
<button type="button" className="btn btn-primary" onClick={handleSaveMeta} disabled={saving}>
|
||||
Metadaten speichern
|
||||
Stammdaten speichern
|
||||
</button>
|
||||
{isSuperadmin ? (
|
||||
<button type="button" className="btn btn-secondary" onClick={handleDeleteModel} disabled={saving}>
|
||||
|
|
@ -511,39 +433,43 @@ export default function MaturityModelsAdminPanel() {
|
|||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||
<h2 style={{ marginTop: 0, fontSize: '1.1rem' }}>Stufen (Bezeichnungen)</h2>
|
||||
<p style={{ fontSize: 14, color: 'var(--text2)', marginTop: 0 }}>
|
||||
Reihenfolge muss lückenlos 1…N sein. Stufenanzahl ändern passt die Tabelle an; danach speichern.
|
||||
<section className="card admin-matrix-section">
|
||||
<div className="admin-matrix-section__head">
|
||||
<span className="admin-matrix-step">2</span>
|
||||
<h2 className="admin-matrix-section__title">Stufen (Bezeichnungen)</h2>
|
||||
</div>
|
||||
<p className="admin-matrix-hint muted">
|
||||
Nummern 1…N lückenlos. Stufenanzahl ändert die Matrix; danach „Stufen speichern“.
|
||||
</p>
|
||||
<div style={{ marginBottom: 12, maxWidth: 200 }}>
|
||||
<div className="admin-matrix-level-count">
|
||||
<label className="form-label">Stufenanzahl (3–10)</label>
|
||||
<input
|
||||
className="form-input"
|
||||
type="number"
|
||||
min={3}
|
||||
max={10}
|
||||
inputMode="numeric"
|
||||
value={levelCount}
|
||||
onChange={(e) => onLevelCountChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
|
||||
<div className="admin-matrix-table-wrap">
|
||||
<table className="admin-matrix-table admin-matrix-table--levels">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', padding: 8 }}>Nr.</th>
|
||||
<th style={{ textAlign: 'left', padding: 8 }}>Name</th>
|
||||
<th style={{ textAlign: 'left', padding: 8 }}>Beschreibung</th>
|
||||
<th style={{ textAlign: 'left', padding: 8 }}>Sort</th>
|
||||
<th>Nr.</th>
|
||||
<th>Name</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Sort</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{levelsForm.map((row, idx) => (
|
||||
<tr key={row.level_number} style={{ borderTop: '1px solid var(--border)' }}>
|
||||
<td style={{ padding: 8 }}>{row.level_number}</td>
|
||||
<td style={{ padding: 8 }}>
|
||||
<tr key={row.level_number}>
|
||||
<td>{row.level_number}</td>
|
||||
<td>
|
||||
<input
|
||||
className="form-input"
|
||||
value={row.name}
|
||||
|
|
@ -554,7 +480,7 @@ export default function MaturityModelsAdminPanel() {
|
|||
}}
|
||||
/>
|
||||
</td>
|
||||
<td style={{ padding: 8 }}>
|
||||
<td>
|
||||
<input
|
||||
className="form-input"
|
||||
value={row.description}
|
||||
|
|
@ -565,10 +491,11 @@ export default function MaturityModelsAdminPanel() {
|
|||
}}
|
||||
/>
|
||||
</td>
|
||||
<td style={{ padding: 8, width: 72 }}>
|
||||
<td className="admin-matrix-table__narrow">
|
||||
<input
|
||||
className="form-input"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
value={row.sort_order}
|
||||
onChange={(e) => {
|
||||
const next = [...levelsForm]
|
||||
|
|
@ -584,19 +511,21 @@ export default function MaturityModelsAdminPanel() {
|
|||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ marginTop: 12 }}
|
||||
className="btn btn-secondary admin-matrix-actions--mt"
|
||||
onClick={handleSaveLevels}
|
||||
disabled={saving}
|
||||
>
|
||||
Stufen speichern
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||
<h2 style={{ marginTop: 0, fontSize: '1.1rem' }}>Fähigkeiten im Modell</h2>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||||
<div style={{ flex: '1 1 220px' }}>
|
||||
<section className="card admin-matrix-section">
|
||||
<div className="admin-matrix-section__head">
|
||||
<span className="admin-matrix-step">3</span>
|
||||
<h2 className="admin-matrix-section__title">Fähigkeiten im Modell</h2>
|
||||
</div>
|
||||
<div className="admin-matrix-skill-add">
|
||||
<div className="admin-matrix-skill-add__select">
|
||||
<label className="form-label">Fähigkeit hinzufügen</label>
|
||||
<select
|
||||
className="form-input"
|
||||
|
|
@ -609,27 +538,30 @@ export default function MaturityModelsAdminPanel() {
|
|||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" className="btn btn-primary" onClick={handleAddSkill} disabled={saving || !skillToAdd}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary admin-matrix-skill-add__btn"
|
||||
onClick={handleAddSkill}
|
||||
disabled={saving || !skillToAdd}
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
<ul style={{ marginTop: 12, paddingLeft: 18 }}>
|
||||
<ul className="admin-matrix-skill-list">
|
||||
{(detail.model_skills || []).map((ms) => (
|
||||
<li key={ms.skill_id} style={{ marginBottom: 10 }}>
|
||||
<div>
|
||||
<strong>{ms.skill_name}</strong>
|
||||
{' '}
|
||||
<li key={ms.skill_id} className="admin-matrix-skill-list__item">
|
||||
<div className="admin-matrix-skill-list__row">
|
||||
<strong className="admin-matrix-skill-list__name">{ms.skill_name}</strong>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ padding: '2px 8px', fontSize: 12 }}
|
||||
className="btn btn-secondary btn-small"
|
||||
onClick={() => handleRemoveSkill(ms.skill_id)}
|
||||
>
|
||||
entfernen
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
{(ms.skill_main_category_name || ms.skill_subcategory_name) ? (
|
||||
<div style={{ fontSize: 12, color: 'var(--text2)', marginTop: 2 }}>
|
||||
<div className="admin-matrix-skill-list__path muted">
|
||||
{ms.skill_main_category_name}
|
||||
{ms.skill_subcategory_name ? ` › ${ms.skill_subcategory_name}` : ''}
|
||||
</div>
|
||||
|
|
@ -637,26 +569,23 @@ export default function MaturityModelsAdminPanel() {
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="card" style={{ padding: 16 }}>
|
||||
<h2 style={{ marginTop: 0, fontSize: '1.1rem' }}>Matrix (Zielbild je Stufe)</h2>
|
||||
<p style={{ fontSize: 14, color: 'var(--text2)', marginTop: 0 }}>
|
||||
Leere Zellen werden beim Speichern aus der Datenbank entfernt. Beobachtungskriterien optional in
|
||||
zweiter Zeile (nach Speichern mit Beschreibung).
|
||||
<section className="card admin-matrix-section">
|
||||
<div className="admin-matrix-section__head">
|
||||
<span className="admin-matrix-step">4</span>
|
||||
<h2 className="admin-matrix-section__title">Matrix (Zielbild je Stufe)</h2>
|
||||
</div>
|
||||
<p className="admin-matrix-hint muted">
|
||||
Leere Zellen werden beim Speichern entfernt. Unten: Beobachtungskriterien (optional).
|
||||
</p>
|
||||
<div style={{ overflow: 'auto', maxHeight: '70vh' }}>
|
||||
<table style={{ borderCollapse: 'collapse', fontSize: 13, minWidth: 600 }}>
|
||||
<div className="admin-matrix-matrix-scroll">
|
||||
<table className="admin-matrix-table admin-matrix-table--matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ padding: 8, border: '1px solid var(--border)', position: 'sticky', left: 0, background: 'var(--surface)' }}>
|
||||
Fähigkeit
|
||||
</th>
|
||||
<th className="admin-matrix-matrix__corner">Fähigkeit</th>
|
||||
{(detail.levels || []).map((l) => (
|
||||
<th
|
||||
key={l.level_number}
|
||||
style={{ padding: 8, border: '1px solid var(--border)', minWidth: 160, background: 'var(--surface2)' }}
|
||||
>
|
||||
<th key={l.level_number} className="admin-matrix-matrix__level-head">
|
||||
{l.level_number}. {l.name}
|
||||
</th>
|
||||
))}
|
||||
|
|
@ -665,21 +594,10 @@ export default function MaturityModelsAdminPanel() {
|
|||
<tbody>
|
||||
{(detail.model_skills || []).map((ms) => (
|
||||
<tr key={ms.skill_id}>
|
||||
<td
|
||||
style={{
|
||||
padding: 8,
|
||||
border: '1px solid var(--border)',
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
background: 'var(--surface)',
|
||||
fontWeight: 600,
|
||||
maxWidth: 220,
|
||||
verticalAlign: 'top'
|
||||
}}
|
||||
>
|
||||
<td className="admin-matrix-matrix__skill-cell">
|
||||
<div>{ms.skill_name}</div>
|
||||
{(ms.skill_main_category_name || ms.skill_subcategory_name) ? (
|
||||
<div style={{ fontSize: 11, fontWeight: 400, color: 'var(--text2)', marginTop: 4, lineHeight: 1.3 }}>
|
||||
<div className="admin-matrix-matrix__skill-path muted">
|
||||
{ms.skill_main_category_name || '—'}
|
||||
{ms.skill_subcategory_name ? ` › ${ms.skill_subcategory_name}` : ''}
|
||||
</div>
|
||||
|
|
@ -689,22 +607,20 @@ export default function MaturityModelsAdminPanel() {
|
|||
const key = `${ms.skill_id}-${l.level_number}`
|
||||
const d = cellDraft[key] || { description: '', observable_criteria: '' }
|
||||
return (
|
||||
<td key={l.level_number} style={{ padding: 6, border: '1px solid var(--border)', verticalAlign: 'top' }}>
|
||||
<td key={l.level_number} className="admin-matrix-matrix__cell">
|
||||
<textarea
|
||||
className="form-input"
|
||||
className="form-input admin-matrix-matrix__ta"
|
||||
rows={3}
|
||||
placeholder="Zielbild / Erwartung"
|
||||
value={d.description}
|
||||
onChange={(e) => setCell(ms.skill_id, l.level_number, 'description', e.target.value)}
|
||||
style={{ fontSize: 12, width: '100%', minWidth: 140 }}
|
||||
/>
|
||||
<textarea
|
||||
className="form-input"
|
||||
className="form-input admin-matrix-matrix__ta admin-matrix-matrix__ta--criteria"
|
||||
rows={2}
|
||||
placeholder="Beobachtungskriterien (optional)"
|
||||
value={d.observable_criteria}
|
||||
onChange={(e) => setCell(ms.skill_id, l.level_number, 'observable_criteria', e.target.value)}
|
||||
style={{ fontSize: 11, width: '100%', minWidth: 140, marginTop: 4 }}
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
|
|
@ -716,14 +632,13 @@ export default function MaturityModelsAdminPanel() {
|
|||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
style={{ marginTop: 12 }}
|
||||
className="btn btn-primary admin-matrix-actions--mt"
|
||||
onClick={handleSaveMatrix}
|
||||
disabled={saving || !(detail.model_skills || []).length}
|
||||
>
|
||||
Matrix speichern
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,17 +9,29 @@ function bySortThenName(a, b) {
|
|||
return String(a.name || '').localeCompare(String(b.name || ''), 'de')
|
||||
}
|
||||
|
||||
async function swapNeighborSort(list, index, delta, updateId) {
|
||||
const sorted = [...list].sort(bySortThenName)
|
||||
const i = sorted.findIndex((x) => x.id === list[index]?.id)
|
||||
/** 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]
|
||||
const oa = a.sort_order != null ? Number(a.sort_order) : (i + 1) * 10
|
||||
const ob = b.sort_order != null ? Number(b.sort_order) : (j + 1) * 10
|
||||
await updateId(a.id, { sort_order: ob })
|
||||
await updateId(b.id, { sort_order: oa })
|
||||
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() {
|
||||
|
|
@ -66,6 +78,9 @@ export default function SkillsCatalogAdmin() {
|
|||
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([])
|
||||
|
|
@ -118,76 +133,59 @@ export default function SkillsCatalogAdmin() {
|
|||
.sort(bySortThenName)
|
||||
}, [catalog, selectedCategoryId])
|
||||
|
||||
const selectedMain = useMemo(
|
||||
() => mains.find((m) => m.id === selectedMainId) || null,
|
||||
[mains, selectedMainId]
|
||||
)
|
||||
const selectedCategory = useMemo(
|
||||
() => categories.find((c) => c.id === selectedCategoryId) || null,
|
||||
[categories, selectedCategoryId]
|
||||
)
|
||||
const selectedSkill = useMemo(
|
||||
() => catalog.find((s) => s.id === selectedSkillId) || null,
|
||||
[catalog, selectedSkillId]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedMain) {
|
||||
setMainForm({ name: '', slug: '', description: '', sort_order: '' })
|
||||
return
|
||||
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: selectedMain.name || '',
|
||||
slug: selectedMain.slug || '',
|
||||
description: selectedMain.description || '',
|
||||
sort_order: selectedMain.sort_order ?? ''
|
||||
name: m.name || '',
|
||||
slug: m.slug || '',
|
||||
description: m.description || '',
|
||||
sort_order: m.sort_order ?? ''
|
||||
})
|
||||
}, [selectedMain])
|
||||
setEditDialog({ type: 'main', id: m.id })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedCategory) {
|
||||
setCategoryForm({
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
main_category_id: '',
|
||||
sort_order: ''
|
||||
})
|
||||
return
|
||||
}
|
||||
function openEditCategory(c) {
|
||||
setCategoryForm({
|
||||
name: selectedCategory.name || '',
|
||||
slug: selectedCategory.slug || '',
|
||||
description: selectedCategory.description || '',
|
||||
main_category_id: selectedCategory.main_category_id ?? '',
|
||||
sort_order: selectedCategory.sort_order ?? ''
|
||||
name: c.name || '',
|
||||
slug: c.slug || '',
|
||||
description: c.description || '',
|
||||
main_category_id: c.main_category_id ?? '',
|
||||
sort_order: c.sort_order ?? ''
|
||||
})
|
||||
}, [selectedCategory])
|
||||
setEditDialog({ type: 'category', id: c.id })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSkill) {
|
||||
setSkillForm({
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
keywords: '',
|
||||
status: 'active',
|
||||
sort_order: '',
|
||||
category_id: ''
|
||||
})
|
||||
return
|
||||
}
|
||||
function openEditSkill(s) {
|
||||
setSkillForm({
|
||||
name: selectedSkill.name || '',
|
||||
description: selectedSkill.description || '',
|
||||
category: selectedSkill.category || '',
|
||||
keywords: selectedSkill.keywords || '',
|
||||
status: selectedSkill.status || 'active',
|
||||
sort_order: selectedSkill.sort_order ?? '',
|
||||
category_id: selectedSkill.category_id ?? ''
|
||||
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 ?? ''
|
||||
})
|
||||
}, [selectedSkill])
|
||||
setEditDialog({ type: 'skill', id: s.id })
|
||||
}
|
||||
|
||||
function closeEditDialog() {
|
||||
setEditDialog(null)
|
||||
}
|
||||
|
||||
/** @returns {Promise<boolean>} true bei Erfolg */
|
||||
async function run(op) {
|
||||
setBusy(true)
|
||||
setMessage('')
|
||||
|
|
@ -196,8 +194,10 @@ export default function SkillsCatalogAdmin() {
|
|||
await bootstrap()
|
||||
if (selectedMainId) await refreshCategories(selectedMainId)
|
||||
setMessage('Gespeichert.')
|
||||
return true
|
||||
} catch (e) {
|
||||
setError(e.message || String(e))
|
||||
return false
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
|
|
@ -214,37 +214,34 @@ export default function SkillsCatalogAdmin() {
|
|||
setSelectedSkillId(null)
|
||||
}
|
||||
|
||||
async function handleSwapMain(row, delta) {
|
||||
const idx = sortedMains.findIndex((m) => m.id === row.id)
|
||||
async function handleSwapMain(rowId, delta) {
|
||||
await run(async () => {
|
||||
await swapNeighborSort(sortedMains, idx, delta, (id, data) =>
|
||||
await swapNeighborSortById(sortedMains, rowId, delta, (id, data) =>
|
||||
api.updateSkillMainCategory(id, data)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSwapCategory(row, delta) {
|
||||
const idx = sortedCategories.findIndex((c) => c.id === row.id)
|
||||
async function handleSwapCategory(rowId, delta) {
|
||||
await run(async () => {
|
||||
await swapNeighborSort(sortedCategories, idx, delta, (id, data) =>
|
||||
await swapNeighborSortById(sortedCategories, rowId, delta, (id, data) =>
|
||||
api.updateSkillCategory(id, data)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSwapSkill(row, delta) {
|
||||
async function handleSwapSkill(rowId, delta) {
|
||||
const sorted = [...skillsInCategory].sort(bySortThenName)
|
||||
const idx = sorted.findIndex((s) => s.id === row.id)
|
||||
await run(async () => {
|
||||
await swapNeighborSort(sorted, idx, delta, (id, data) => api.updateSkill(id, data))
|
||||
await swapNeighborSortById(sorted, rowId, delta, (id, data) => api.updateSkill(id, data))
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSaveMain(e) {
|
||||
e.preventDefault()
|
||||
if (!selectedMainId) return
|
||||
await run(async () => {
|
||||
await api.updateSkillMainCategory(selectedMainId, {
|
||||
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,
|
||||
|
|
@ -254,17 +251,18 @@ export default function SkillsCatalogAdmin() {
|
|||
: Number(mainForm.sort_order)
|
||||
})
|
||||
})
|
||||
if (ok) closeEditDialog()
|
||||
}
|
||||
|
||||
async function handleSaveCategory(e) {
|
||||
e.preventDefault()
|
||||
if (!selectedCategoryId) return
|
||||
if (!editDialog || editDialog.type !== 'category') return
|
||||
const mid =
|
||||
categoryForm.main_category_id === '' || categoryForm.main_category_id == null
|
||||
? null
|
||||
: Number(categoryForm.main_category_id)
|
||||
await run(async () => {
|
||||
await api.updateSkillCategory(selectedCategoryId, {
|
||||
const ok = await run(async () => {
|
||||
await api.updateSkillCategory(editDialog.id, {
|
||||
name: categoryForm.name.trim(),
|
||||
slug: (categoryForm.slug || '').trim() || undefined,
|
||||
description: categoryForm.description || null,
|
||||
|
|
@ -275,24 +273,28 @@ export default function SkillsCatalogAdmin() {
|
|||
: Number(categoryForm.sort_order)
|
||||
})
|
||||
})
|
||||
if (mid != null && mid !== selectedMainId) {
|
||||
setSelectedMainId(mid)
|
||||
setSelectedSkillId(null)
|
||||
if (ok) {
|
||||
if (mid != null && mid !== selectedMainId) {
|
||||
setSelectedMainId(mid)
|
||||
setSelectedSkillId(null)
|
||||
}
|
||||
closeEditDialog()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveSkill(e) {
|
||||
e.preventDefault()
|
||||
if (!selectedSkillId) return
|
||||
if (!editDialog || editDialog.type !== 'skill') return
|
||||
let cid =
|
||||
skillForm.category_id === '' || skillForm.category_id == null
|
||||
? null
|
||||
: Number(skillForm.category_id)
|
||||
if (cid == null && selectedSkill?.category_id) {
|
||||
cid = selectedSkill.category_id
|
||||
const skillRow = catalog.find((s) => s.id === editDialog.id)
|
||||
if (cid == null && skillRow?.category_id) {
|
||||
cid = skillRow.category_id
|
||||
}
|
||||
await run(async () => {
|
||||
await api.updateSkill(selectedSkillId, {
|
||||
const ok = await run(async () => {
|
||||
await api.updateSkill(editDialog.id, {
|
||||
name: skillForm.name.trim(),
|
||||
description: skillForm.description || null,
|
||||
category: skillForm.category || null,
|
||||
|
|
@ -305,12 +307,15 @@ export default function SkillsCatalogAdmin() {
|
|||
category_id: cid
|
||||
})
|
||||
})
|
||||
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)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,43 +363,45 @@ export default function SkillsCatalogAdmin() {
|
|||
}
|
||||
|
||||
async function handleDeleteMain() {
|
||||
if (!selectedMainId || !isSuperadmin) return
|
||||
if (!editDialog || editDialog.type !== 'main' || !isSuperadmin) return
|
||||
if (!window.confirm('Hauptkategorie wirklich löschen?')) return
|
||||
await run(async () => {
|
||||
await api.deleteSkillMainCategory(selectedMainId)
|
||||
setSelectedMainId(null)
|
||||
setSelectedCategoryId(null)
|
||||
setSelectedSkillId(null)
|
||||
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 (!selectedCategoryId || !isSuperadmin) return
|
||||
if (!editDialog || editDialog.type !== 'category' || !isSuperadmin) return
|
||||
if (!window.confirm('Kategorie wirklich löschen?')) return
|
||||
await run(async () => {
|
||||
await api.deleteSkillCategory(selectedCategoryId)
|
||||
setSelectedCategoryId(null)
|
||||
setSelectedSkillId(null)
|
||||
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 (!selectedSkillId || !isSuperadmin) return
|
||||
if (!editDialog || editDialog.type !== 'skill' || !isSuperadmin) return
|
||||
if (!window.confirm('Fähigkeit wirklich löschen?')) return
|
||||
await run(async () => {
|
||||
await api.deleteSkill(selectedSkillId)
|
||||
setSelectedSkillId(null)
|
||||
const id = editDialog.id
|
||||
const ok = await run(async () => {
|
||||
await api.deleteSkill(id)
|
||||
if (selectedSkillId === id) setSelectedSkillId(null)
|
||||
})
|
||||
if (ok) closeEditDialog()
|
||||
}
|
||||
|
||||
const detailMode = selectedSkillId
|
||||
? 'skill'
|
||||
: selectedCategoryId
|
||||
? 'category'
|
||||
: selectedMainId
|
||||
? 'main'
|
||||
: 'none'
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="skills-catalog-admin">
|
||||
|
|
@ -407,8 +414,8 @@ export default function SkillsCatalogAdmin() {
|
|||
<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 den Pfeiltasten; Zuordnungen und Texte im
|
||||
Bereich „Bearbeiten“.
|
||||
<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 ? (
|
||||
|
|
@ -454,7 +461,11 @@ export default function SkillsCatalogAdmin() {
|
|||
className="btn btn-secondary btn-tiny"
|
||||
disabled={busy}
|
||||
title="Nach oben"
|
||||
onClick={() => handleSwapMain(m, -1)}
|
||||
aria-label="Nach oben sortieren"
|
||||
onClick={(e) => {
|
||||
stop(e)
|
||||
handleSwapMain(m.id, -1)
|
||||
}}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
|
|
@ -463,10 +474,27 @@ export default function SkillsCatalogAdmin() {
|
|||
className="btn btn-secondary btn-tiny"
|
||||
disabled={busy}
|
||||
title="Nach unten"
|
||||
onClick={() => handleSwapMain(m, 1)}
|
||||
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>
|
||||
|
|
@ -515,7 +543,11 @@ export default function SkillsCatalogAdmin() {
|
|||
className="btn btn-secondary btn-tiny"
|
||||
disabled={busy}
|
||||
title="Nach oben"
|
||||
onClick={() => handleSwapCategory(c, -1)}
|
||||
aria-label="Nach oben sortieren"
|
||||
onClick={(e) => {
|
||||
stop(e)
|
||||
handleSwapCategory(c.id, -1)
|
||||
}}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
|
|
@ -524,10 +556,27 @@ export default function SkillsCatalogAdmin() {
|
|||
className="btn btn-secondary btn-tiny"
|
||||
disabled={busy}
|
||||
title="Nach unten"
|
||||
onClick={() => handleSwapCategory(c, 1)}
|
||||
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>
|
||||
|
|
@ -581,7 +630,11 @@ export default function SkillsCatalogAdmin() {
|
|||
className="btn btn-secondary btn-tiny"
|
||||
disabled={busy}
|
||||
title="Nach oben"
|
||||
onClick={() => handleSwapSkill(s, -1)}
|
||||
aria-label="Nach oben sortieren"
|
||||
onClick={(e) => {
|
||||
stop(e)
|
||||
handleSwapSkill(s.id, -1)
|
||||
}}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
|
|
@ -590,10 +643,27 @@ export default function SkillsCatalogAdmin() {
|
|||
className="btn btn-secondary btn-tiny"
|
||||
disabled={busy}
|
||||
title="Nach unten"
|
||||
onClick={() => handleSwapSkill(s, 1)}
|
||||
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>
|
||||
|
|
@ -617,222 +687,279 @@ export default function SkillsCatalogAdmin() {
|
|||
</section>
|
||||
</div>
|
||||
|
||||
<section className="skills-catalog-detail" aria-label="Bearbeiten">
|
||||
<h3 className="skills-catalog-detail__title">Bearbeiten</h3>
|
||||
{detailMode === 'none' ? (
|
||||
<p className="muted">
|
||||
Wählen Sie eine Hauptkategorie, Kategorie oder Fähigkeit in den Spalten oben.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{detailMode === 'main' ? (
|
||||
<form className="skills-catalog-form" onSubmit={handleSaveMain}>
|
||||
<h4 className="skills-catalog-form__heading">Hauptkategorie</h4>
|
||||
<label className="form-label">Name</label>
|
||||
<input
|
||||
className="form-input"
|
||||
value={mainForm.name}
|
||||
onChange={(e) => setMainForm((f) => ({ ...f, name: e.target.value }))}
|
||||
disabled={busy}
|
||||
/>
|
||||
<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"
|
||||
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
|
||||
{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>
|
||||
{isSuperadmin ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busy}
|
||||
onClick={handleDeleteMain}
|
||||
>
|
||||
Löschen
|
||||
</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">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>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
{detailMode === 'category' ? (
|
||||
<form className="skills-catalog-form" onSubmit={handleSaveCategory}>
|
||||
<h4 className="skills-catalog-form__heading">Kategorie</h4>
|
||||
<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"
|
||||
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>
|
||||
{isSuperadmin ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busy}
|
||||
onClick={handleDeleteCategory}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
{detailMode === 'skill' ? (
|
||||
<form className="skills-catalog-form" onSubmit={handleSaveSkill}>
|
||||
<h4 className="skills-catalog-form__heading">Fähigkeit</h4>
|
||||
<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">Sortierung (optional)</label>
|
||||
<input
|
||||
className="form-input"
|
||||
type="number"
|
||||
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>
|
||||
{isSuperadmin ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busy}
|
||||
onClick={handleDeleteSkill}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user