mitai-jinkendo/frontend/src/pages/AdminTrainingProfiles.jsx
Lars d07baa260c
Some checks failed
Deploy Development / deploy (push) Successful in 48s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Has been cancelled
feat: display batch evaluation error details in UI
- 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>
2026-03-23 13:24:29 +01:00

308 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { 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>
)
}