mitai-jinkendo/frontend/src/pages/AdminTrainingProfiles.jsx

308 lines
10 KiB
React
Raw Normal View History

import { useState, useEffect } from 'react'
import { api } from '../utils/api'
import '../app.css'
export default function AdminTrainingProfiles() {
const [stats, setStats] = useState(null)
const [trainingTypes, setTrainingTypes] = useState([])
const [templates, setTemplates] = useState([])
const [selectedType, setSelectedType] = useState(null)
const [editingProfile, setEditingProfile] = useState(null)
const [profileJson, setProfileJson] = useState('')
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
useEffect(() => {
load()
}, [])
const load = async () => {
try {
setLoading(true)
const [typesData, statsData, templatesData] = await Promise.all([
api.adminListTrainingTypes(),
api.getProfileStats(),
api.getProfileTemplates()
])
setTrainingTypes(typesData)
setStats(statsData)
setTemplates(templatesData)
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
const openEditor = (type) => {
setSelectedType(type)
setEditingProfile(type.profile || null)
setProfileJson(JSON.stringify(type.profile || {}, null, 2))
setError('')
setSuccess('')
}
const closeEditor = () => {
setSelectedType(null)
setEditingProfile(null)
setProfileJson('')
}
const saveProfile = async () => {
try {
// Validate JSON
const profile = JSON.parse(profileJson)
// Update training type
await api.adminUpdateTrainingType(selectedType.id, { profile })
setSuccess(`Profil für "${selectedType.name_de}" gespeichert`)
closeEditor()
load()
} catch (e) {
setError(e.message || 'Ungültiges JSON')
}
}
const applyTemplate = async (typeId, templateKey) => {
if (!confirm(`Template "${templateKey}" auf diesen Trainingstyp anwenden?`)) return
try {
await api.applyProfileTemplate(typeId, templateKey)
setSuccess('Template erfolgreich angewendet')
load()
} catch (e) {
setError(e.message)
}
}
const batchReEvaluate = async () => {
if (!confirm('Alle Aktivitäten neu evaluieren? Das kann einige Sekunden dauern.')) return
try {
const result = await api.batchEvaluateActivities()
let message = `Batch-Evaluation abgeschlossen: ${result.stats.evaluated} evaluiert, ` +
`${result.stats.skipped} übersprungen, ${result.stats.errors} Fehler`
// Show error details if available
if (result.stats.error_details && result.stats.error_details.length > 0) {
message += '\n\nErste Fehler:\n' + result.stats.error_details.map(err =>
`- Aktivität ${err.activity_id} (Typ: ${err.training_type_id || 'keine'}): ${err.error}`
).join('\n')
}
if (result.stats.errors > 0) {
setError(message)
} else {
setSuccess(message)
}
} catch (e) {
setError(e.message)
}
}
if (loading) return <div className="spinner" />
return (
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto', paddingBottom: '100px' }}>
<h1>Training Type Profiles</h1>
<p style={{ color: 'var(--text2)', marginBottom: '24px' }}>
Konfiguriere Bewertungsprofile für Trainingstypen
</p>
{error && (
<div style={{
padding: '12px',
background: 'var(--danger)',
color: 'white',
borderRadius: '8px',
marginBottom: '16px'
}}>
{error}
</div>
)}
{success && (
<div style={{
padding: '12px',
background: 'var(--accent)',
color: 'white',
borderRadius: '8px',
marginBottom: '16px'
}}>
{success}
</div>
)}
{/* Statistics */}
{stats && (
<div className="card" style={{ marginBottom: '24px' }}>
<h3>Übersicht</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px', marginTop: '16px' }}>
<div>
<div style={{ fontSize: '32px', fontWeight: '600', color: 'var(--accent)' }}>{stats.total}</div>
<div style={{ color: 'var(--text2)', fontSize: '14px' }}>Trainingstypen gesamt</div>
</div>
<div>
<div style={{ fontSize: '32px', fontWeight: '600', color: 'var(--accent)' }}>{stats.configured}</div>
<div style={{ color: 'var(--text2)', fontSize: '14px' }}>Profile konfiguriert</div>
</div>
<div>
<div style={{ fontSize: '32px', fontWeight: '600', color: 'var(--text3)' }}>{stats.unconfigured}</div>
<div style={{ color: 'var(--text2)', fontSize: '14px' }}>Noch keine Profile</div>
</div>
</div>
<button
onClick={batchReEvaluate}
className="btn btn-primary"
style={{ marginTop: '16px', width: '100%' }}
>
🔄 Alle Aktivitäten neu evaluieren
</button>
</div>
)}
{/* Training Types List */}
<div className="card">
<h3>Trainingstypen</h3>
<div style={{ marginTop: '16px' }}>
{trainingTypes.map(type => (
<div
key={type.id}
style={{
padding: '16px',
borderBottom: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
gap: '16px'
}}
>
<div style={{ fontSize: '24px' }}>{type.icon || '📊'}</div>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: '500', marginBottom: '4px' }}>
{type.name_de}
{type.profile && (
<span style={{
marginLeft: '8px',
padding: '2px 8px',
background: 'var(--accent)',
color: 'white',
borderRadius: '4px',
fontSize: '12px'
}}>
✓ Profil
</span>
)}
</div>
<div style={{ fontSize: '14px', color: 'var(--text2)' }}>
{type.category} {type.subcategory && `· ${type.subcategory}`}
</div>
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{/* Template Buttons */}
{templates.map(template => (
<button
key={template.key}
onClick={() => applyTemplate(type.id, template.key)}
className="btn"
style={{ padding: '6px 12px', fontSize: '13px' }}
title={`Template "${template.name_de}" anwenden`}
>
{template.icon} {template.name_de}
</button>
))}
<button
onClick={() => openEditor(type)}
className="btn btn-primary"
style={{ padding: '6px 16px' }}
>
{type.profile ? '✏️ Bearbeiten' : '➕ Profil erstellen'}
</button>
</div>
</div>
))}
</div>
</div>
{/* Profile Editor Modal */}
{selectedType && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px'
}}>
<div style={{
background: 'var(--bg)',
borderRadius: '12px',
maxWidth: '900px',
width: '100%',
maxHeight: '90vh',
overflow: 'auto',
padding: '24px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2>{selectedType.icon} {selectedType.name_de} - Profil bearbeiten</h2>
<button onClick={closeEditor} className="btn" style={{ padding: '8px 16px' }}>✕ Schließen</button>
</div>
<p style={{ color: 'var(--text2)', marginBottom: '16px' }}>
JSON-basierter Editor. Siehe Dokumentation für vollständige Struktur.
</p>
<textarea
value={profileJson}
onChange={(e) => setProfileJson(e.target.value)}
style={{
width: '100%',
minHeight: '400px',
padding: '12px',
fontFamily: 'monospace',
fontSize: '13px',
border: '1px solid var(--border)',
borderRadius: '8px',
background: 'var(--surface)',
color: 'var(--text1)',
resize: 'vertical'
}}
/>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
<button onClick={saveProfile} className="btn btn-primary" style={{ flex: 1 }}>
💾 Profil speichern
</button>
<button onClick={closeEditor} className="btn" style={{ flex: 1 }}>
Abbrechen
</button>
</div>
{selectedType.profile && (
<div style={{ marginTop: '16px', padding: '12px', background: 'var(--surface2)', borderRadius: '8px' }}>
<strong>Aktuelles Profil:</strong>
<div style={{ fontSize: '13px', color: 'var(--text2)', marginTop: '8px' }}>
Version: {selectedType.profile.version || 'n/a'}
<br />
Regel-Sets: {selectedType.profile.rule_sets ? Object.keys(selectedType.profile.rule_sets).join(', ') : 'keine'}
</div>
</div>
)}
</div>
</div>
)}
</div>
)
}