shinkan-jinkendo/frontend/src/pages/SkillsPage.jsx
Lars 623af621b4
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m8s
Enhance MediaWiki import functionality with category normalization and skill attributes
- Introduced `_normalize_mw_category` function to clean category names for API calls, ensuring consistent handling of category prefixes.
- Updated `SmwClient` methods to utilize normalized category names, improving data retrieval accuracy.
- Added `_wiki_category_or_default` function to provide default categories based on import type, enhancing user experience during imports.
- Integrated new fields `karate_relevance` and `relevance_level` into various admin components, allowing for better skill management.
- Incremented app version to 0.8.145 and updated changelog to reflect these changes.
2026-05-16 11:05:15 +02:00

519 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import api from '../utils/api'
import { useAuth } from '../context/AuthContext'
import PageSectionNav from '../components/PageSectionNav'
const SKILLS_SECTION_TABS = [
{ id: 'skills', label: 'Fähigkeiten' },
{ id: 'methods', label: 'Trainingsmethoden' },
]
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',
karate_relevance: '',
relevance_level: ''
})
} 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') {
const raw = { ...formData }
raw.karate_relevance =
typeof raw.karate_relevance === 'string' && raw.karate_relevance.trim()
? raw.karate_relevance.trim()
: null
raw.relevance_level =
raw.relevance_level === '' || raw.relevance_level == null || raw.relevance_level === undefined
? null
: Number(raw.relevance_level)
if (editing) {
await api.updateSkill(editing.id, raw)
} else {
await api.createSkill(raw)
}
} 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 className="skills-page__loading">
<div className="spinner"></div>
<p>Laden...</p>
</div>
)
}
const skillsByCategory = groupByCategory(skills)
const methodsByCategory = groupByCategory(methods)
return (
<div className="app-page skills-page">
<h1 className="page-title">Fähigkeiten & Methoden</h1>
<PageSectionNav
ariaLabel="Bereich wählen"
value={activeTab}
onChange={setActiveTab}
items={SKILLS_SECTION_TABS}
className="skills-page__tabs-scroll"
/>
{/* Skills Tab */}
{activeTab === 'skills' && (
<>
<div className="skills-page__intro-row">
<p>
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 className="skills-page__empty">
Keine Fähigkeiten gefunden
</p>
</div>
) : (
Object.keys(skillsByCategory).sort().map(category => (
<div key={category} className="skills-page__category">
<h2 className="skills-page__category-title">
{category}
</h2>
<div className="skills-page__card-grid">
{skillsByCategory[category].map(skill => (
<div key={skill.id} className="card skills-page-card">
<div className="skills-page-card__head">
<h3 className="skills-page-card__title">{skill.name}</h3>
{skill.importance && (
<span className="skills-page-card__badge">
{skill.importance}/5
</span>
)}
</div>
{skill.description && (
<p className="skills-page-card__desc">
{skill.description}
</p>
)}
{isAdmin && (
<div className="skills-page-card__actions">
<button
type="button"
className="btn btn-secondary skills-page-card__grow"
onClick={() => handleEdit(skill, 'skill')}
>
Bearbeiten
</button>
<button
type="button"
className="btn btn-danger"
onClick={() => handleDelete(skill, 'skill')}
>
Löschen
</button>
</div>
)}
</div>
))}
</div>
</div>
))
)}
</>
)}
{/* Methods Tab */}
{activeTab === 'methods' && (
<>
<div className="skills-page__intro-row">
<p>
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 className="skills-page__empty">
Keine Trainingsmethoden gefunden
</p>
</div>
) : (
Object.keys(methodsByCategory).sort().map(category => (
<div key={category} className="skills-page__category">
<h2 className="skills-page__category-title">
{category}
</h2>
<div className="skills-page__card-grid skills-page__card-grid--methods">
{methodsByCategory[category].map(method => (
<div key={method.id} className="card skills-page-card">
<div className="skills-page-card__meta-block">
<h3 className="skills-page-card__title skills-page-card__title--method">
{method.name}
{method.abbreviation && (
<span className="skills-page-card__abbr">
({method.abbreviation})
</span>
)}
</h3>
<div className="skills-page-card__meta-row">
{method.typical_duration && (
<span className="skills-page-card__chip">
{method.typical_duration} min
</span>
)}
{method.typical_group_size && (
<span className="skills-page-card__chip">
👥 {method.typical_group_size}
</span>
)}
</div>
</div>
{method.description && (
<p className="skills-page-card__desc">
{method.description}
</p>
)}
{isAdmin && (
<div className="skills-page-card__actions">
<button
type="button"
className="btn btn-secondary skills-page-card__grow"
onClick={() => handleEdit(method, 'method')}
>
Bearbeiten
</button>
<button
type="button"
className="btn btn-danger"
onClick={() => handleDelete(method, 'method')}
>
Löschen
</button>
</div>
)}
</div>
))}
</div>
</div>
))
)}
</>
)}
{/* Modal */}
{showModal && isAdmin && (
<div
className="admin-modal-backdrop"
role="presentation"
onClick={(e) => {
if (e.target === e.currentTarget) setShowModal(false)
}}
>
<div
className="admin-modal-sheet skills-page-modal"
role="dialog"
aria-modal="true"
aria-labelledby="skills-page-modal-title"
onClick={(e) => e.stopPropagation()}
>
<div className="admin-modal-sheet__header">
<h2 id="skills-page-modal-title" className="admin-modal-sheet__title">
{editing
? (modalType === 'skill' ? 'Fähigkeit bearbeiten' : 'Methode bearbeiten')
: (modalType === 'skill' ? 'Neue Fähigkeit' : 'Neue Methode')
}
</h2>
<button
type="button"
className="btn btn-secondary admin-modal-sheet__close"
onClick={() => setShowModal(false)}
>
Schließen
</button>
</div>
<div className="admin-modal-sheet__body">
<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">Karate-Relevanz (Wiki)</label>
<textarea
className="form-input"
rows={3}
value={formData.karate_relevance || ''}
onChange={(e) => updateFormField('karate_relevance', e.target.value)}
/>
</div>
<div className="form-row">
<label className="form-label">Relevanzgrad (Wiki, 13)</label>
<select
className="form-input"
value={
formData.relevance_level === null ||
formData.relevance_level === undefined ||
formData.relevance_level === ''
? ''
: String(formData.relevance_level)
}
onChange={(e) =>
updateFormField('relevance_level', e.target.value === '' ? '' : e.target.value)
}
>
<option value=""> nicht gesetzt </option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</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 className="exercise-filters-modal-grid exercise-filters-modal-grid--two">
<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 className="skills-page-modal__footer">
<button type="submit" className="btn btn-primary skills-page-modal__submit">
{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