Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 6s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 29s
- Added new CSS styles for Skills and Exercises pages, improving layout and responsiveness. - Refactored components to utilize new styles, enhancing visual consistency and user experience. - Implemented horizontal scrollable navigation for exercises and skills tabs, improving usability on smaller screens. - Updated button styles and introduced new class names for better maintainability and accessibility. - Enhanced loading states and empty messages for improved user feedback during data fetching.
375 lines
14 KiB
JavaScript
375 lines
14 KiB
JavaScript
import React, { useState } from 'react'
|
|
import { api } from '../../utils/api'
|
|
|
|
function DetailPanel({ item, onUpdate, focusAreas }) {
|
|
const type = item._type
|
|
|
|
if (type === 'create_style_direction') {
|
|
return <CreateStyleDirectionForm item={item} onUpdate={onUpdate} />
|
|
}
|
|
if (type === 'create_training_type') {
|
|
return <CreateTrainingTypeForm item={item} onUpdate={onUpdate} />
|
|
}
|
|
if (type === 'focus_area') {
|
|
return <FocusAreaDetail item={item} onUpdate={onUpdate} />
|
|
}
|
|
if (type === 'style_direction') {
|
|
return <StyleDirectionDetail item={item} onUpdate={onUpdate} focusAreas={focusAreas} />
|
|
}
|
|
if (type === 'training_type') {
|
|
return <TrainingTypeDetail item={item} onUpdate={onUpdate} focusAreas={focusAreas} />
|
|
}
|
|
|
|
return <div className="detail-panel__unknown">Unbekannter Typ: {type}</div>
|
|
}
|
|
|
|
function FocusAreaDetail({ item, onUpdate }) {
|
|
const [form, setForm] = useState({
|
|
name: item.name || '',
|
|
icon: item.icon || '',
|
|
description: item.description || '',
|
|
sort_order: item.sort_order || 0,
|
|
status: item.status || 'active'
|
|
})
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
async function handleSave() {
|
|
setSaving(true)
|
|
try {
|
|
await api.updateFocusArea(item.id, form)
|
|
onUpdate()
|
|
} catch (e) {
|
|
alert('Fehler: ' + e.message)
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
async function handleDelete() {
|
|
if (!confirm(`Fokusbereich "${item.name}" wirklich löschen? ACHTUNG: Alle zugeordneten Stilrichtungen und Trainingstypen werden ebenfalls gelöscht!`)) return
|
|
try {
|
|
await api.deleteFocusArea(item.id)
|
|
onUpdate()
|
|
} catch (e) {
|
|
alert('Fehler: ' + e.message)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h2 className="detail-panel__title">Fokusbereich bearbeiten</h2>
|
|
<div className="form-row">
|
|
<label className="form-label">Name *</label>
|
|
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Icon (Emoji)</label>
|
|
<input className="form-input" value={form.icon} onChange={e => setForm({ ...form, icon: e.target.value })} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Beschreibung</label>
|
|
<textarea className="form-input" value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} rows={3} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Sortierung</label>
|
|
<input className="form-input" type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Status</label>
|
|
<select className="form-input" value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>
|
|
<option value="active">Aktiv</option>
|
|
<option value="inactive">Inaktiv</option>
|
|
</select>
|
|
</div>
|
|
<div className="detail-panel__actions">
|
|
<button className="btn btn-primary" onClick={handleSave} disabled={saving || !form.name}>
|
|
{saving ? 'Speichert...' : 'Speichern'}
|
|
</button>
|
|
<button type="button" className="btn btn-danger" onClick={handleDelete}>Löschen</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StyleDirectionDetail({ item, onUpdate, focusAreas }) {
|
|
const [form, setForm] = useState({
|
|
name: item.name || '',
|
|
abbreviation: item.abbreviation || '',
|
|
description: item.description || '',
|
|
focus_area_id: item.focus_area_id || null,
|
|
sort_order: item.sort_order || 0,
|
|
status: item.status || 'active'
|
|
})
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
async function handleSave() {
|
|
if (!form.focus_area_id) {
|
|
alert('Bitte Fokusbereich auswählen')
|
|
return
|
|
}
|
|
setSaving(true)
|
|
try {
|
|
await api.updateStyleDirection(item.id, form)
|
|
onUpdate()
|
|
} catch (e) {
|
|
alert('Fehler: ' + e.message)
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
async function handleDelete() {
|
|
if (!confirm(`Stilrichtung "${item.name}" wirklich löschen?`)) return
|
|
try {
|
|
await api.deleteStyleDirection(item.id)
|
|
onUpdate()
|
|
} catch (e) {
|
|
alert('Fehler: ' + e.message)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h2 className="detail-panel__title">Stilrichtung bearbeiten</h2>
|
|
<div className="form-row">
|
|
<label className="form-label">Name *</label>
|
|
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Abkürzung</label>
|
|
<input className="form-input" value={form.abbreviation} onChange={e => setForm({ ...form, abbreviation: e.target.value })} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Beschreibung</label>
|
|
<textarea className="form-input" value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} rows={3} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Fokusbereich *</label>
|
|
<select className="form-input" value={form.focus_area_id || ''} onChange={e => setForm({ ...form, focus_area_id: parseInt(e.target.value) || null })}>
|
|
<option value="">Bitte wählen...</option>
|
|
{focusAreas.map(fa => (
|
|
<option key={fa.id} value={fa.id}>{fa.icon} {fa.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Sortierung</label>
|
|
<input className="form-input" type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Status</label>
|
|
<select className="form-input" value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>
|
|
<option value="active">Aktiv</option>
|
|
<option value="inactive">Inaktiv</option>
|
|
</select>
|
|
</div>
|
|
<div className="detail-panel__actions">
|
|
<button className="btn btn-primary" onClick={handleSave} disabled={saving || !form.name || !form.focus_area_id}>
|
|
{saving ? 'Speichert...' : 'Speichern'}
|
|
</button>
|
|
<button type="button" className="btn btn-danger" onClick={handleDelete}>Löschen</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function TrainingTypeDetail({ item, onUpdate, focusAreas }) {
|
|
const [form, setForm] = useState({
|
|
name: item.name || '',
|
|
abbreviation: item.abbreviation || '',
|
|
description: item.description || '',
|
|
focus_area_id: item.focus_area_id || null,
|
|
sort_order: item.sort_order || 0,
|
|
status: item.status || 'active'
|
|
})
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
async function handleSave() {
|
|
if (!form.focus_area_id) {
|
|
alert('Bitte Fokusbereich auswählen')
|
|
return
|
|
}
|
|
setSaving(true)
|
|
try {
|
|
await api.updateTrainingType(item.id, form)
|
|
onUpdate()
|
|
} catch (e) {
|
|
alert('Fehler: ' + e.message)
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
async function handleDelete() {
|
|
if (!confirm(`Trainingstyp "${item.name}" wirklich löschen?`)) return
|
|
try {
|
|
await api.deleteTrainingType(item.id)
|
|
onUpdate()
|
|
} catch (e) {
|
|
alert('Fehler: ' + e.message)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h2 className="detail-panel__title">Trainingstyp bearbeiten</h2>
|
|
<div className="form-row">
|
|
<label className="form-label">Name *</label>
|
|
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Abkürzung</label>
|
|
<input className="form-input" value={form.abbreviation} onChange={e => setForm({ ...form, abbreviation: e.target.value })} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Beschreibung</label>
|
|
<textarea className="form-input" value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} rows={3} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Fokusbereich *</label>
|
|
<select className="form-input" value={form.focus_area_id || ''} onChange={e => setForm({ ...form, focus_area_id: parseInt(e.target.value) || null })}>
|
|
<option value="">Bitte wählen...</option>
|
|
{focusAreas.map(fa => (
|
|
<option key={fa.id} value={fa.id}>{fa.icon} {fa.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Sortierung</label>
|
|
<input className="form-input" type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Status</label>
|
|
<select className="form-input" value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>
|
|
<option value="active">Aktiv</option>
|
|
<option value="inactive">Inaktiv</option>
|
|
</select>
|
|
</div>
|
|
<div className="detail-panel__actions">
|
|
<button className="btn btn-primary" onClick={handleSave} disabled={saving || !form.name || !form.focus_area_id}>
|
|
{saving ? 'Speichert...' : 'Speichern'}
|
|
</button>
|
|
<button type="button" className="btn btn-danger" onClick={handleDelete}>Löschen</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CreateStyleDirectionForm({ item, onUpdate }) {
|
|
const [form, setForm] = useState({
|
|
name: '',
|
|
abbreviation: '',
|
|
description: '',
|
|
focus_area_id: item.focus_area_id,
|
|
sort_order: 0,
|
|
status: 'active'
|
|
})
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
async function handleCreate() {
|
|
if (!form.name) {
|
|
alert('Bitte Name eingeben')
|
|
return
|
|
}
|
|
setSaving(true)
|
|
try {
|
|
await api.createStyleDirection(form)
|
|
onUpdate()
|
|
} catch (e) {
|
|
alert('Fehler: ' + e.message)
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h2 className="detail-panel__title">Neue Stilrichtung erstellen</h2>
|
|
<div className="detail-panel__context">
|
|
Fokusbereich: <strong>{item.focus_area_name}</strong>
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Name *</label>
|
|
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="z.B. Shotokan" />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Abkürzung</label>
|
|
<input className="form-input" value={form.abbreviation} onChange={e => setForm({ ...form, abbreviation: e.target.value })} placeholder="z.B. SHO" />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Beschreibung</label>
|
|
<textarea className="form-input" value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} rows={3} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Sortierung</label>
|
|
<input className="form-input" type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
|
|
</div>
|
|
<div className="detail-panel__actions">
|
|
<button className="btn btn-primary" onClick={handleCreate} disabled={saving || !form.name}>
|
|
{saving ? 'Erstellt...' : 'Erstellen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CreateTrainingTypeForm({ item, onUpdate }) {
|
|
const [form, setForm] = useState({
|
|
name: '',
|
|
abbreviation: '',
|
|
description: '',
|
|
focus_area_id: item.focus_area_id,
|
|
sort_order: 0,
|
|
status: 'active'
|
|
})
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
async function handleCreate() {
|
|
if (!form.name) {
|
|
alert('Bitte Name eingeben')
|
|
return
|
|
}
|
|
setSaving(true)
|
|
try {
|
|
await api.createTrainingType(form)
|
|
onUpdate()
|
|
} catch (e) {
|
|
alert('Fehler: ' + e.message)
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h2 className="detail-panel__title">Neuen Trainingstyp erstellen</h2>
|
|
<div className="detail-panel__context">
|
|
Fokusbereich: <strong>{item.focus_area_name}</strong>
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Name *</label>
|
|
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="z.B. Breitensport" />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Abkürzung</label>
|
|
<input className="form-input" value={form.abbreviation} onChange={e => setForm({ ...form, abbreviation: e.target.value })} placeholder="z.B. BREIT" />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Beschreibung</label>
|
|
<textarea className="form-input" value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} rows={3} />
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Sortierung</label>
|
|
<input className="form-input" type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
|
|
</div>
|
|
<div className="detail-panel__actions">
|
|
<button className="btn btn-primary" onClick={handleCreate} disabled={saving || !form.name}>
|
|
{saving ? 'Erstellt...' : 'Erstellen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default DetailPanel
|