diff --git a/frontend/src/components/admin/AssignmentsTab.jsx b/frontend/src/components/admin/AssignmentsTab.jsx new file mode 100644 index 0000000..83e00b6 --- /dev/null +++ b/frontend/src/components/admin/AssignmentsTab.jsx @@ -0,0 +1,160 @@ +import React, { useState } from 'react' +import { api } from '../../utils/api' + +function AssignmentsTab({ styleDirections, targetGroups, assignments, loading, error, onUpdate }) { + const [saving, setSaving] = useState(false) + + if (loading) { + return
+ } + + async function toggleAssignment(styleDirectionId, targetGroupId, currentlyAssigned) { + setSaving(true) + try { + if (currentlyAssigned) { + // Find and delete the assignment + const assignment = assignments.find( + a => a.style_direction_id === styleDirectionId && a.target_group_id === targetGroupId + ) + if (assignment) { + await api.deleteStyleDirectionTargetGroup(assignment.id) + } + } else { + // Create new assignment + 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 ( +
+

Zuordnungen: Stilrichtungen ↔ Zielgruppen

+ {error &&
{error}
} + + {targetGroups.length === 0 && ( +
+ Keine Zielgruppen vorhanden. Bitte erst im Tab "Kataloge" anlegen. +
+ )} + + {styleDirections.length === 0 && ( +
+ Keine Stilrichtungen vorhanden. Bitte erst im Tab "Hierarchie" anlegen. +
+ )} + + {targetGroups.length > 0 && styleDirections.length > 0 && ( +
+ + + + + {targetGroups.map(tg => ( + + ))} + + + + {Object.entries(groupedStyles).map(([focusAreaName, styles]) => ( + + + + + {styles.map(sd => ( + + + {targetGroups.map(tg => { + const assigned = isAssigned(sd.id, tg.id) + return ( + + ) + })} + + ))} + + ))} + +
Stilrichtung + {tg.name} +
+ {focusAreaName} +
+ {sd.name} + + toggleAssignment(sd.id, tg.id, assigned)} + disabled={saving} + style={{ width: '20px', height: '20px', cursor: 'pointer' }} + /> +
+
+ )} + + +
+ ) +} + +export default AssignmentsTab diff --git a/frontend/src/components/admin/CatalogsTab.jsx b/frontend/src/components/admin/CatalogsTab.jsx new file mode 100644 index 0000000..1ffc4ff --- /dev/null +++ b/frontend/src/components/admin/CatalogsTab.jsx @@ -0,0 +1,194 @@ +import React, { useState } from 'react' +import { api } from '../../utils/api' + +function CatalogsTab({ targetGroups, skillCategories, trainingCharacters, loading, error, onUpdate }) { + if (loading) { + return
+ } + + return ( +
+ {error &&
{error}
} + + + + + + +
+ ) +} + +function CatalogSection({ title, icon, items, onUpdate, createFn, updateFn, deleteFn, fields }) { + const [creating, setCreating] = useState(false) + const [editing, setEditing] = useState(null) + const [form, setForm] = useState({}) + + function startCreate() { + const emptyForm = {} + fields.forEach(f => { emptyForm[f.key] = '' }) + setForm(emptyForm) + setCreating(true) + } + + function startEdit(item) { + const editForm = {} + fields.forEach(f => { editForm[f.key] = item[f.key] || '' }) + setEditing(item.id) + setForm(editForm) + } + + async function handleCreate() { + const required = fields.filter(f => f.required) + for (const field of required) { + if (!form[field.key]) { + alert(`${field.label} ist erforderlich`) + return + } + } + try { + await createFn(form) + setCreating(false) + setForm({}) + onUpdate() + } catch (e) { + alert('Fehler: ' + e.message) + } + } + + async function handleUpdate(id) { + try { + await updateFn(id, form) + setEditing(null) + setForm({}) + onUpdate() + } catch (e) { + alert('Fehler: ' + e.message) + } + } + + async function handleDelete(id, name) { + if (!confirm(`"${name}" wirklich löschen?`)) return + try { + await deleteFn(id) + onUpdate() + } catch (e) { + alert('Fehler: ' + e.message) + } + } + + return ( +
+
+

{icon} {title}

+ +
+ + {creating && ( +
+

Neu erstellen

+ {fields.map(field => ( +
+ + {field.type === 'textarea' ? ( +