shinkan-jinkendo/frontend/src/components/admin/MaturityModelBindingsAdmin.jsx
Lars 3397b2094d
Some checks failed
Deploy Development / deploy (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 1m55s
feat: update version to 0.7.3 and enhance maturity model context bindings
- Incremented application version to 0.7.3 and updated database schema version to 20260427026.
- Enhanced maturity model context bindings with new hierarchical resolution logic for focus areas, style directions, and training types.
- Added new API endpoints for managing maturity model context bindings.
- Updated frontend components to support the new context binding functionality and improved admin UI for better user experience.
- Documented changes in the changelog for version 0.7.3, including new features and improvements.
2026-04-27 12:35:48 +02:00

292 lines
9.5 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_style_type: 'Fokus + Stilrichtung + Trainingsstil'
}
function tierFromRow(row) {
if (row.style_direction_id == null && row.training_type_id == null) return 'focus'
if (row.training_type_id == null) return 'focus_style'
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('')
}
}, [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_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>Vererbung:</strong> Ein Modell auf Ebene Fokusbereich gilt als Basis für diesen Fokus. Gibt es
eine spezifischere Zeile (Fokus + Stilrichtung oder + Trainingsstil), werden bei der Auflösung der Matrix{' '}
<strong>dieselben Fähigkeiten</strong> mit den Texten aus dem spezifischeren Modell überschrieben.
Zusätzliche Fähigkeiten aus einem Overlay-Modell erscheinen als weitere Zeilen. Stufen (Spalten) kommen
immer vom <strong>Basis-Modell</strong> (Fokus-Ebene).
</p>
<p className="admin-bindings__intro muted">
API <code className="admin-bindings__code">GET /api/maturity-models/resolve</code> berücksichtigt{' '}
<code className="admin-bindings__code">training_type_id</code> (Trainingsstil ={' '}
<em>training_types</em>, z. B. Leistungssport) zusätzlich zu Fokus und Stilrichtung.
</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_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_style_type' && (
<div>
<label className="form-label">Trainingsstil (z. B. Leistungssport)</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. Legen Sie mindestens eine Fokus-Basis-Zeile an, damit{' '}
<code>/maturity-models/resolve</code> die neue Logik nutzt.
</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>
)
}