shinkan-jinkendo/frontend/src/components/admin/MaturityModelBindingsAdmin.jsx
Lars 863535aa26
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 1m55s
feat: update version to 0.7.5 and enhance maturity model binding logic
- Incremented application version to 0.7.5 and updated maturity model version to 1.3.1.
- Improved binding logic to ensure that only relevant entries are considered based on the presence of context bindings.
- Updated documentation to clarify the behavior of binding resolution and its impact on legacy model resolution.
- Documented changes in the changelog for version 0.7.5, detailing the new binding resolution rules.
2026-04-27 13:05:18 +02:00

301 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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). Eine Zeile gilt nur, wenn alle von ihr
gesetzten Dimensionen mit der Anfrage übereinstimmen; fehlende Spalten in der Zeile bedeuten alle
(z.B. Fokus+Trainingsstil gilt unter jeder Stilrichtung, aber nur für genau diesen Trainingsstil).
Sobald für einen Fokusbereich mindestens eine Zuordnung in dieser Tabelle existiert, wird{' '}
<strong>kein</strong> älteres Legacy-Resolve (nur M:N am Modell) mehr verwendet fehlende Treffer
liefern dann keine Matrix. Beim Aufruf von{' '}
<code className="admin-bindings__code">/maturity-models/resolve</code> werden alle passenden Zeilen nach
Spezifität sortiert (weniger zuerst) gemerged; 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>
)
}