feat: Admin-managed exercise catalogs + frontend integration
Backend (already committed): - Migration 007: focus_areas, training_styles, training_characters, skill_categories tables - routers/catalogs.py: 20 CRUD endpoints for all catalogs - routers/exercises.py: Updated to support new FK fields - Trainer focus area assignment for role-based filtering Frontend (new): - AdminCatalogsPage: Comprehensive admin UI with 5 tabs - Focus Areas (with color + icon) - Training Styles (hierarchical with parent_style_id) - Training Characters - Skill Categories (hierarchical) - Trainer Assignments (trainer → focus area mapping) - ExercisesPage: Updated to use catalog dropdowns - Focus area dropdown now loads from API - Added missing Training Style dropdown - Training character dropdown now loads from API - Uses IDs instead of hard-coded text values - App.jsx: Added /admin/catalogs route - api.js: Added all catalog endpoints All form fields standardized: labels on top, full width, left-aligned Ready for testing via /admin/catalogs
This commit is contained in:
parent
74a92439eb
commit
c7cda03201
|
|
@ -10,6 +10,7 @@ import ExercisesPage from './pages/ExercisesPage'
|
||||||
import ClubsPage from './pages/ClubsPage'
|
import ClubsPage from './pages/ClubsPage'
|
||||||
import SkillsPage from './pages/SkillsPage'
|
import SkillsPage from './pages/SkillsPage'
|
||||||
import TrainingPlanningPage from './pages/TrainingPlanningPage'
|
import TrainingPlanningPage from './pages/TrainingPlanningPage'
|
||||||
|
import AdminCatalogsPage from './pages/AdminCatalogsPage'
|
||||||
import './app.css'
|
import './app.css'
|
||||||
|
|
||||||
// Bottom Navigation (Mobile)
|
// Bottom Navigation (Mobile)
|
||||||
|
|
@ -176,6 +177,14 @@ function AppRoutes() {
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/admin/catalogs"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AdminCatalogsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Catch all - redirect to dashboard or login */}
|
{/* Catch all - redirect to dashboard or login */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|
|
||||||
698
frontend/src/pages/AdminCatalogsPage.jsx
Normal file
698
frontend/src/pages/AdminCatalogsPage.jsx
Normal file
|
|
@ -0,0 +1,698 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { api } from '../utils/api'
|
||||||
|
|
||||||
|
export default function AdminCatalogsPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState('focus-areas')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
// Focus Areas
|
||||||
|
const [focusAreas, setFocusAreas] = useState([])
|
||||||
|
const [editingFA, setEditingFA] = useState(null)
|
||||||
|
const [newFA, setNewFA] = useState({ name: '', description: '', color: '#1D9E75', icon: '' })
|
||||||
|
|
||||||
|
// Training Styles
|
||||||
|
const [trainingStyles, setTrainingStyles] = useState([])
|
||||||
|
const [editingTS, setEditingTS] = useState(null)
|
||||||
|
const [newTS, setNewTS] = useState({ name: '', description: '', parent_style_id: null })
|
||||||
|
|
||||||
|
// Training Characters
|
||||||
|
const [trainingCharacters, setTrainingCharacters] = useState([])
|
||||||
|
const [editingTC, setEditingTC] = useState(null)
|
||||||
|
const [newTC, setNewTC] = useState({ name: '', description: '' })
|
||||||
|
|
||||||
|
// Skill Categories
|
||||||
|
const [skillCategories, setSkillCategories] = useState([])
|
||||||
|
const [editingSC, setEditingSC] = useState(null)
|
||||||
|
const [newSC, setNewSC] = useState({ name: '', description: '', parent_category_id: null })
|
||||||
|
|
||||||
|
// Trainer Focus Areas
|
||||||
|
const [trainerAssignments, setTrainerAssignments] = useState([])
|
||||||
|
const [profiles, setProfiles] = useState([])
|
||||||
|
const [newAssignment, setNewAssignment] = useState({ profile_id: '', focus_area_id: '' })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData()
|
||||||
|
}, [activeTab])
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
if (activeTab === 'focus-areas') {
|
||||||
|
const data = await api.listFocusAreas()
|
||||||
|
setFocusAreas(data)
|
||||||
|
} else if (activeTab === 'training-styles') {
|
||||||
|
const data = await api.listTrainingStyles()
|
||||||
|
setTrainingStyles(data)
|
||||||
|
} else if (activeTab === 'training-characters') {
|
||||||
|
const data = await api.listTrainingCharacters()
|
||||||
|
setTrainingCharacters(data)
|
||||||
|
} else if (activeTab === 'skill-categories') {
|
||||||
|
const data = await api.listSkillCategories()
|
||||||
|
setSkillCategories(data)
|
||||||
|
} else if (activeTab === 'trainer-assignments') {
|
||||||
|
const [assignments, profs, areas] = await Promise.all([
|
||||||
|
api.listTrainerFocusAreas(),
|
||||||
|
fetch('/api/profiles').then(r => r.json()),
|
||||||
|
api.listFocusAreas()
|
||||||
|
])
|
||||||
|
setTrainerAssignments(assignments)
|
||||||
|
setProfiles(profs)
|
||||||
|
setFocusAreas(areas)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus Areas
|
||||||
|
async function createFocusArea() {
|
||||||
|
try {
|
||||||
|
await api.createFocusArea(newFA)
|
||||||
|
setNewFA({ name: '', description: '', color: '#1D9E75', icon: '' })
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateFocusArea(id, data) {
|
||||||
|
try {
|
||||||
|
await api.updateFocusArea(id, data)
|
||||||
|
setEditingFA(null)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFocusArea(id) {
|
||||||
|
if (!confirm('Fokusbereich wirklich löschen?')) return
|
||||||
|
try {
|
||||||
|
await api.deleteFocusArea(id)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Training Styles
|
||||||
|
async function createTrainingStyle() {
|
||||||
|
try {
|
||||||
|
await api.createTrainingStyle(newTS)
|
||||||
|
setNewTS({ name: '', description: '', parent_style_id: null })
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTrainingStyle(id, data) {
|
||||||
|
try {
|
||||||
|
await api.updateTrainingStyle(id, data)
|
||||||
|
setEditingTS(null)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTrainingStyle(id) {
|
||||||
|
if (!confirm('Trainingsstil wirklich löschen?')) return
|
||||||
|
try {
|
||||||
|
await api.deleteTrainingStyle(id)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Training Characters
|
||||||
|
async function createTrainingCharacter() {
|
||||||
|
try {
|
||||||
|
await api.createTrainingCharacter(newTC)
|
||||||
|
setNewTC({ name: '', description: '' })
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTrainingCharacter(id, data) {
|
||||||
|
try {
|
||||||
|
await api.updateTrainingCharacter(id, data)
|
||||||
|
setEditingTC(null)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTrainingCharacter(id) {
|
||||||
|
if (!confirm('Trainingscharakter wirklich löschen?')) return
|
||||||
|
try {
|
||||||
|
await api.deleteTrainingCharacter(id)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill Categories
|
||||||
|
async function createSkillCategory() {
|
||||||
|
try {
|
||||||
|
await api.createSkillCategory(newSC)
|
||||||
|
setNewSC({ name: '', description: '', parent_category_id: null })
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSkillCategory(id, data) {
|
||||||
|
try {
|
||||||
|
await api.updateSkillCategory(id, data)
|
||||||
|
setEditingSC(null)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSkillCategory(id) {
|
||||||
|
if (!confirm('Fähigkeitskategorie wirklich löschen?')) return
|
||||||
|
try {
|
||||||
|
await api.deleteSkillCategory(id)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trainer Assignments
|
||||||
|
async function assignTrainer() {
|
||||||
|
try {
|
||||||
|
await api.assignTrainerFocusArea(newAssignment)
|
||||||
|
setNewAssignment({ profile_id: '', focus_area_id: '' })
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAssignment(id) {
|
||||||
|
if (!confirm('Zuordnung wirklich entfernen?')) return
|
||||||
|
try {
|
||||||
|
await api.deleteTrainerFocusArea(id)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '16px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||||
|
<h1 style={{ marginBottom: '24px' }}>Stammdaten-Kataloge</h1>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div style={{ display: 'flex', gap: '8px', borderBottom: '2px solid var(--border)', marginBottom: '24px', overflowX: 'auto' }}>
|
||||||
|
{[
|
||||||
|
{ id: 'focus-areas', label: 'Fokusbereiche' },
|
||||||
|
{ id: 'training-styles', label: 'Trainingsstile' },
|
||||||
|
{ id: 'training-characters', label: 'Trainingscharakter' },
|
||||||
|
{ id: 'skill-categories', label: 'Fähigkeitskategorien' },
|
||||||
|
{ id: 'trainer-assignments', label: 'Trainer-Zuordnungen' }
|
||||||
|
].map(tab => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className="btn"
|
||||||
|
style={{
|
||||||
|
borderBottom: activeTab === tab.id ? '3px solid var(--accent)' : 'none',
|
||||||
|
borderRadius: 0,
|
||||||
|
fontWeight: activeTab === tab.id ? 600 : 400,
|
||||||
|
color: activeTab === tab.id ? 'var(--accent)' : 'var(--text2)',
|
||||||
|
padding: '12px 16px',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{ padding: '12px', background: 'var(--danger)', color: 'white', borderRadius: '8px', marginBottom: '16px' }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="spinner" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Focus Areas */}
|
||||||
|
{activeTab === 'focus-areas' && (
|
||||||
|
<div>
|
||||||
|
<div className="card" style={{ marginBottom: '24px' }}>
|
||||||
|
<h3>Neuer Fokusbereich</h3>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={newFA.name}
|
||||||
|
onChange={e => setNewFA({ ...newFA, name: e.target.value })}
|
||||||
|
placeholder="z.B. Karate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={newFA.description}
|
||||||
|
onChange={e => setNewFA({ ...newFA, description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Farbe</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="form-input"
|
||||||
|
value={newFA.color}
|
||||||
|
onChange={e => setNewFA({ ...newFA, color: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Icon</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={newFA.icon}
|
||||||
|
onChange={e => setNewFA({ ...newFA, icon: e.target.value })}
|
||||||
|
placeholder="z.B. 🥋"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={createFocusArea}>Anlegen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gap: '16px' }}>
|
||||||
|
{focusAreas.map(fa => (
|
||||||
|
<div key={fa.id} className="card">
|
||||||
|
{editingFA?.id === fa.id ? (
|
||||||
|
<div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={editingFA.name}
|
||||||
|
onChange={e => setEditingFA({ ...editingFA, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={editingFA.description || ''}
|
||||||
|
onChange={e => setEditingFA({ ...editingFA, description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Farbe</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="form-input"
|
||||||
|
value={editingFA.color}
|
||||||
|
onChange={e => setEditingFA({ ...editingFA, color: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Icon</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={editingFA.icon || ''}
|
||||||
|
onChange={e => setEditingFA({ ...editingFA, icon: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button className="btn btn-primary" onClick={() => updateFocusArea(fa.id, editingFA)}>Speichern</button>
|
||||||
|
<button className="btn" onClick={() => setEditingFA(null)}>Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
|
||||||
|
<span style={{ fontSize: '24px' }}>{fa.icon}</span>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<h3 style={{ margin: 0 }}>{fa.name}</h3>
|
||||||
|
<p style={{ margin: '4px 0 0', color: 'var(--text2)', fontSize: '14px' }}>{fa.description}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: fa.color
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button className="btn" onClick={() => setEditingFA(fa)}>Bearbeiten</button>
|
||||||
|
<button className="btn" onClick={() => deleteFocusArea(fa.id)}>Löschen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Training Styles */}
|
||||||
|
{activeTab === 'training-styles' && (
|
||||||
|
<div>
|
||||||
|
<div className="card" style={{ marginBottom: '24px' }}>
|
||||||
|
<h3>Neuer Trainingsstil</h3>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={newTS.name}
|
||||||
|
onChange={e => setNewTS({ ...newTS, name: e.target.value })}
|
||||||
|
placeholder="z.B. Shotokan"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={newTS.description}
|
||||||
|
onChange={e => setNewTS({ ...newTS, description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Übergeordneter Stil (optional)</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={newTS.parent_style_id || ''}
|
||||||
|
onChange={e => setNewTS({ ...newTS, parent_style_id: e.target.value ? parseInt(e.target.value) : null })}
|
||||||
|
>
|
||||||
|
<option value="">- Kein -</option>
|
||||||
|
{trainingStyles.map(ts => (
|
||||||
|
<option key={ts.id} value={ts.id}>{ts.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={createTrainingStyle}>Anlegen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gap: '16px' }}>
|
||||||
|
{trainingStyles.map(ts => (
|
||||||
|
<div key={ts.id} className="card">
|
||||||
|
{editingTS?.id === ts.id ? (
|
||||||
|
<div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={editingTS.name}
|
||||||
|
onChange={e => setEditingTS({ ...editingTS, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={editingTS.description || ''}
|
||||||
|
onChange={e => setEditingTS({ ...editingTS, description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Übergeordneter Stil</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={editingTS.parent_style_id || ''}
|
||||||
|
onChange={e => setEditingTS({ ...editingTS, parent_style_id: e.target.value ? parseInt(e.target.value) : null })}
|
||||||
|
>
|
||||||
|
<option value="">- Kein -</option>
|
||||||
|
{trainingStyles.filter(x => x.id !== ts.id).map(x => (
|
||||||
|
<option key={x.id} value={x.id}>{x.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button className="btn btn-primary" onClick={() => updateTrainingStyle(ts.id, editingTS)}>Speichern</button>
|
||||||
|
<button className="btn" onClick={() => setEditingTS(null)}>Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<h3>{ts.name}</h3>
|
||||||
|
{ts.parent_style_name && (
|
||||||
|
<p style={{ margin: '4px 0', color: 'var(--text2)', fontSize: '14px' }}>
|
||||||
|
→ {ts.parent_style_name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p style={{ margin: '8px 0', color: 'var(--text2)' }}>{ts.description}</p>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button className="btn" onClick={() => setEditingTS(ts)}>Bearbeiten</button>
|
||||||
|
<button className="btn" onClick={() => deleteTrainingStyle(ts.id)}>Löschen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Training Characters */}
|
||||||
|
{activeTab === 'training-characters' && (
|
||||||
|
<div>
|
||||||
|
<div className="card" style={{ marginBottom: '24px' }}>
|
||||||
|
<h3>Neuer Trainingscharakter</h3>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={newTC.name}
|
||||||
|
onChange={e => setNewTC({ ...newTC, name: e.target.value })}
|
||||||
|
placeholder="z.B. Technisch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={newTC.description}
|
||||||
|
onChange={e => setNewTC({ ...newTC, description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={createTrainingCharacter}>Anlegen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gap: '16px' }}>
|
||||||
|
{trainingCharacters.map(tc => (
|
||||||
|
<div key={tc.id} className="card">
|
||||||
|
{editingTC?.id === tc.id ? (
|
||||||
|
<div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={editingTC.name}
|
||||||
|
onChange={e => setEditingTC({ ...editingTC, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={editingTC.description || ''}
|
||||||
|
onChange={e => setEditingTC({ ...editingTC, description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button className="btn btn-primary" onClick={() => updateTrainingCharacter(tc.id, editingTC)}>Speichern</button>
|
||||||
|
<button className="btn" onClick={() => setEditingTC(null)}>Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<h3>{tc.name}</h3>
|
||||||
|
<p style={{ margin: '8px 0', color: 'var(--text2)' }}>{tc.description}</p>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button className="btn" onClick={() => setEditingTC(tc)}>Bearbeiten</button>
|
||||||
|
<button className="btn" onClick={() => deleteTrainingCharacter(tc.id)}>Löschen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Skill Categories */}
|
||||||
|
{activeTab === 'skill-categories' && (
|
||||||
|
<div>
|
||||||
|
<div className="card" style={{ marginBottom: '24px' }}>
|
||||||
|
<h3>Neue Fähigkeitskategorie</h3>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={newSC.name}
|
||||||
|
onChange={e => setNewSC({ ...newSC, name: e.target.value })}
|
||||||
|
placeholder="z.B. Technik"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={newSC.description}
|
||||||
|
onChange={e => setNewSC({ ...newSC, description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Übergeordnete Kategorie (optional)</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={newSC.parent_category_id || ''}
|
||||||
|
onChange={e => setNewSC({ ...newSC, parent_category_id: e.target.value ? parseInt(e.target.value) : null })}
|
||||||
|
>
|
||||||
|
<option value="">- Keine -</option>
|
||||||
|
{skillCategories.map(sc => (
|
||||||
|
<option key={sc.id} value={sc.id}>{sc.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={createSkillCategory}>Anlegen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gap: '16px' }}>
|
||||||
|
{skillCategories.map(sc => (
|
||||||
|
<div key={sc.id} className="card">
|
||||||
|
{editingSC?.id === sc.id ? (
|
||||||
|
<div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={editingSC.name}
|
||||||
|
onChange={e => setEditingSC({ ...editingSC, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={editingSC.description || ''}
|
||||||
|
onChange={e => setEditingSC({ ...editingSC, description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Übergeordnete Kategorie</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={editingSC.parent_category_id || ''}
|
||||||
|
onChange={e => setEditingSC({ ...editingSC, parent_category_id: e.target.value ? parseInt(e.target.value) : null })}
|
||||||
|
>
|
||||||
|
<option value="">- Keine -</option>
|
||||||
|
{skillCategories.filter(x => x.id !== sc.id).map(x => (
|
||||||
|
<option key={x.id} value={x.id}>{x.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button className="btn btn-primary" onClick={() => updateSkillCategory(sc.id, editingSC)}>Speichern</button>
|
||||||
|
<button className="btn" onClick={() => setEditingSC(null)}>Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<h3>{sc.name}</h3>
|
||||||
|
{sc.parent_category_name && (
|
||||||
|
<p style={{ margin: '4px 0', color: 'var(--text2)', fontSize: '14px' }}>
|
||||||
|
→ {sc.parent_category_name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p style={{ margin: '8px 0', color: 'var(--text2)' }}>{sc.description}</p>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button className="btn" onClick={() => setEditingSC(sc)}>Bearbeiten</button>
|
||||||
|
<button className="btn" onClick={() => deleteSkillCategory(sc.id)}>Löschen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Trainer Assignments */}
|
||||||
|
{activeTab === 'trainer-assignments' && (
|
||||||
|
<div>
|
||||||
|
<div className="card" style={{ marginBottom: '24px' }}>
|
||||||
|
<h3>Neue Zuordnung</h3>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Trainer</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={newAssignment.profile_id}
|
||||||
|
onChange={e => setNewAssignment({ ...newAssignment, profile_id: parseInt(e.target.value) })}
|
||||||
|
>
|
||||||
|
<option value="">- Trainer auswählen -</option>
|
||||||
|
{profiles.filter(p => p.role === 'trainer' || p.role === 'admin').map(p => (
|
||||||
|
<option key={p.id} value={p.id}>{p.name} ({p.email})</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Fokusbereich</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={newAssignment.focus_area_id}
|
||||||
|
onChange={e => setNewAssignment({ ...newAssignment, focus_area_id: parseInt(e.target.value) })}
|
||||||
|
>
|
||||||
|
<option value="">- Fokusbereich auswählen -</option>
|
||||||
|
{focusAreas.map(fa => (
|
||||||
|
<option key={fa.id} value={fa.id}>{fa.icon} {fa.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={assignTrainer}>Zuordnen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gap: '16px' }}>
|
||||||
|
{trainerAssignments.map(ta => (
|
||||||
|
<div key={ta.id} className="card">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<div>
|
||||||
|
<h3 style={{ margin: 0 }}>{ta.trainer_name}</h3>
|
||||||
|
<p style={{ margin: '4px 0', color: 'var(--text2)', fontSize: '14px' }}>
|
||||||
|
{ta.focus_area_icon} {ta.focus_area_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button className="btn" onClick={() => removeAssignment(ta.id)}>Entfernen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,9 @@ import api from '../utils/api'
|
||||||
function ExercisesPage() {
|
function ExercisesPage() {
|
||||||
const [exercises, setExercises] = useState([])
|
const [exercises, setExercises] = useState([])
|
||||||
const [skills, setSkills] = useState([])
|
const [skills, setSkills] = useState([])
|
||||||
|
const [focusAreas, setFocusAreas] = useState([])
|
||||||
|
const [trainingStyles, setTrainingStyles] = useState([])
|
||||||
|
const [trainingCharacters, setTrainingCharacters] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
const [editingExercise, setEditingExercise] = useState(null)
|
const [editingExercise, setEditingExercise] = useState(null)
|
||||||
|
|
@ -28,8 +31,11 @@ function ExercisesPage() {
|
||||||
group_size_max: '',
|
group_size_max: '',
|
||||||
age_groups: [],
|
age_groups: [],
|
||||||
focus_area: '',
|
focus_area: '',
|
||||||
|
focus_area_id: null,
|
||||||
secondary_areas: [],
|
secondary_areas: [],
|
||||||
|
training_style_id: null,
|
||||||
training_character: '',
|
training_character: '',
|
||||||
|
training_character_id: null,
|
||||||
visibility: 'private',
|
visibility: 'private',
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
skills: []
|
skills: []
|
||||||
|
|
@ -41,12 +47,18 @@ function ExercisesPage() {
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const [exercisesData, skillsData] = await Promise.all([
|
const [exercisesData, skillsData, focusAreasData, stylesData, charactersData] = await Promise.all([
|
||||||
api.listExercises(filters),
|
api.listExercises(filters),
|
||||||
api.listSkills()
|
api.listSkills(),
|
||||||
|
api.listFocusAreas(),
|
||||||
|
api.listTrainingStyles(),
|
||||||
|
api.listTrainingCharacters()
|
||||||
])
|
])
|
||||||
setExercises(exercisesData)
|
setExercises(exercisesData)
|
||||||
setSkills(skillsData)
|
setSkills(skillsData)
|
||||||
|
setFocusAreas(focusAreasData)
|
||||||
|
setTrainingStyles(stylesData)
|
||||||
|
setTrainingCharacters(charactersData)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load data:', err)
|
console.error('Failed to load data:', err)
|
||||||
alert('Fehler beim Laden: ' + err.message)
|
alert('Fehler beim Laden: ' + err.message)
|
||||||
|
|
@ -71,8 +83,11 @@ function ExercisesPage() {
|
||||||
group_size_max: '',
|
group_size_max: '',
|
||||||
age_groups: [],
|
age_groups: [],
|
||||||
focus_area: '',
|
focus_area: '',
|
||||||
|
focus_area_id: null,
|
||||||
secondary_areas: [],
|
secondary_areas: [],
|
||||||
|
training_style_id: null,
|
||||||
training_character: '',
|
training_character: '',
|
||||||
|
training_character_id: null,
|
||||||
visibility: 'private',
|
visibility: 'private',
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
skills: []
|
skills: []
|
||||||
|
|
@ -96,8 +111,11 @@ function ExercisesPage() {
|
||||||
group_size_max: exercise.group_size_max || '',
|
group_size_max: exercise.group_size_max || '',
|
||||||
age_groups: exercise.age_groups || [],
|
age_groups: exercise.age_groups || [],
|
||||||
focus_area: exercise.focus_area || '',
|
focus_area: exercise.focus_area || '',
|
||||||
|
focus_area_id: exercise.focus_area_id || null,
|
||||||
secondary_areas: exercise.secondary_areas || [],
|
secondary_areas: exercise.secondary_areas || [],
|
||||||
|
training_style_id: exercise.training_style_id || null,
|
||||||
training_character: exercise.training_character || '',
|
training_character: exercise.training_character || '',
|
||||||
|
training_character_id: exercise.training_character_id || null,
|
||||||
visibility: exercise.visibility || 'private',
|
visibility: exercise.visibility || 'private',
|
||||||
status: exercise.status || 'draft',
|
status: exercise.status || 'draft',
|
||||||
skills: exercise.skills?.map(s => ({
|
skills: exercise.skills?.map(s => ({
|
||||||
|
|
@ -195,9 +213,11 @@ function ExercisesPage() {
|
||||||
onChange={(e) => setFilters({ ...filters, focus_area: e.target.value })}
|
onChange={(e) => setFilters({ ...filters, focus_area: e.target.value })}
|
||||||
>
|
>
|
||||||
<option value="">Alle</option>
|
<option value="">Alle</option>
|
||||||
<option value="karate">Karate</option>
|
{focusAreas.map(fa => (
|
||||||
<option value="selbstverteidigung">Selbstverteidigung</option>
|
<option key={fa.id} value={fa.id}>
|
||||||
<option value="gewaltschutz">Gewaltschutz</option>
|
{fa.icon} {fa.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -451,13 +471,32 @@ function ExercisesPage() {
|
||||||
<label className="form-label">Fokusbereich</label>
|
<label className="form-label">Fokusbereich</label>
|
||||||
<select
|
<select
|
||||||
className="form-input"
|
className="form-input"
|
||||||
value={formData.focus_area}
|
value={formData.focus_area_id || ''}
|
||||||
onChange={(e) => updateFormField('focus_area', e.target.value)}
|
onChange={(e) => updateFormField('focus_area_id', e.target.value ? parseInt(e.target.value) : null)}
|
||||||
>
|
>
|
||||||
<option value="">Bitte wählen</option>
|
<option value="">Bitte wählen</option>
|
||||||
<option value="karate">Karate</option>
|
{focusAreas.map(fa => (
|
||||||
<option value="selbstverteidigung">Selbstverteidigung</option>
|
<option key={fa.id} value={fa.id}>
|
||||||
<option value="gewaltschutz">Gewaltschutz</option>
|
{fa.icon} {fa.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Trainingsstil</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={formData.training_style_id || ''}
|
||||||
|
onChange={(e) => updateFormField('training_style_id', e.target.value ? parseInt(e.target.value) : null)}
|
||||||
|
>
|
||||||
|
<option value="">Bitte wählen</option>
|
||||||
|
{trainingStyles.map(ts => (
|
||||||
|
<option key={ts.id} value={ts.id}>
|
||||||
|
{ts.name}
|
||||||
|
{ts.parent_style_name ? ` (${ts.parent_style_name})` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -465,15 +504,15 @@ function ExercisesPage() {
|
||||||
<label className="form-label">Trainingscharakter</label>
|
<label className="form-label">Trainingscharakter</label>
|
||||||
<select
|
<select
|
||||||
className="form-input"
|
className="form-input"
|
||||||
value={formData.training_character}
|
value={formData.training_character_id || ''}
|
||||||
onChange={(e) => updateFormField('training_character', e.target.value)}
|
onChange={(e) => updateFormField('training_character_id', e.target.value ? parseInt(e.target.value) : null)}
|
||||||
>
|
>
|
||||||
<option value="">Bitte wählen</option>
|
<option value="">Bitte wählen</option>
|
||||||
<option value="grundlage">Grundlage</option>
|
{trainingCharacters.map(tc => (
|
||||||
<option value="aufbau">Aufbau</option>
|
<option key={tc.id} value={tc.id}>
|
||||||
<option value="vertiefung">Vertiefung</option>
|
{tc.name}
|
||||||
<option value="festigung">Festigung</option>
|
</option>
|
||||||
<option value="diagnose">Diagnose</option>
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,123 @@ export async function deleteExercise(id) {
|
||||||
return request(`/api/exercises/${id}`, { method: 'DELETE' })
|
return request(`/api/exercises/${id}`, { method: 'DELETE' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Catalogs (Admin-verwaltbare Stammdaten)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Focus Areas
|
||||||
|
export async function listFocusAreas(filters = {}) {
|
||||||
|
const query = new URLSearchParams(filters).toString()
|
||||||
|
return request(`/api/focus-areas${query ? '?' + query : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFocusArea(data) {
|
||||||
|
return request('/api/focus-areas', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateFocusArea(id, data) {
|
||||||
|
return request(`/api/focus-areas/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFocusArea(id) {
|
||||||
|
return request(`/api/focus-areas/${id}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Training Styles
|
||||||
|
export async function listTrainingStyles(filters = {}) {
|
||||||
|
const query = new URLSearchParams(filters).toString()
|
||||||
|
return request(`/api/training-styles${query ? '?' + query : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTrainingStyle(data) {
|
||||||
|
return request('/api/training-styles', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTrainingStyle(id, data) {
|
||||||
|
return request(`/api/training-styles/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTrainingStyle(id) {
|
||||||
|
return request(`/api/training-styles/${id}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Training Characters
|
||||||
|
export async function listTrainingCharacters(filters = {}) {
|
||||||
|
const query = new URLSearchParams(filters).toString()
|
||||||
|
return request(`/api/training-characters${query ? '?' + query : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTrainingCharacter(data) {
|
||||||
|
return request('/api/training-characters', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTrainingCharacter(id, data) {
|
||||||
|
return request(`/api/training-characters/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTrainingCharacter(id) {
|
||||||
|
return request(`/api/training-characters/${id}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill Categories
|
||||||
|
export async function listSkillCategories(filters = {}) {
|
||||||
|
const query = new URLSearchParams(filters).toString()
|
||||||
|
return request(`/api/skill-categories${query ? '?' + query : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSkillCategory(data) {
|
||||||
|
return request('/api/skill-categories', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSkillCategory(id, data) {
|
||||||
|
return request(`/api/skill-categories/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSkillCategory(id) {
|
||||||
|
return request(`/api/skill-categories/${id}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trainer Focus Areas
|
||||||
|
export async function listTrainerFocusAreas(filters = {}) {
|
||||||
|
const query = new URLSearchParams(filters).toString()
|
||||||
|
return request(`/api/trainer-focus-areas${query ? '?' + query : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assignTrainerFocusArea(data) {
|
||||||
|
return request('/api/trainer-focus-areas', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTrainerFocusArea(id) {
|
||||||
|
return request(`/api/trainer-focus-areas/${id}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Training Planning
|
// Training Planning
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -329,6 +446,27 @@ export const api = {
|
||||||
deleteTrainingUnit,
|
deleteTrainingUnit,
|
||||||
quickCreateTrainingUnit,
|
quickCreateTrainingUnit,
|
||||||
|
|
||||||
|
// Catalogs
|
||||||
|
listFocusAreas,
|
||||||
|
createFocusArea,
|
||||||
|
updateFocusArea,
|
||||||
|
deleteFocusArea,
|
||||||
|
listTrainingStyles,
|
||||||
|
createTrainingStyle,
|
||||||
|
updateTrainingStyle,
|
||||||
|
deleteTrainingStyle,
|
||||||
|
listTrainingCharacters,
|
||||||
|
createTrainingCharacter,
|
||||||
|
updateTrainingCharacter,
|
||||||
|
deleteTrainingCharacter,
|
||||||
|
listSkillCategories,
|
||||||
|
createSkillCategory,
|
||||||
|
updateSkillCategory,
|
||||||
|
deleteSkillCategory,
|
||||||
|
listTrainerFocusAreas,
|
||||||
|
assignTrainerFocusArea,
|
||||||
|
deleteTrainerFocusArea,
|
||||||
|
|
||||||
// System
|
// System
|
||||||
getVersion,
|
getVersion,
|
||||||
healthCheck
|
healthCheck
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user