shinkan-jinkendo/frontend/src/components/admin/MaturityModelBindingsAdmin.jsx
Lars f2c007cc68
Some checks failed
Deploy Development / deploy (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 1m59s
feat: update version to 0.7.4 and enhance maturity model functionality
- Incremented application version to 0.7.4 and updated database schema version to 20260427027.
- Enhanced maturity model context bindings to support new filtering options for training styles.
- Introduced new API endpoints for importing and exporting maturity model bundles.
- Updated frontend components to include a matrix view and improved admin UI for managing maturity models.
- Documented changes in the changelog for version 0.7.4, detailing new features and improvements.
2026-04-27 12:48:22 +02:00

296 lines
9.7 KiB
JavaScript

import React, { useCallback, useEffect, useState } from 'react'
import api from '../../utils/api'
const TIER_LABEL = {
focus: 'Nur Fokusbereich',
focus_style: 'Fokus + Stilrichtung',
focus_training: 'Fokus + Trainingsstil',
focus_style_type: 'Fokus + Stilrichtung + Trainingsstil'
}
function tierFromRow(row) {
const hs = row.style_direction_id != null
const ht = row.training_type_id != null
if (!hs && !ht) return 'focus'
if (hs && !ht) return 'focus_style'
if (!hs && ht) return 'focus_training'
return 'focus_style_type'
}
export default function MaturityModelBindingsAdmin() {
const [bindings, setBindings] = useState([])
const [models, setModels] = useState([])
const [focusAreas, setFocusAreas] = useState([])
const [styleDirections, setStyleDirections] = useState([])
const [trainingTypes, setTrainingTypes] = useState([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [tier, setTier] = useState('focus')
const [formModelId, setFormModelId] = useState('')
const [formFocusId, setFormFocusId] = useState('')
const [formStyleId, setFormStyleId] = useState('')
const [formTypeId, setFormTypeId] = useState('')
const load = useCallback(async () => {
setError('')
try {
const [b, m, fa, sd, tt] = await Promise.all([
api.listMaturityModelContextBindings(),
api.listMaturityModels({}),
api.listFocusAreas({}),
api.listStyleDirections({}),
api.listTrainingTypes({})
])
setBindings(b)
setModels(m)
setFocusAreas(fa)
setStyleDirections(sd)
setTrainingTypes(tt)
} catch (e) {
setError(e.message || String(e))
}
}, [])
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
await load()
if (!cancelled) setLoading(false)
})()
return () => {
cancelled = true
}
}, [load])
useEffect(() => {
if (tier === 'focus') {
setFormStyleId('')
setFormTypeId('')
} else if (tier === 'focus_style') {
setFormTypeId('')
} else if (tier === 'focus_training') {
setFormStyleId('')
}
}, [tier])
async function handleSubmit(e) {
e.preventDefault()
if (!formModelId || !formFocusId) {
setError('Modell und Fokusbereich sind Pflichtfelder.')
return
}
const payload = {
maturity_model_id: parseInt(formModelId, 10),
focus_area_id: parseInt(formFocusId, 10)
}
if (tier === 'focus_style' || tier === 'focus_style_type') {
if (!formStyleId) {
setError('Stilrichtung auswählen.')
return
}
payload.style_direction_id = parseInt(formStyleId, 10)
}
if (tier === 'focus_training' || tier === 'focus_style_type') {
if (!formTypeId) {
setError('Trainingsstil auswählen.')
return
}
payload.training_type_id = parseInt(formTypeId, 10)
}
setSaving(true)
setError('')
try {
await api.upsertMaturityModelContextBinding(payload)
await load()
} catch (err) {
setError(err.message || String(err))
} finally {
setSaving(false)
}
}
async function handleDelete(id) {
if (!window.confirm('Diese Zuordnung wirklich entfernen?')) return
setError('')
try {
await api.deleteMaturityModelContextBinding(id)
await load()
} catch (err) {
setError(err.message || String(err))
}
}
if (loading) {
return <p className="muted">Lade Kontext-Zuordnungen</p>
}
return (
<div className="admin-bindings">
<p className="admin-bindings__intro muted">
<strong>Zusammenführung:</strong> Zu einem Fokus können mehrere Zeilen existieren (nur Fokus, Fokus +
Stilrichtung, Fokus + Trainingsstil ohne Stil, oder alle drei). Beim Aufruf von{' '}
<code className="admin-bindings__code">/maturity-models/resolve</code> werden alle zur Anfrage passenden
Zeilen ermittelt, nach Spezifität sortiert (weniger zuerst) und zu einer Matrix verbunden: spezifischere
Zuordnungen überschreiben Zelltexte gleicher Fähigkeit und Stufe. Stufen (Spalten) stammen vom{' '}
<strong>ersten</strong> (am wenigsten spezifischen) Modell in dieser Kette.
</p>
{error ? (
<div className="skills-catalog-admin__error" role="alert">
{error}
</div>
) : null}
<section className="card admin-bindings__form-section">
<h2 className="admin-bindings__h2">Zuordnung anlegen oder ersetzen</h2>
<form className="admin-bindings__form" onSubmit={handleSubmit}>
<div>
<label className="form-label">Ebene</label>
<select
className="form-input"
value={tier}
onChange={(e) => setTier(e.target.value)}
disabled={saving}
>
<option value="focus">{TIER_LABEL.focus}</option>
<option value="focus_style">{TIER_LABEL.focus_style}</option>
<option value="focus_training">{TIER_LABEL.focus_training}</option>
<option value="focus_style_type">{TIER_LABEL.focus_style_type}</option>
</select>
</div>
<div>
<label className="form-label">Fokusbereich</label>
<select
className="form-input"
value={formFocusId}
onChange={(e) => setFormFocusId(e.target.value)}
required
disabled={saving}
>
<option value=""> wählen </option>
{focusAreas.map((f) => (
<option key={f.id} value={f.id}>
{f.name}
</option>
))}
</select>
</div>
{(tier === 'focus_style' || tier === 'focus_style_type') && (
<div>
<label className="form-label">Stilrichtung</label>
<select
className="form-input"
value={formStyleId}
onChange={(e) => setFormStyleId(e.target.value)}
required
disabled={saving}
>
<option value=""> wählen </option>
{styleDirections.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</select>
</div>
)}
{(tier === 'focus_training' || tier === 'focus_style_type') && (
<div>
<label className="form-label">Trainingsstil (z. B. Breitensport)</label>
<select
className="form-input"
value={formTypeId}
onChange={(e) => setFormTypeId(e.target.value)}
required
disabled={saving}
>
<option value=""> wählen </option>
{trainingTypes.map((t) => (
<option key={t.id} value={t.id}>
{t.name}
</option>
))}
</select>
</div>
)}
<div>
<label className="form-label">Reifegradmodell</label>
<select
className="form-input"
value={formModelId}
onChange={(e) => setFormModelId(e.target.value)}
required
disabled={saving}
>
<option value=""> wählen </option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name} ({m.status})
</option>
))}
</select>
</div>
<button type="submit" className="btn btn-primary" disabled={saving}>
Speichern
</button>
</form>
</section>
<section className="admin-bindings__table-section">
<h2 className="admin-bindings__h2">Aktive Zuordnungen</h2>
<div className="admin-bindings-table-wrap">
<table className="admin-bindings-table">
<thead>
<tr>
<th>Ebene</th>
<th>Fokus</th>
<th>Stilrichtung</th>
<th>Trainingsstil</th>
<th>Modell</th>
<th />
</tr>
</thead>
<tbody>
{bindings.length === 0 ? (
<tr>
<td colSpan={6} className="muted">
Noch keine Einträge. Sobald hier Zeilen existieren und ein passender Kontext an{' '}
<code>/maturity-models/resolve</code> übergeben wird, werden diese Zuordnungen statt der
reinen Legacy-Suche verwendet.
</td>
</tr>
) : (
bindings.map((row) => (
<tr key={row.id}>
<td>{TIER_LABEL[tierFromRow(row)]}</td>
<td>{row.focus_area_name}</td>
<td>{row.style_direction_name || '—'}</td>
<td>{row.training_type_name || '—'}</td>
<td>
{row.maturity_model_name}
<span className="muted"> ({row.model_status})</span>
</td>
<td>
<button
type="button"
className="btn btn-secondary btn-small"
onClick={() => handleDelete(row.id)}
>
Entfernen
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
)
}