pre-Prod Alpha #18

Merged
Lars merged 8 commits from develop into main 2026-05-07 10:37:21 +02:00
4 changed files with 156 additions and 2 deletions
Showing only changes of commit c6a7d668c5 - Show all commits

View File

@ -21,12 +21,17 @@ const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null)
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({
open,
onClose,
onSelectExercise,
multiSelect = false,
onSelectExercises = null,
enableQuickCreateDraft = false,
}) {
const { user } = useAuth()
const [catalogs, setCatalogs] = useState({
@ -49,6 +54,10 @@ export default function ExercisePickerModal({
const [offset, setOffset] = useState(0)
const [hasMore, setHasMore] = useState(false)
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) => {
setMultiPicked((prev) =>
@ -110,6 +119,10 @@ export default function ExercisePickerModal({
setOffset(0)
setHasMore(false)
setMultiPicked([])
setQuickOpen(false)
setQuickTitle('')
setQuickSummary('')
setQuickSaving(false)
return
}
setFilters(mergeExerciseListPrefsFromApi(user?.exercise_list_prefs))
@ -256,6 +269,48 @@ export default function ExercisePickerModal({
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
return (
@ -282,6 +337,75 @@ export default function ExercisePickerModal({
</button>
</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={{ display: 'grid', gap: '0.65rem' }}>
<div>

View File

@ -115,10 +115,15 @@ function Dashboard() {
}),
])
if (!cancelled) {
const drafts = Array.isArray(draftList) ? draftList : []
setPhase0Stats({
year,
draftCount: Array.isArray(draftList) ? draftList.length : 0,
draftCapped: Array.isArray(draftList) && draftList.length >= 100,
draftCount: drafts.length,
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,
mineCapped: Array.isArray(mineList) && mineList.length >= 100,
ytdCompletedCount: Array.isArray(ytdCompleted) ? ytdCompleted.length : 0,
@ -233,6 +238,29 @@ function Dashboard() {
</div>
</div>
) : 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 className="dashboard-section" aria-labelledby="dash-trainings-title">

View File

@ -1086,6 +1086,7 @@ export default function TrainingFrameworkProgramEditPage() {
<ExercisePickerModal
open={sectionPickerCtx != null}
multiSelect
enableQuickCreateDraft
onClose={() => setSectionPickerCtx(null)}
onSelectExercises={async (picked) => {
if (!sectionPickerCtx || !picked?.length) return

View File

@ -2421,6 +2421,7 @@ function TrainingPlanningPage() {
<ExercisePickerModal
open={exercisePickerOpen}
multiSelect
enableQuickCreateDraft
onClose={() => {
setExercisePickerOpen(false)
setExercisePickerTarget(null)