shinkan-jinkendo/frontend/src/pages/SkillsPage.jsx

518 lines
18 KiB
React
Raw Normal View History

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