146 lines
5.2 KiB
React
146 lines
5.2 KiB
React
|
|
import React, { useMemo, useState } from 'react'
|
|||
|
|
|
|||
|
|
function newRuleKey() {
|
|||
|
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()
|
|||
|
|
return `fr-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Fokusbereiche mit „+“ (muss haben) und „−“ (darf nicht haben); optional nur Übungen ohne Fokus-Zuordnung.
|
|||
|
|
*/
|
|||
|
|
export default function ExerciseFocusRulePicker({
|
|||
|
|
focusOptions,
|
|||
|
|
focusRules,
|
|||
|
|
focusOnlyWithout,
|
|||
|
|
excludeWithoutFocus,
|
|||
|
|
legacyFocusAreaIds = [],
|
|||
|
|
onPatch,
|
|||
|
|
}) {
|
|||
|
|
const [pendingId, setPendingId] = useState('')
|
|||
|
|
|
|||
|
|
const legacyWarning = useMemo(
|
|||
|
|
() => Array.isArray(legacyFocusAreaIds) && legacyFocusAreaIds.length > 0,
|
|||
|
|
[legacyFocusAreaIds]
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const addRule = (mode) => {
|
|||
|
|
const id = String(pendingId || '').trim()
|
|||
|
|
if (!id || focusOnlyWithout) return
|
|||
|
|
const dup = (focusRules || []).some(
|
|||
|
|
(r) => String(r.focus_area_id) === id && r.mode === mode
|
|||
|
|
)
|
|||
|
|
if (dup) return
|
|||
|
|
onPatch({
|
|||
|
|
focus_rules: [...(focusRules || []), { key: newRuleKey(), focus_area_id: id, mode }],
|
|||
|
|
})
|
|||
|
|
setPendingId('')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removeRule = (key) => {
|
|||
|
|
onPatch({
|
|||
|
|
focus_rules: (focusRules || []).filter((r) => r.key !== key),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const setFocusOnly = (on) => {
|
|||
|
|
if (on) {
|
|||
|
|
onPatch({
|
|||
|
|
focus_only_without: true,
|
|||
|
|
exclude_without_focus: false,
|
|||
|
|
focus_rules: [],
|
|||
|
|
focus_area_ids: [],
|
|||
|
|
})
|
|||
|
|
setPendingId('')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
onPatch({ focus_only_without: false })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="exercise-focus-rule-picker">
|
|||
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', marginBottom: '10px' }}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={!!focusOnlyWithout}
|
|||
|
|
onChange={(e) => setFocusOnly(e.target.checked)}
|
|||
|
|
/>
|
|||
|
|
<span>Nur Übungen <strong>ohne</strong> Fokusbereich (keine Zuordnung)</span>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
{!focusOnlyWithout ? (
|
|||
|
|
<>
|
|||
|
|
{legacyWarning ? (
|
|||
|
|
<p className="muted" style={{ fontSize: '13px', marginTop: 0, marginBottom: '10px' }}>
|
|||
|
|
Es sind noch ältere Fokusfilter (ODER-Liste) aktiv — bitte über die Chips entfernen oder durch Regeln
|
|||
|
|
ersetzen.
|
|||
|
|
</p>
|
|||
|
|
) : null}
|
|||
|
|
<label className="form-label">Fokusbereiche (+ mit / − ohne)</label>
|
|||
|
|
<p className="muted" style={{ fontSize: '12px', marginTop: 0, marginBottom: '8px' }}>
|
|||
|
|
Mehrere „+“: alle müssen gesetzt sein (UND). Mehrere „−“: keiner davon darf gesetzt sein.
|
|||
|
|
Kombination z. B. „+ Karate“ und „− Fitness“ = hat Karate, aber nicht zusätzlich Fitness.
|
|||
|
|
</p>
|
|||
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '10px' }}>
|
|||
|
|
<select
|
|||
|
|
className="form-input"
|
|||
|
|
style={{ minWidth: '200px', flex: '1 1 180px' }}
|
|||
|
|
value={pendingId}
|
|||
|
|
onChange={(e) => setPendingId(e.target.value)}
|
|||
|
|
aria-label="Fokusbereich wählen"
|
|||
|
|
>
|
|||
|
|
<option value="">Fokusbereich wählen …</option>
|
|||
|
|
{focusOptions.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" disabled={!pendingId} onClick={() => addRule('require')}>
|
|||
|
|
+ Mit
|
|||
|
|
</button>
|
|||
|
|
<button type="button" className="btn btn-secondary btn-small" disabled={!pendingId} onClick={() => addRule('forbid')}>
|
|||
|
|
− Ohne
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
{(focusRules || []).length > 0 ? (
|
|||
|
|
<ul style={{ listStyle: 'none', padding: 0, margin: '0 0 12px 0' }}>
|
|||
|
|
{(focusRules || []).map((r) => {
|
|||
|
|
const opt = focusOptions.find((o) => String(o.id) === String(r.focus_area_id))
|
|||
|
|
const name = opt?.label || r.focus_area_id
|
|||
|
|
return (
|
|||
|
|
<li
|
|||
|
|
key={r.key}
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'space-between',
|
|||
|
|
gap: '8px',
|
|||
|
|
padding: '6px 10px',
|
|||
|
|
marginBottom: '6px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid var(--border)',
|
|||
|
|
background: 'var(--surface2)',
|
|||
|
|
fontSize: '13px',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<span>
|
|||
|
|
<strong>{r.mode === 'forbid' ? '−' : '+'}</strong> {name}
|
|||
|
|
</span>
|
|||
|
|
<button type="button" className="btn btn-secondary btn-small" onClick={() => removeRule(r.key)}>
|
|||
|
|
Entfernen
|
|||
|
|
</button>
|
|||
|
|
</li>
|
|||
|
|
)
|
|||
|
|
})}
|
|||
|
|
</ul>
|
|||
|
|
) : null}
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<p className="muted" style={{ fontSize: '13px', marginTop: 0 }}>
|
|||
|
|
Solange diese Option aktiv ist, sind Fokus-Regeln und die ODER-Fokusliste deaktiviert.
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|