import React, { useState, useEffect } from 'react' import { api } from '../utils/api' /** * AdminHierarchyPage - Hierarchische Katalog-Verwaltung * * Layout: * ┌─ Tree (links) ─────────┬─ Detail Panel (rechts) ─┐ * │ ▼ Fokusbereich │ │ * │ ├─ Stilrichtungen │ [Edit-Form für │ * │ └─ Trainingstypen │ ausgewähltes Element] │ * └────────────────────────┴──────────────────────────┘ */ function AdminHierarchyPage() { const [activeTab, setActiveTab] = useState('hierarchy') const [hierarchy, setHierarchy] = useState([]) const [expandedNodes, setExpandedNodes] = useState(new Set()) const [selectedItem, setSelectedItem] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') // Global catalogs state const [targetGroups, setTargetGroups] = useState([]) const [skillCategories, setSkillCategories] = useState([]) const [trainingCharacters, setTrainingCharacters] = useState([]) // Matrix state const [styleDirections, setStyleDirections] = useState([]) const [assignments, setAssignments] = useState([]) useEffect(() => { if (activeTab === 'hierarchy') { loadHierarchy() } else if (activeTab === 'catalogs') { loadCatalogs() } else if (activeTab === 'assignments') { loadAssignments() } }, [activeTab]) async function loadHierarchy() { setLoading(true) setError('') try { const data = await api.getAdminHierarchy() setHierarchy(data) // Auto-expand first focus area if (data.length > 0) { setExpandedNodes(new Set([`fa-${data[0].id}`])) } } catch (e) { setError(e.message) } finally { setLoading(false) } } async function loadCatalogs() { setLoading(true) setError('') try { const [tgs, scs, tcs] = await Promise.all([ api.listTargetGroups(), api.listSkillCategories(), api.listTrainingCharacters() ]) setTargetGroups(tgs) setSkillCategories(scs) setTrainingCharacters(tcs) } catch (e) { setError(e.message) } finally { setLoading(false) } } async function loadAssignments() { setLoading(true) setError('') try { const [sds, tgs, assigns] = await Promise.all([ api.listStyleDirections(), api.listTargetGroups(), api.listStyleDirectionTargetGroups() ]) setStyleDirections(sds) setTargetGroups(tgs) setAssignments(assigns) } catch (e) { setError(e.message) } finally { setLoading(false) } } function toggleNode(nodeId) { const newExpanded = new Set(expandedNodes) if (newExpanded.has(nodeId)) { newExpanded.delete(nodeId) } else { newExpanded.add(nodeId) } setExpandedNodes(newExpanded) } function selectItem(item, type) { setSelectedItem({ ...item, _type: type }) } if (loading && hierarchy.length === 0) { return (
) } return ( <>
{/* Tabs */}
{/* Tab Content */} {activeTab === 'hierarchy' && ( { const newExpanded = new Set(expandedNodes) if (newExpanded.has(nodeId)) { newExpanded.delete(nodeId) } else { newExpanded.add(nodeId) } setExpandedNodes(newExpanded) }} onSelectItem={setSelectedItem} onUpdate={loadHierarchy} /> )} {activeTab === 'catalogs' && ( )} {activeTab === 'assignments' && ( )}
) } // ============================================================================ // Tab 1: Hierarchy // ============================================================================ function HierarchyTab({ hierarchy, expandedNodes, selectedItem, loading, error, onToggleNode, onSelectItem, onUpdate }) { if (loading && hierarchy.length === 0) { return
} return (
{/* Tree View */}

Katalog-Hierarchie

{error &&
{error}
} {hierarchy.map(fa => ( ))}
{/* Detail Panel */} {selectedItem && (
)}
) } // ============================================================================ // Tab 2: Global Catalogs // ============================================================================ function CatalogsTab({ targetGroups, skillCategories, trainingCharacters, loading, error, onUpdate }) { if (loading) { return
} return (
{error &&
{error}
}
) } // ============================================================================ // Tab 3: Assignments Matrix // ============================================================================ function AssignmentsTab({ styleDirections, targetGroups, assignments, loading, error, onUpdate }) { const [saving, setSaving] = useState(false) if (loading) { return
} async function toggleAssignment(styleDirectionId, targetGroupId) { const existing = assignments.find( a => a.style_direction_id === styleDirectionId && a.target_group_id === targetGroupId ) setSaving(true) try { if (existing) { await api.deleteStyleDirectionTargetGroup(existing.id) } else { await api.createStyleDirectionTargetGroup({ style_direction_id: styleDirectionId, target_group_id: targetGroupId, is_primary: false }) } onUpdate() } catch (e) { alert('Fehler: ' + e.message) } finally { setSaving(false) } } function isAssigned(styleDirectionId, targetGroupId) { return assignments.some( a => a.style_direction_id === styleDirectionId && a.target_group_id === targetGroupId ) } // Group style directions by focus area const groupedStyles = styleDirections.reduce((acc, sd) => { const key = sd.focus_area_name || 'Ohne Fokusbereich' if (!acc[key]) acc[key] = [] acc[key].push(sd) return acc }, {}) return (
{error &&
{error}
}

Stilrichtungen ↔ Zielgruppen

Ordne Zielgruppen den Stilrichtungen zu (mehrere möglich)

{targetGroups.map(tg => ( ))} {Object.entries(groupedStyles).map(([focusArea, styles]) => ( {styles.map(sd => ( {targetGroups.map(tg => ( ))} ))} ))}
Stilrichtung {tg.name}
{focusArea}
{sd.name} {sd.abbreviation && ({sd.abbreviation})} toggleAssignment(sd.id, tg.id)} disabled={saving} style={{ cursor: 'pointer', width: '20px', height: '20px' }} />
{styleDirections.length === 0 && (
Keine Stilrichtungen vorhanden. Erstelle zuerst Stilrichtungen in der Hierarchie.
)} {targetGroups.length === 0 && (
Keine Zielgruppen vorhanden. Erstelle zuerst Zielgruppen im Kataloge-Tab.
)}
) } // ============================================================================ // Tree Node Components // ============================================================================ function FocusAreaNode({ focusArea, expanded, onToggle, onSelect, selectedId, selectedType }) { const nodeId = `fa-${focusArea.id}` const isExpanded = expanded.has(nodeId) const isSelected = selectedType === 'focus_area' && selectedId === focusArea.id return (
{/* Focus Area Header */}
onSelect(focusArea, 'focus_area')} style={{ display: 'flex', alignItems: 'center', padding: '8px 12px', borderRadius: '8px', cursor: 'pointer', background: isSelected ? 'var(--accent)' : 'transparent', color: isSelected ? 'white' : 'var(--text1)', fontWeight: 600 }} > { e.stopPropagation(); onToggle(nodeId) }} style={{ marginRight: '8px', cursor: 'pointer', fontSize: '18px' }} > {isExpanded ? '▼' : '▶'} {focusArea.icon} {focusArea.name}
{/* Children: Style Directions + Training Types */} {isExpanded && (
{/* Style Directions Section */}
Stilrichtungen
{focusArea.style_directions && focusArea.style_directions.map(sd => ( ))}
{/* Training Types Section */}
Trainingstypen
{focusArea.training_types && focusArea.training_types.map(tt => ( ))}
)}
) } function StyleDirectionNode({ styleDirection, onSelect, isSelected }) { return (
onSelect(styleDirection, 'style_direction')} style={{ padding: '6px 12px', marginBottom: '4px', borderRadius: '6px', cursor: 'pointer', background: isSelected ? 'var(--accent)' : 'var(--surface2)', color: isSelected ? 'white' : 'var(--text1)', fontSize: '14px' }} > {styleDirection.name} {styleDirection.abbreviation && ( ({styleDirection.abbreviation}) )} {styleDirection.target_groups && styleDirection.target_groups.length > 0 && (
Zielgruppen: {styleDirection.target_groups.map(tg => tg.name).join(', ')}
)}
) } function TrainingTypeNode({ trainingType, onSelect, isSelected }) { return (
onSelect(trainingType, 'training_type')} style={{ padding: '6px 12px', marginBottom: '4px', borderRadius: '6px', cursor: 'pointer', background: isSelected ? 'var(--accent)' : 'var(--surface2)', color: isSelected ? 'white' : 'var(--text1)', fontSize: '14px' }} > {trainingType.name} {trainingType.abbreviation && ( ({trainingType.abbreviation}) )}
) } // ============================================================================ // Detail Panel (Edit Forms) // ============================================================================ function DetailPanel({ item, onUpdate, focusAreas }) { if (!item) return null const type = item._type if (type === 'focus_area') { return } else if (type === 'style_direction') { return } else if (type === 'training_type') { return } else if (type === 'create_style_direction') { return } else if (type === 'create_training_type') { return } return null } function FocusAreaDetail({ focusArea, onUpdate }) { const [editing, setEditing] = useState(false) const [form, setForm] = useState({ name: focusArea.name, icon: focusArea.icon, description: focusArea.description || '' }) async function handleSave() { try { await api.updateFocusArea(focusArea.id, form) setEditing(false) onUpdate() } catch (e) { alert('Fehler: ' + e.message) } } if (!editing) { return (

{focusArea.icon} {focusArea.name}

{focusArea.description &&

{focusArea.description}

}
) } return (

Fokusbereich bearbeiten

setForm({ ...form, name: e.target.value })} />
setForm({ ...form, icon: e.target.value })} />