- 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.
301 lines
10 KiB
JavaScript
301 lines
10 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). 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>
|
||
)
|
||
}
|