shinkan-jinkendo/frontend/src/components/ExerciseFocusRulePicker.jsx

146 lines
5.2 KiB
React
Raw Normal View History

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>
)
}