pre-Prod Alpha #18
|
|
@ -21,12 +21,17 @@ const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null)
|
||||||
|
|
||||||
const INITIAL_FILTERS = { ...INITIAL_EXERCISE_LIST_FILTERS }
|
const INITIAL_FILTERS = { ...INITIAL_EXERCISE_LIST_FILTERS }
|
||||||
|
|
||||||
|
/** Stub-Ziel für API-Validator (mind. Ziel oder Durchführung); Nutzer ergänzt Details in der Übungsbearbeitung. */
|
||||||
|
const QUICK_CREATE_GOAL_PLACEHOLDER =
|
||||||
|
'Aus der Trainingsplanung angelegt — bitte Ziel und Durchführung in der Übungsbearbeitung ergänzen.'
|
||||||
|
|
||||||
export default function ExercisePickerModal({
|
export default function ExercisePickerModal({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onSelectExercise,
|
onSelectExercise,
|
||||||
multiSelect = false,
|
multiSelect = false,
|
||||||
onSelectExercises = null,
|
onSelectExercises = null,
|
||||||
|
enableQuickCreateDraft = false,
|
||||||
}) {
|
}) {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const [catalogs, setCatalogs] = useState({
|
const [catalogs, setCatalogs] = useState({
|
||||||
|
|
@ -49,6 +54,10 @@ export default function ExercisePickerModal({
|
||||||
const [offset, setOffset] = useState(0)
|
const [offset, setOffset] = useState(0)
|
||||||
const [hasMore, setHasMore] = useState(false)
|
const [hasMore, setHasMore] = useState(false)
|
||||||
const [multiPicked, setMultiPicked] = useState([])
|
const [multiPicked, setMultiPicked] = useState([])
|
||||||
|
const [quickOpen, setQuickOpen] = useState(false)
|
||||||
|
const [quickTitle, setQuickTitle] = useState('')
|
||||||
|
const [quickSummary, setQuickSummary] = useState('')
|
||||||
|
const [quickSaving, setQuickSaving] = useState(false)
|
||||||
|
|
||||||
const toggleMultiPick = (ex) => {
|
const toggleMultiPick = (ex) => {
|
||||||
setMultiPicked((prev) =>
|
setMultiPicked((prev) =>
|
||||||
|
|
@ -110,6 +119,10 @@ export default function ExercisePickerModal({
|
||||||
setOffset(0)
|
setOffset(0)
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
setMultiPicked([])
|
setMultiPicked([])
|
||||||
|
setQuickOpen(false)
|
||||||
|
setQuickTitle('')
|
||||||
|
setQuickSummary('')
|
||||||
|
setQuickSaving(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setFilters(mergeExerciseListPrefsFromApi(user?.exercise_list_prefs))
|
setFilters(mergeExerciseListPrefsFromApi(user?.exercise_list_prefs))
|
||||||
|
|
@ -256,6 +269,48 @@ export default function ExercisePickerModal({
|
||||||
|
|
||||||
const resetFilters = () => setFilters({ ...INITIAL_FILTERS })
|
const resetFilters = () => setFilters({ ...INITIAL_FILTERS })
|
||||||
|
|
||||||
|
const submitQuickCreate = async () => {
|
||||||
|
const title = (quickTitle || '').trim()
|
||||||
|
if (title.length < 3) {
|
||||||
|
alert('Titel: mindestens 3 Zeichen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const summaryRaw = (quickSummary || '').trim()
|
||||||
|
setQuickSaving(true)
|
||||||
|
try {
|
||||||
|
const created = await api.createExercise({
|
||||||
|
title,
|
||||||
|
summary: summaryRaw || null,
|
||||||
|
goal: QUICK_CREATE_GOAL_PLACEHOLDER,
|
||||||
|
execution: null,
|
||||||
|
visibility: 'private',
|
||||||
|
status: 'draft',
|
||||||
|
equipment: [],
|
||||||
|
focus_areas_multi: [],
|
||||||
|
training_styles_multi: [],
|
||||||
|
training_types_multi: [],
|
||||||
|
target_groups_multi: [],
|
||||||
|
age_groups: [],
|
||||||
|
skills: [],
|
||||||
|
club_id: null,
|
||||||
|
})
|
||||||
|
if (!created?.id) {
|
||||||
|
throw new Error('Anlegen fehlgeschlagen')
|
||||||
|
}
|
||||||
|
if (multiSelect && typeof onSelectExercises === 'function') {
|
||||||
|
await Promise.resolve(onSelectExercises([created]))
|
||||||
|
} else if (typeof onSelectExercise === 'function') {
|
||||||
|
await Promise.resolve(onSelectExercise(created))
|
||||||
|
}
|
||||||
|
onClose()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
alert(e.message || 'Übung konnte nicht angelegt werden')
|
||||||
|
} finally {
|
||||||
|
setQuickSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!open) return null
|
if (!open) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -282,6 +337,75 @@ export default function ExercisePickerModal({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{enableQuickCreateDraft ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '10px 1rem 12px',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
flexShrink: 0,
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onClick={() => setQuickOpen((v) => !v)}
|
||||||
|
aria-expanded={quickOpen}
|
||||||
|
>
|
||||||
|
{quickOpen ? 'Neue Übung ausblenden' : 'Neue Übung anlegen (Entwurf, privat)'}
|
||||||
|
</button>
|
||||||
|
{quickOpen ? (
|
||||||
|
<div style={{ marginTop: '12px', display: 'grid', gap: '10px' }}>
|
||||||
|
<p style={{ margin: 0, fontSize: '13px', color: 'var(--text2)', lineHeight: 1.45 }}>
|
||||||
|
Wird mit Sichtbarkeit <strong>privat</strong> und Status <strong>Entwurf</strong> gespeichert und
|
||||||
|
erscheint auf dem Dashboard zum Weiterbearbeiten. Nach dem Speichern wird die Übung direkt in den
|
||||||
|
Ablauf übernommen.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" htmlFor="ex-picker-quick-title">
|
||||||
|
Titel
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="ex-picker-quick-title"
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
value={quickTitle}
|
||||||
|
onChange={(e) => setQuickTitle(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
minLength={3}
|
||||||
|
maxLength={300}
|
||||||
|
placeholder="z. B. Partnerübung Abwehr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" htmlFor="ex-picker-quick-summary">
|
||||||
|
Kurzbeschreibung
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="ex-picker-quick-summary"
|
||||||
|
className="form-input"
|
||||||
|
rows={3}
|
||||||
|
value={quickSummary}
|
||||||
|
onChange={(e) => setQuickSummary(e.target.value)}
|
||||||
|
placeholder="Optional: grobe Idee, Kontext aus der Planung …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={quickSaving || (quickTitle || '').trim().length < 3}
|
||||||
|
onClick={submitQuickCreate}
|
||||||
|
>
|
||||||
|
{quickSaving ? 'Wird angelegt…' : 'Entwurf anlegen und übernehmen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div style={{ padding: '0 1rem 0.75rem', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
|
<div style={{ padding: '0 1rem 0.75rem', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
|
||||||
<div style={{ display: 'grid', gap: '0.65rem' }}>
|
<div style={{ display: 'grid', gap: '0.65rem' }}>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -115,10 +115,15 @@ function Dashboard() {
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
|
const drafts = Array.isArray(draftList) ? draftList : []
|
||||||
setPhase0Stats({
|
setPhase0Stats({
|
||||||
year,
|
year,
|
||||||
draftCount: Array.isArray(draftList) ? draftList.length : 0,
|
draftCount: drafts.length,
|
||||||
draftCapped: Array.isArray(draftList) && draftList.length >= 100,
|
draftCapped: drafts.length >= 100,
|
||||||
|
draftPreview: drafts.slice(0, 8).map((ex) => ({
|
||||||
|
id: ex.id,
|
||||||
|
title: ex.title || `Übung #${ex.id}`,
|
||||||
|
})),
|
||||||
mineCount: Array.isArray(mineList) ? mineList.length : 0,
|
mineCount: Array.isArray(mineList) ? mineList.length : 0,
|
||||||
mineCapped: Array.isArray(mineList) && mineList.length >= 100,
|
mineCapped: Array.isArray(mineList) && mineList.length >= 100,
|
||||||
ytdCompletedCount: Array.isArray(ytdCompleted) ? ytdCompleted.length : 0,
|
ytdCompletedCount: Array.isArray(ytdCompleted) ? ytdCompleted.length : 0,
|
||||||
|
|
@ -233,6 +238,29 @@ function Dashboard() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{!phase0Err && phase0Stats?.draftPreview?.length ? (
|
||||||
|
<div className="card dashboard-draft-preview" style={{ marginTop: '1rem' }}>
|
||||||
|
<h3 className="dashboard-preview-card__title" style={{ marginTop: 0 }}>
|
||||||
|
Entwürfe fertigstellen
|
||||||
|
</h3>
|
||||||
|
<p className="muted" style={{ marginTop: '0.35rem', marginBottom: '0.85rem', fontSize: '0.92rem' }}>
|
||||||
|
Private Übungs-Entwürfe (z. B. aus der Planung) — Ziel, Durchführung und Details in der Bearbeitung
|
||||||
|
ergänzen.
|
||||||
|
</p>
|
||||||
|
<ul className="dashboard-preview-card__list">
|
||||||
|
{phase0Stats.draftPreview.map((ex) => (
|
||||||
|
<li key={ex.id}>
|
||||||
|
<Link to={`/exercises/${ex.id}/edit`} className="dashboard-preview-card__link">
|
||||||
|
{ex.title}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p style={{ margin: '0.75rem 0 0', fontSize: '0.86rem' }}>
|
||||||
|
<Link to={draftsHref}>Alle Entwürfe in der Übersicht</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="dashboard-section" aria-labelledby="dash-trainings-title">
|
<section className="dashboard-section" aria-labelledby="dash-trainings-title">
|
||||||
|
|
|
||||||
|
|
@ -1086,6 +1086,7 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
<ExercisePickerModal
|
<ExercisePickerModal
|
||||||
open={sectionPickerCtx != null}
|
open={sectionPickerCtx != null}
|
||||||
multiSelect
|
multiSelect
|
||||||
|
enableQuickCreateDraft
|
||||||
onClose={() => setSectionPickerCtx(null)}
|
onClose={() => setSectionPickerCtx(null)}
|
||||||
onSelectExercises={async (picked) => {
|
onSelectExercises={async (picked) => {
|
||||||
if (!sectionPickerCtx || !picked?.length) return
|
if (!sectionPickerCtx || !picked?.length) return
|
||||||
|
|
|
||||||
|
|
@ -2421,6 +2421,7 @@ function TrainingPlanningPage() {
|
||||||
<ExercisePickerModal
|
<ExercisePickerModal
|
||||||
open={exercisePickerOpen}
|
open={exercisePickerOpen}
|
||||||
multiSelect
|
multiSelect
|
||||||
|
enableQuickCreateDraft
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setExercisePickerOpen(false)
|
setExercisePickerOpen(false)
|
||||||
setExercisePickerTarget(null)
|
setExercisePickerTarget(null)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user