- 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.
292 lines
9.5 KiB
JavaScript
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>
|
|
)
|
|
}
|