- Shows first 10 errors with activity_id and error message - Helps admin debug evaluation failures - Errors shown in error box with details Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
308 lines
10 KiB
JavaScript
308 lines
10 KiB
JavaScript
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>
|
||
)
|
||
}
|