110 lines
3.3 KiB
React
110 lines
3.3 KiB
React
|
|
import React, { useState } from 'react'
|
|||
|
|
import { newCatalogRuleKey } from '../constants/exerciseListFilters'
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Kompakte +/- Regeln für Katalogwerte (numerische IDs oder Slugs).
|
|||
|
|
* Chips oben, schmales Dropdown, Schalter nur „+“ und „−“.
|
|||
|
|
*/
|
|||
|
|
export default function CatalogRulePicker({
|
|||
|
|
label,
|
|||
|
|
hint,
|
|||
|
|
options = [],
|
|||
|
|
rules = [],
|
|||
|
|
rulesFieldName,
|
|||
|
|
disabled = false,
|
|||
|
|
placeholder = 'Auswählen …',
|
|||
|
|
idKind = 'numeric',
|
|||
|
|
onPatch,
|
|||
|
|
}) {
|
|||
|
|
const [pendingId, setPendingId] = useState('')
|
|||
|
|
|
|||
|
|
const labelFor = (id) => options.find((o) => String(o.id) === String(id))?.label ?? id
|
|||
|
|
|
|||
|
|
const addRule = (mode) => {
|
|||
|
|
const raw = String(pendingId || '').trim()
|
|||
|
|
if (!raw || disabled) return
|
|||
|
|
if (idKind === 'numeric') {
|
|||
|
|
const n = Number(raw)
|
|||
|
|
if (!Number.isFinite(n) || n < 1) return
|
|||
|
|
}
|
|||
|
|
const dup = (rules || []).some((r) => String(r.id) === raw && r.mode === mode)
|
|||
|
|
if (dup) return
|
|||
|
|
onPatch({
|
|||
|
|
[rulesFieldName]: [
|
|||
|
|
...(rules || []),
|
|||
|
|
{ key: newCatalogRuleKey(rulesFieldName), id: raw, mode },
|
|||
|
|
],
|
|||
|
|
})
|
|||
|
|
setPendingId('')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removeRule = (key) => {
|
|||
|
|
onPatch({
|
|||
|
|
[rulesFieldName]: (rules || []).filter((r) => r.key !== key),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className={`catalog-rule-picker${disabled ? ' catalog-rule-picker--disabled' : ''}`}>
|
|||
|
|
<label className="form-label catalog-rule-picker__label">{label}</label>
|
|||
|
|
{hint ? (
|
|||
|
|
<p className="muted catalog-rule-picker__hint" style={{ fontSize: '11px', marginTop: '2px', marginBottom: '6px' }}>
|
|||
|
|
{hint}
|
|||
|
|
</p>
|
|||
|
|
) : null}
|
|||
|
|
<div className="catalog-rule-picker__chips" aria-live="polite">
|
|||
|
|
{(rules || []).map((r) => (
|
|||
|
|
<span key={r.key} className="catalog-rule-chip">
|
|||
|
|
<span className={`catalog-rule-chip__sign catalog-rule-chip__sign--${r.mode}`}>
|
|||
|
|
{r.mode === 'forbid' ? '−' : '+'}
|
|||
|
|
</span>
|
|||
|
|
<span className="catalog-rule-chip__text">{labelFor(r.id)}</span>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="catalog-rule-chip__x"
|
|||
|
|
aria-label={`${label}: Regel entfernen`}
|
|||
|
|
onClick={() => removeRule(r.key)}
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
<div className="catalog-rule-picker__row">
|
|||
|
|
<select
|
|||
|
|
className="form-input catalog-rule-picker__select"
|
|||
|
|
value={pendingId}
|
|||
|
|
disabled={disabled}
|
|||
|
|
onChange={(e) => setPendingId(e.target.value)}
|
|||
|
|
aria-label={label}
|
|||
|
|
>
|
|||
|
|
<option value="">{placeholder}</option>
|
|||
|
|
{options.map((o) => (
|
|||
|
|
<option key={o.id} value={String(o.id)}>
|
|||
|
|
{o.label || o.id}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="btn btn-secondary btn-small catalog-rule-picker__sign-btn"
|
|||
|
|
disabled={disabled || !pendingId}
|
|||
|
|
title="Muss zutreffen"
|
|||
|
|
onClick={() => addRule('require')}
|
|||
|
|
>
|
|||
|
|
+
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="btn btn-secondary btn-small catalog-rule-picker__sign-btn"
|
|||
|
|
disabled={disabled || !pendingId}
|
|||
|
|
title="Darf nicht zutreffen"
|
|||
|
|
onClick={() => addRule('forbid')}
|
|||
|
|
>
|
|||
|
|
−
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|