v9d Phase 2d: Vitals Module Refactoring (Baseline + Blood Pressure) #22
|
|
@ -29,6 +29,7 @@ import AdminCouponsPage from './pages/AdminCouponsPage'
|
|||
import AdminUserRestrictionsPage from './pages/AdminUserRestrictionsPage'
|
||||
import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage'
|
||||
import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage'
|
||||
import AdminTrainingProfiles from './pages/AdminTrainingProfiles'
|
||||
import SubscriptionPage from './pages/SubscriptionPage'
|
||||
import SleepPage from './pages/SleepPage'
|
||||
import RestDaysPage from './pages/RestDaysPage'
|
||||
|
|
@ -180,6 +181,7 @@ function AppShell() {
|
|||
<Route path="/admin/user-restrictions" element={<AdminUserRestrictionsPage/>}/>
|
||||
<Route path="/admin/training-types" element={<AdminTrainingTypesPage/>}/>
|
||||
<Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/>
|
||||
<Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/>
|
||||
<Route path="/subscription" element={<SubscriptionPage/>}/>
|
||||
</Routes>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -444,6 +444,11 @@ export default function AdminPanel() {
|
|||
🔗 Activity-Mappings (lernendes System)
|
||||
</button>
|
||||
</Link>
|
||||
<Link to="/admin/training-profiles">
|
||||
<button className="btn btn-secondary btn-full">
|
||||
⭐ Training Type Profiles (#15)
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
297
frontend/src/pages/AdminTrainingProfiles.jsx
Normal file
297
frontend/src/pages/AdminTrainingProfiles.jsx
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
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.get('/admin/training-types'),
|
||||
api.get('/admin/training-types/profiles/stats'),
|
||||
api.get('/admin/training-types/profiles/templates')
|
||||
])
|
||||
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.put(`/admin/training-types/${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.post(`/admin/training-types/${typeId}/profile/apply-template`, {
|
||||
template_key: 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.post('/evaluation/batch')
|
||||
setSuccess(
|
||||
`Batch-Evaluation abgeschlossen: ${result.stats.evaluated} evaluiert, ` +
|
||||
`${result.stats.skipped} übersprungen, ${result.stats.errors} Fehler`
|
||||
)
|
||||
} 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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user