518 lines
18 KiB
React
518 lines
18 KiB
React
|
|
import React, { useState, useEffect } from 'react'
|
|||
|
|
import api from '../utils/api'
|
|||
|
|
import { useAuth } from '../context/AuthContext'
|
|||
|
|
|
|||
|
|
function SkillsPage() {
|
|||
|
|
const { user } = useAuth()
|
|||
|
|
const [activeTab, setActiveTab] = useState('skills')
|
|||
|
|
const [skills, setSkills] = useState([])
|
|||
|
|
const [methods, setMethods] = useState([])
|
|||
|
|
const [loading, setLoading] = useState(true)
|
|||
|
|
const [showModal, setShowModal] = useState(false)
|
|||
|
|
const [editing, setEditing] = useState(null)
|
|||
|
|
const [modalType, setModalType] = useState('skill')
|
|||
|
|
const [formData, setFormData] = useState({})
|
|||
|
|
|
|||
|
|
const isAdmin = user?.role === 'admin' || user?.role === 'superadmin'
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadData()
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
const loadData = async () => {
|
|||
|
|
try {
|
|||
|
|
const [skillsData, methodsData] = await Promise.all([
|
|||
|
|
api.listSkills(),
|
|||
|
|
api.listMethods()
|
|||
|
|
])
|
|||
|
|
setSkills(skillsData)
|
|||
|
|
setMethods(methodsData)
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('Failed to load data:', err)
|
|||
|
|
alert('Fehler beim Laden: ' + err.message)
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleCreate = (type) => {
|
|||
|
|
setEditing(null)
|
|||
|
|
setModalType(type)
|
|||
|
|
|
|||
|
|
if (type === 'skill') {
|
|||
|
|
setFormData({
|
|||
|
|
name: '',
|
|||
|
|
category: '',
|
|||
|
|
description: '',
|
|||
|
|
importance: 3,
|
|||
|
|
keywords: [],
|
|||
|
|
status: 'active'
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
setFormData({
|
|||
|
|
name: '',
|
|||
|
|
abbreviation: '',
|
|||
|
|
category: '',
|
|||
|
|
description: '',
|
|||
|
|
typical_duration: '',
|
|||
|
|
typical_group_size: '',
|
|||
|
|
related_skills: [],
|
|||
|
|
keywords: [],
|
|||
|
|
status: 'active'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setShowModal(true)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleEdit = (item, type) => {
|
|||
|
|
setEditing(item)
|
|||
|
|
setModalType(type)
|
|||
|
|
setFormData({ ...item })
|
|||
|
|
setShowModal(true)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleDelete = async (item, type) => {
|
|||
|
|
const confirmMsg = type === 'skill'
|
|||
|
|
? `Fähigkeit "${item.name}" wirklich löschen?`
|
|||
|
|
: `Trainingsmethode "${item.name}" wirklich löschen?`
|
|||
|
|
|
|||
|
|
if (!confirm(confirmMsg)) return
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if (type === 'skill') {
|
|||
|
|
await api.deleteSkill(item.id)
|
|||
|
|
} else {
|
|||
|
|
await api.deleteMethod(item.id)
|
|||
|
|
}
|
|||
|
|
await loadData()
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('Fehler beim Löschen: ' + err.message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleSubmit = async (e) => {
|
|||
|
|
e.preventDefault()
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if (modalType === 'skill') {
|
|||
|
|
if (editing) {
|
|||
|
|
await api.updateSkill(editing.id, formData)
|
|||
|
|
} else {
|
|||
|
|
await api.createSkill(formData)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if (editing) {
|
|||
|
|
await api.updateMethod(editing.id, formData)
|
|||
|
|
} else {
|
|||
|
|
await api.createMethod(formData)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setShowModal(false)
|
|||
|
|
await loadData()
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('Fehler beim Speichern: ' + err.message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updateFormField = (field, value) => {
|
|||
|
|
setFormData(prev => ({ ...prev, [field]: value }))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const groupByCategory = (items) => {
|
|||
|
|
const grouped = {}
|
|||
|
|
items.forEach(item => {
|
|||
|
|
const cat = item.category || 'Ohne Kategorie'
|
|||
|
|
if (!grouped[cat]) grouped[cat] = []
|
|||
|
|
grouped[cat].push(item)
|
|||
|
|
})
|
|||
|
|
return grouped
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|||
|
|
<div className="spinner"></div>
|
|||
|
|
<p>Laden...</p>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const skillsByCategory = groupByCategory(skills)
|
|||
|
|
const methodsByCategory = groupByCategory(methods)
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div style={{ padding: '2rem' }}>
|
|||
|
|
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
|||
|
|
<h1 style={{ marginBottom: '1.5rem' }}>Fähigkeiten & Methoden</h1>
|
|||
|
|
|
|||
|
|
{/* Tabs */}
|
|||
|
|
<div style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
gap: '0.5rem',
|
|||
|
|
marginBottom: '1.5rem',
|
|||
|
|
borderBottom: '2px solid var(--border)'
|
|||
|
|
}}>
|
|||
|
|
{['skills', 'methods'].map(tab => (
|
|||
|
|
<button
|
|||
|
|
key={tab}
|
|||
|
|
onClick={() => setActiveTab(tab)}
|
|||
|
|
style={{
|
|||
|
|
padding: '0.75rem 1.5rem',
|
|||
|
|
background: activeTab === tab ? 'var(--accent)' : 'transparent',
|
|||
|
|
color: activeTab === tab ? 'white' : 'var(--text1)',
|
|||
|
|
border: 'none',
|
|||
|
|
borderRadius: '8px 8px 0 0',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
fontWeight: activeTab === tab ? 'bold' : 'normal'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{tab === 'skills' ? 'Fähigkeiten' : 'Trainingsmethoden'}
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Skills Tab */}
|
|||
|
|
{activeTab === 'skills' && (
|
|||
|
|
<>
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
|||
|
|
<p style={{ color: 'var(--text2)' }}>
|
|||
|
|
Fähigkeiten sind Kompetenzen, die in Übungen trainiert werden.
|
|||
|
|
</p>
|
|||
|
|
{isAdmin && (
|
|||
|
|
<button className="btn btn-primary" onClick={() => handleCreate('skill')}>
|
|||
|
|
+ Neue Fähigkeit
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{Object.keys(skillsByCategory).length === 0 ? (
|
|||
|
|
<div className="card">
|
|||
|
|
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
|||
|
|
Keine Fähigkeiten gefunden
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
Object.keys(skillsByCategory).sort().map(category => (
|
|||
|
|
<div key={category} style={{ marginBottom: '2rem' }}>
|
|||
|
|
<h2 style={{ marginBottom: '1rem', textTransform: 'capitalize' }}>
|
|||
|
|
{category}
|
|||
|
|
</h2>
|
|||
|
|
<div style={{
|
|||
|
|
display: 'grid',
|
|||
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
|||
|
|
gap: '1rem'
|
|||
|
|
}}>
|
|||
|
|
{skillsByCategory[category].map(skill => (
|
|||
|
|
<div key={skill.id} className="card">
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '0.5rem' }}>
|
|||
|
|
<h3 style={{ fontSize: '1rem' }}>{skill.name}</h3>
|
|||
|
|
{skill.importance && (
|
|||
|
|
<span style={{
|
|||
|
|
fontSize: '0.875rem',
|
|||
|
|
padding: '0.25rem 0.5rem',
|
|||
|
|
borderRadius: '4px',
|
|||
|
|
background: 'var(--accent)',
|
|||
|
|
color: 'white'
|
|||
|
|
}}>
|
|||
|
|
⭐ {skill.importance}/5
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{skill.description && (
|
|||
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem' }}>
|
|||
|
|
{skill.description}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{isAdmin && (
|
|||
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: 'auto' }}>
|
|||
|
|
<button
|
|||
|
|
className="btn btn-secondary"
|
|||
|
|
style={{ flex: 1 }}
|
|||
|
|
onClick={() => handleEdit(skill, 'skill')}
|
|||
|
|
>
|
|||
|
|
Bearbeiten
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
className="btn"
|
|||
|
|
style={{
|
|||
|
|
background: 'var(--danger)',
|
|||
|
|
color: 'white',
|
|||
|
|
border: 'none'
|
|||
|
|
}}
|
|||
|
|
onClick={() => handleDelete(skill, 'skill')}
|
|||
|
|
>
|
|||
|
|
Löschen
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Methods Tab */}
|
|||
|
|
{activeTab === 'methods' && (
|
|||
|
|
<>
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
|||
|
|
<p style={{ color: 'var(--text2)' }}>
|
|||
|
|
Trainingsmethoden sind didaktische Ansätze für die Trainingsgestaltung.
|
|||
|
|
</p>
|
|||
|
|
{isAdmin && (
|
|||
|
|
<button className="btn btn-primary" onClick={() => handleCreate('method')}>
|
|||
|
|
+ Neue Methode
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{Object.keys(methodsByCategory).length === 0 ? (
|
|||
|
|
<div className="card">
|
|||
|
|
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
|||
|
|
Keine Trainingsmethoden gefunden
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
Object.keys(methodsByCategory).sort().map(category => (
|
|||
|
|
<div key={category} style={{ marginBottom: '2rem' }}>
|
|||
|
|
<h2 style={{ marginBottom: '1rem', textTransform: 'capitalize' }}>
|
|||
|
|
{category}
|
|||
|
|
</h2>
|
|||
|
|
<div style={{
|
|||
|
|
display: 'grid',
|
|||
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
|||
|
|
gap: '1rem'
|
|||
|
|
}}>
|
|||
|
|
{methodsByCategory[category].map(method => (
|
|||
|
|
<div key={method.id} className="card">
|
|||
|
|
<div style={{ marginBottom: '0.5rem' }}>
|
|||
|
|
<h3 style={{ fontSize: '1rem', marginBottom: '0.25rem' }}>
|
|||
|
|
{method.name}
|
|||
|
|
{method.abbreviation && (
|
|||
|
|
<span style={{ color: 'var(--text2)', fontSize: '0.875rem', marginLeft: '0.5rem' }}>
|
|||
|
|
({method.abbreviation})
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</h3>
|
|||
|
|
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
|||
|
|
{method.typical_duration && (
|
|||
|
|
<span style={{
|
|||
|
|
fontSize: '0.75rem',
|
|||
|
|
padding: '0.25rem 0.5rem',
|
|||
|
|
borderRadius: '4px',
|
|||
|
|
background: 'var(--surface2)',
|
|||
|
|
color: 'var(--text2)'
|
|||
|
|
}}>
|
|||
|
|
⏱️ {method.typical_duration} min
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
{method.typical_group_size && (
|
|||
|
|
<span style={{
|
|||
|
|
fontSize: '0.75rem',
|
|||
|
|
padding: '0.25rem 0.5rem',
|
|||
|
|
borderRadius: '4px',
|
|||
|
|
background: 'var(--surface2)',
|
|||
|
|
color: 'var(--text2)'
|
|||
|
|
}}>
|
|||
|
|
👥 {method.typical_group_size}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{method.description && (
|
|||
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem' }}>
|
|||
|
|
{method.description}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{isAdmin && (
|
|||
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: 'auto' }}>
|
|||
|
|
<button
|
|||
|
|
className="btn btn-secondary"
|
|||
|
|
style={{ flex: 1 }}
|
|||
|
|
onClick={() => handleEdit(method, 'method')}
|
|||
|
|
>
|
|||
|
|
Bearbeiten
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
className="btn"
|
|||
|
|
style={{
|
|||
|
|
background: 'var(--danger)',
|
|||
|
|
color: 'white',
|
|||
|
|
border: 'none'
|
|||
|
|
}}
|
|||
|
|
onClick={() => handleDelete(method, 'method')}
|
|||
|
|
>
|
|||
|
|
Löschen
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Modal */}
|
|||
|
|
{showModal && isAdmin && (
|
|||
|
|
<div style={{
|
|||
|
|
position: 'fixed',
|
|||
|
|
top: 0,
|
|||
|
|
left: 0,
|
|||
|
|
right: 0,
|
|||
|
|
bottom: 0,
|
|||
|
|
background: 'rgba(0,0,0,0.5)',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
zIndex: 1000,
|
|||
|
|
padding: '1rem'
|
|||
|
|
}}>
|
|||
|
|
<div style={{
|
|||
|
|
background: 'var(--surface)',
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
padding: '2rem',
|
|||
|
|
maxWidth: '600px',
|
|||
|
|
width: '100%',
|
|||
|
|
maxHeight: '90vh',
|
|||
|
|
overflowY: 'auto'
|
|||
|
|
}}>
|
|||
|
|
<h2 style={{ marginBottom: '1.5rem' }}>
|
|||
|
|
{editing
|
|||
|
|
? (modalType === 'skill' ? 'Fähigkeit bearbeiten' : 'Methode bearbeiten')
|
|||
|
|
: (modalType === 'skill' ? 'Neue Fähigkeit' : 'Neue Methode')
|
|||
|
|
}
|
|||
|
|
</h2>
|
|||
|
|
|
|||
|
|
<form onSubmit={handleSubmit}>
|
|||
|
|
<div className="form-row">
|
|||
|
|
<label className="form-label">Name *</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
className="form-input"
|
|||
|
|
value={formData.name || ''}
|
|||
|
|
onChange={(e) => updateFormField('name', e.target.value)}
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{modalType === 'method' && (
|
|||
|
|
<div className="form-row">
|
|||
|
|
<label className="form-label">Kürzel</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
className="form-input"
|
|||
|
|
value={formData.abbreviation || ''}
|
|||
|
|
onChange={(e) => updateFormField('abbreviation', e.target.value)}
|
|||
|
|
maxLength={20}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="form-row">
|
|||
|
|
<label className="form-label">Kategorie</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
className="form-input"
|
|||
|
|
value={formData.category || ''}
|
|||
|
|
onChange={(e) => updateFormField('category', e.target.value)}
|
|||
|
|
placeholder={modalType === 'skill' ? 'z.B. kihon, kumite, kata' : 'z.B. kondition, didaktik'}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="form-row">
|
|||
|
|
<label className="form-label">Beschreibung</label>
|
|||
|
|
<textarea
|
|||
|
|
className="form-input"
|
|||
|
|
rows={3}
|
|||
|
|
value={formData.description || ''}
|
|||
|
|
onChange={(e) => updateFormField('description', e.target.value)}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{modalType === 'skill' && (
|
|||
|
|
<div className="form-row">
|
|||
|
|
<label className="form-label">Wichtigkeit (1-5)</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
className="form-input"
|
|||
|
|
min={1}
|
|||
|
|
max={5}
|
|||
|
|
value={formData.importance || 3}
|
|||
|
|
onChange={(e) => updateFormField('importance', parseInt(e.target.value))}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{modalType === 'method' && (
|
|||
|
|
<>
|
|||
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
|||
|
|
<div className="form-row">
|
|||
|
|
<label className="form-label">Typische Dauer (min)</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
className="form-input"
|
|||
|
|
value={formData.typical_duration || ''}
|
|||
|
|
onChange={(e) => updateFormField('typical_duration', e.target.value ? parseInt(e.target.value) : '')}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="form-row">
|
|||
|
|
<label className="form-label">Gruppengröße</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
className="form-input"
|
|||
|
|
value={formData.typical_group_size || ''}
|
|||
|
|
onChange={(e) => updateFormField('typical_group_size', e.target.value)}
|
|||
|
|
placeholder="z.B. 10-20"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="form-row">
|
|||
|
|
<label className="form-label">Status</label>
|
|||
|
|
<select
|
|||
|
|
className="form-input"
|
|||
|
|
value={formData.status || 'active'}
|
|||
|
|
onChange={(e) => updateFormField('status', e.target.value)}
|
|||
|
|
>
|
|||
|
|
<option value="active">Aktiv</option>
|
|||
|
|
<option value="inactive">Inaktiv</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1.5rem' }}>
|
|||
|
|
<button type="submit" className="btn btn-primary" style={{ flex: 1 }}>
|
|||
|
|
{editing ? 'Speichern' : 'Erstellen'}
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="btn btn-secondary"
|
|||
|
|
onClick={() => setShowModal(false)}
|
|||
|
|
>
|
|||
|
|
Abbrechen
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default SkillsPage
|