feat: add quick create draft functionality in ExercisePickerModal
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 7s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Successful in 22s
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 7s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Successful in 22s
- Introduced a quick create draft feature allowing users to create private exercise drafts directly from the ExercisePickerModal. - Added state management for quick create inputs including title and summary, with validation for minimum title length. - Updated the Dashboard to display a preview of private exercise drafts, enhancing user visibility of pending exercises. - Enabled quick create functionality in TrainingFrameworkProgramEditPage and TrainingPlanningPage for streamlined exercise management.
This commit is contained in:
parent
ff8fd78a31
commit
c6a7d668c5
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2421,6 +2421,7 @@ function TrainingPlanningPage() {
|
|||
<ExercisePickerModal
|
||||
open={exercisePickerOpen}
|
||||
multiSelect
|
||||
enableQuickCreateDraft
|
||||
onClose={() => {
|
||||
setExercisePickerOpen(false)
|
||||
setExercisePickerTarget(null)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user