- Removed constrained width classes from multiple pages to allow full-width layout, enhancing adaptability on larger screens. - Updated app.css to eliminate unnecessary max-width properties, ensuring a more fluid design across various components. - Adjusted styles in Navigation and other pages for consistent full-width presentation, improving user experience on diverse devices.
388 lines
13 KiB
JavaScript
388 lines
13 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { api } from '../utils/api'
|
|
|
|
export default function TrainerContextsPage() {
|
|
const [contexts, setContexts] = useState([])
|
|
const [focusAreas, setFocusAreas] = useState([])
|
|
const [styleDirections, setStyleDirections] = useState([])
|
|
const [trainingTypes, setTrainingTypes] = useState([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
// Edit/Create state
|
|
const [editingContext, setEditingContext] = useState(null)
|
|
const [newContext, setNewContext] = useState({
|
|
name: '',
|
|
focus_area_id: null,
|
|
style_direction_id: null,
|
|
training_type_id: null,
|
|
is_style_independent: false,
|
|
description: '',
|
|
is_active: true
|
|
})
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [])
|
|
|
|
async function loadData() {
|
|
setLoading(true)
|
|
setError('')
|
|
try {
|
|
const [ctx, fa, sd, tt] = await Promise.all([
|
|
api.listTrainerContexts(),
|
|
api.listFocusAreas(),
|
|
api.listStyleDirections(),
|
|
api.listTrainingTypes()
|
|
])
|
|
setContexts(ctx)
|
|
setFocusAreas(fa)
|
|
setStyleDirections(sd)
|
|
setTrainingTypes(tt)
|
|
} catch (e) {
|
|
setError(e.message)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
async function createContext() {
|
|
if (!newContext.name) {
|
|
setError('Name ist Pflichtfeld')
|
|
return
|
|
}
|
|
try {
|
|
await api.createTrainerContext(newContext)
|
|
setNewContext({
|
|
name: '',
|
|
focus_area_id: null,
|
|
style_direction_id: null,
|
|
training_type_id: null,
|
|
is_style_independent: false,
|
|
description: '',
|
|
is_active: true
|
|
})
|
|
loadData()
|
|
} catch (e) {
|
|
setError(e.message)
|
|
}
|
|
}
|
|
|
|
async function updateContext(id, data) {
|
|
try {
|
|
await api.updateTrainerContext(id, data)
|
|
setEditingContext(null)
|
|
loadData()
|
|
} catch (e) {
|
|
setError(e.message)
|
|
}
|
|
}
|
|
|
|
async function deleteContext(id) {
|
|
if (!confirm('Trainer-Kontext wirklich löschen?')) return
|
|
try {
|
|
await api.deleteTrainerContext(id)
|
|
loadData()
|
|
} catch (e) {
|
|
setError(e.message)
|
|
}
|
|
}
|
|
|
|
// Filter style directions by selected focus area
|
|
function getFilteredStyleDirections(focusAreaId) {
|
|
if (!focusAreaId) return []
|
|
return styleDirections.filter(sd => sd.focus_area_id === parseInt(focusAreaId))
|
|
}
|
|
|
|
return (
|
|
<div className="app-page">
|
|
<h1>Meine Trainer-Bereiche</h1>
|
|
<p style={{ color: 'var(--text2)', marginBottom: '32px' }}>
|
|
Definiere deine Tätigkeitsbereiche für fokussierte Ansichten und Filter.
|
|
</p>
|
|
|
|
{error && (
|
|
<div style={{
|
|
padding: '16px',
|
|
marginBottom: '24px',
|
|
background: 'var(--danger)',
|
|
color: 'white',
|
|
borderRadius: '8px'
|
|
}}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Create New Context */}
|
|
<div className="card" style={{ marginBottom: '32px' }}>
|
|
<h3>Neuer Trainer-Bereich</h3>
|
|
|
|
<div className="form-row">
|
|
<label className="form-label">Name *</label>
|
|
<input
|
|
className="form-input"
|
|
value={newContext.name}
|
|
onChange={e => setNewContext({ ...newContext, name: e.target.value })}
|
|
placeholder="z.B. Karate Goju-Ryu Breitensport"
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label className="form-label">Fokusbereich</label>
|
|
<select
|
|
className="form-input"
|
|
value={newContext.focus_area_id || ''}
|
|
onChange={e => setNewContext({
|
|
...newContext,
|
|
focus_area_id: e.target.value ? parseInt(e.target.value) : null,
|
|
style_direction_id: null // Reset style when focus changes
|
|
})}
|
|
>
|
|
<option value="">- Alle Fokusbereiche -</option>
|
|
{focusAreas.map(fa => (
|
|
<option key={fa.id} value={fa.id}>{fa.icon} {fa.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{newContext.focus_area_id && (
|
|
<div className="form-row">
|
|
<label className="form-label">Stilrichtung</label>
|
|
<select
|
|
className="form-input"
|
|
value={newContext.style_direction_id || ''}
|
|
onChange={e => setNewContext({
|
|
...newContext,
|
|
style_direction_id: e.target.value ? parseInt(e.target.value) : null
|
|
})}
|
|
>
|
|
<option value="">- Alle Stilrichtungen -</option>
|
|
{getFilteredStyleDirections(newContext.focus_area_id).map(sd => (
|
|
<option key={sd.id} value={sd.id}>{sd.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
<div className="form-row">
|
|
<label className="form-label">Trainingsstil</label>
|
|
<select
|
|
className="form-input"
|
|
value={newContext.training_type_id || ''}
|
|
onChange={e => setNewContext({
|
|
...newContext,
|
|
training_type_id: e.target.value ? parseInt(e.target.value) : null
|
|
})}
|
|
>
|
|
<option value="">- Alle Trainingsstile -</option>
|
|
{trainingTypes.map(tt => (
|
|
<option key={tt.id} value={tt.id}>{tt.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={newContext.is_style_independent}
|
|
onChange={e => setNewContext({ ...newContext, is_style_independent: e.target.checked })}
|
|
/>
|
|
Stilrichtungsunabhängig (z.B. Leistungssport Kumite für alle Stilrichtungen)
|
|
</label>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label className="form-label">Beschreibung</label>
|
|
<textarea
|
|
className="form-input"
|
|
value={newContext.description}
|
|
onChange={e => setNewContext({ ...newContext, description: e.target.value })}
|
|
rows={2}
|
|
placeholder="Optionale Beschreibung..."
|
|
/>
|
|
</div>
|
|
|
|
<button className="btn btn-primary" onClick={createContext}>Anlegen</button>
|
|
</div>
|
|
|
|
{/* List of Contexts */}
|
|
<div style={{ display: 'grid', gap: '16px' }}>
|
|
{loading && <p>Laden...</p>}
|
|
|
|
{!loading && contexts.length === 0 && (
|
|
<div className="card">
|
|
<p style={{ margin: 0, color: 'var(--text2)' }}>
|
|
Noch keine Trainer-Bereiche definiert. Lege oben deinen ersten Bereich an.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{contexts.map(ctx => (
|
|
<div key={ctx.id} className="card">
|
|
{editingContext?.id === ctx.id ? (
|
|
// Edit Mode
|
|
<div>
|
|
<div className="form-row">
|
|
<label className="form-label">Name</label>
|
|
<input
|
|
className="form-input"
|
|
value={editingContext.name}
|
|
onChange={e => setEditingContext({ ...editingContext, name: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label className="form-label">Fokusbereich</label>
|
|
<select
|
|
className="form-input"
|
|
value={editingContext.focus_area_id || ''}
|
|
onChange={e => setEditingContext({
|
|
...editingContext,
|
|
focus_area_id: e.target.value ? parseInt(e.target.value) : null,
|
|
style_direction_id: null
|
|
})}
|
|
>
|
|
<option value="">- Alle -</option>
|
|
{focusAreas.map(fa => (
|
|
<option key={fa.id} value={fa.id}>{fa.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{editingContext.focus_area_id && (
|
|
<div className="form-row">
|
|
<label className="form-label">Stilrichtung</label>
|
|
<select
|
|
className="form-input"
|
|
value={editingContext.style_direction_id || ''}
|
|
onChange={e => setEditingContext({
|
|
...editingContext,
|
|
style_direction_id: e.target.value ? parseInt(e.target.value) : null
|
|
})}
|
|
>
|
|
<option value="">- Alle -</option>
|
|
{getFilteredStyleDirections(editingContext.focus_area_id).map(sd => (
|
|
<option key={sd.id} value={sd.id}>{sd.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
<div className="form-row">
|
|
<label className="form-label">Trainingsstil</label>
|
|
<select
|
|
className="form-input"
|
|
value={editingContext.training_type_id || ''}
|
|
onChange={e => setEditingContext({
|
|
...editingContext,
|
|
training_type_id: e.target.value ? parseInt(e.target.value) : null
|
|
})}
|
|
>
|
|
<option value="">- Alle -</option>
|
|
{trainingTypes.map(tt => (
|
|
<option key={tt.id} value={tt.id}>{tt.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={editingContext.is_style_independent}
|
|
onChange={e => setEditingContext({ ...editingContext, is_style_independent: e.target.checked })}
|
|
/>
|
|
Stilrichtungsunabhängig
|
|
</label>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<label className="form-label">Beschreibung</label>
|
|
<textarea
|
|
className="form-input"
|
|
value={editingContext.description || ''}
|
|
onChange={e => setEditingContext({ ...editingContext, description: e.target.value })}
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
<button className="btn btn-primary" onClick={() => updateContext(ctx.id, editingContext)}>
|
|
Speichern
|
|
</button>
|
|
<button className="btn" onClick={() => setEditingContext(null)}>
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
// Display Mode
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
<div>
|
|
<h3 style={{ margin: '0 0 8px 0' }}>
|
|
{ctx.name}
|
|
{!ctx.is_active && (
|
|
<span style={{ marginLeft: '8px', fontSize: '14px', color: 'var(--text3)' }}>
|
|
(inaktiv)
|
|
</span>
|
|
)}
|
|
</h3>
|
|
|
|
<div style={{ fontSize: '14px', color: 'var(--text2)', marginBottom: '4px' }}>
|
|
{ctx.focus_area_name && (
|
|
<span>
|
|
{ctx.focus_area_icon} {ctx.focus_area_name}
|
|
</span>
|
|
)}
|
|
{!ctx.focus_area_name && <span>Alle Fokusbereiche</span>}
|
|
|
|
{ctx.style_direction_name && (
|
|
<span> → {ctx.style_direction_name}</span>
|
|
)}
|
|
{ctx.focus_area_name && !ctx.style_direction_name && !ctx.is_style_independent && (
|
|
<span> → Alle Stilrichtungen</span>
|
|
)}
|
|
{ctx.is_style_independent && (
|
|
<span style={{ color: 'var(--accent)' }}> → Stilunabhängig</span>
|
|
)}
|
|
|
|
{ctx.training_type_name && (
|
|
<span> → {ctx.training_type_name}</span>
|
|
)}
|
|
</div>
|
|
|
|
{ctx.description && (
|
|
<p style={{ fontSize: '13px', color: 'var(--text3)', margin: '8px 0 0 0' }}>
|
|
{ctx.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
<button
|
|
className="btn"
|
|
onClick={() => setEditingContext(ctx)}
|
|
style={{ padding: '4px 12px', fontSize: '14px' }}
|
|
>
|
|
Bearbeiten
|
|
</button>
|
|
<button
|
|
className="btn"
|
|
onClick={() => deleteContext(ctx.id)}
|
|
style={{ padding: '4px 12px', fontSize: '14px' }}
|
|
>
|
|
Löschen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|