diff --git a/frontend/src/pages/AdminHierarchyPage.broken.jsx b/frontend/src/pages/AdminHierarchyPage.broken.jsx new file mode 100644 index 0000000..36b5631 --- /dev/null +++ b/frontend/src/pages/AdminHierarchyPage.broken.jsx @@ -0,0 +1,1235 @@ +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 })} + /> +
+
+ +