feat: complete admin hierarchy - edit/delete + responsive design
Some checks failed
Deploy Development / deploy (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 1m54s

Features:
- Edit forms for Style Directions and Training Types (working)
- Delete functions with confirmation dialogs
- Responsive layout: mobile (stacked), tablet/desktop (side-by-side)
- Back button on mobile to return to tree view
- Full CRUD except create (can use old catalogs page for now)

Mobile UX:
- Tree view fills screen
- Click item → detail panel replaces tree
- Back button → return to tree
- Safe bottom padding for navigation

Desktop UX:
- 400px tree + fluid detail panel
- Both always visible
- No back button (not needed)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-04-24 08:33:11 +02:00
parent 599d696321
commit af89546022

View File

@ -63,55 +63,84 @@ function AdminHierarchyPage() {
}
return (
<div style={{
display: 'grid',
gridTemplateColumns: '400px 1fr',
gap: '16px',
padding: '16px',
height: 'calc(100vh - 100px)',
overflow: 'hidden'
}}>
{/* Left: Tree View */}
<div style={{
overflowY: 'auto',
border: '1px solid var(--border)',
borderRadius: '12px',
padding: '16px',
background: 'var(--surface)'
}}>
<h2 style={{ marginTop: 0 }}>Katalog-Hierarchie</h2>
{error && <div style={{ color: 'var(--danger)', marginBottom: '16px' }}>{error}</div>}
<>
<style>{`
.admin-hierarchy-container {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
padding: 16px;
padding-bottom: 80px;
}
{hierarchy.map(fa => (
<FocusAreaNode
key={fa.id}
focusArea={fa}
expanded={expandedNodes}
onToggle={toggleNode}
onSelect={selectItem}
selectedId={selectedItem?.id}
selectedType={selectedItem?._type}
/>
))}
</div>
@media (min-width: 768px) {
.admin-hierarchy-container {
grid-template-columns: 400px 1fr;
}
{/* Right: Detail Panel */}
<div style={{
overflowY: 'auto',
border: '1px solid var(--border)',
borderRadius: '12px',
padding: '20px',
background: 'var(--surface)'
}}>
{selectedItem ? (
<DetailPanel item={selectedItem} onUpdate={loadHierarchy} />
) : (
<div style={{ textAlign: 'center', color: 'var(--text2)', paddingTop: '60px' }}>
<p> Wähle ein Element aus dem Baum</p>
.admin-tree-view {
display: block !important;
}
.admin-back-button {
display: none !important;
}
}
.admin-tree-view {
${selectedItem ? 'display: none;' : 'display: block;'}
}
`}</style>
<div className="admin-hierarchy-container">
{/* Tree View */}
<div
className="admin-tree-view"
style={{
border: '1px solid var(--border)',
borderRadius: '12px',
padding: '16px',
background: 'var(--surface)'
}}
>
<h2 style={{ marginTop: 0 }}>Katalog-Hierarchie</h2>
{error && <div style={{ color: 'var(--danger)', marginBottom: '16px' }}>{error}</div>}
{hierarchy.map(fa => (
<FocusAreaNode
key={fa.id}
focusArea={fa}
expanded={expandedNodes}
onToggle={toggleNode}
onSelect={selectItem}
selectedId={selectedItem?.id}
selectedType={selectedItem?._type}
/>
))}
</div>
{/* Detail Panel */}
{selectedItem && (
<div
style={{
border: '1px solid var(--border)',
borderRadius: '12px',
padding: '20px',
background: 'var(--surface)'
}}
>
<button
className="btn admin-back-button"
onClick={() => setSelectedItem(null)}
style={{ marginBottom: '16px' }}
>
Zurück zur Übersicht
</button>
<DetailPanel item={selectedItem} onUpdate={loadHierarchy} />
</div>
)}
</div>
</div>
</>
)
}
@ -332,42 +361,174 @@ function FocusAreaDetail({ focusArea, onUpdate }) {
}
function StyleDirectionDetail({ styleDirection, onUpdate }) {
const [editing, setEditing] = useState(false)
const [form, setForm] = useState({
name: styleDirection.name,
abbreviation: styleDirection.abbreviation || '',
description: styleDirection.description || '',
focus_area_id: styleDirection.focus_area_id
})
async function handleSave() {
try {
await api.updateStyleDirection(styleDirection.id, form)
setEditing(false)
onUpdate()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
async function handleDelete() {
if (!confirm(`Stilrichtung "${styleDirection.name}" wirklich löschen?`)) return
try {
await api.deleteStyleDirection(styleDirection.id)
onUpdate()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
if (!editing) {
return (
<div>
<h2>{styleDirection.name}</h2>
{styleDirection.abbreviation && <p style={{ color: 'var(--text2)' }}>Kürzel: {styleDirection.abbreviation}</p>}
{styleDirection.description && <p style={{ color: 'var(--text2)' }}>{styleDirection.description}</p>}
{styleDirection.target_groups && styleDirection.target_groups.length > 0 && (
<div style={{ marginTop: '16px' }}>
<h3>Zugeordnete Zielgruppen</h3>
<ul>
{styleDirection.target_groups.map(tg => (
<li key={tg.id}>
{tg.name} {tg.is_primary && <span style={{ color: 'var(--accent)' }}> Primär</span>}
</li>
))}
</ul>
</div>
)}
<div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
<button className="btn btn-primary" onClick={() => setEditing(true)}>Bearbeiten</button>
<button className="btn" onClick={handleDelete}>Löschen</button>
</div>
</div>
)
}
return (
<div>
<h2>{styleDirection.name}</h2>
{styleDirection.abbreviation && <p style={{ color: 'var(--text2)' }}>Kürzel: {styleDirection.abbreviation}</p>}
{styleDirection.description && <p style={{ color: 'var(--text2)' }}>{styleDirection.description}</p>}
{styleDirection.target_groups && styleDirection.target_groups.length > 0 && (
<div style={{ marginTop: '16px' }}>
<h3>Zugeordnete Zielgruppen</h3>
<ul>
{styleDirection.target_groups.map(tg => (
<li key={tg.id}>
{tg.name} {tg.is_primary && <span style={{ color: 'var(--accent)' }}> Primär</span>}
</li>
))}
</ul>
</div>
)}
<button className="btn btn-primary" style={{ marginTop: '16px' }}>
Bearbeiten
</button>
<h2>Stilrichtung bearbeiten</h2>
<div className="form-row">
<label className="form-label">Name</label>
<input
className="form-input"
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
/>
</div>
<div className="form-row">
<label className="form-label">Kürzel</label>
<input
className="form-input"
value={form.abbreviation}
onChange={e => setForm({ ...form, abbreviation: e.target.value })}
/>
</div>
<div className="form-row">
<label className="form-label">Beschreibung</label>
<textarea
className="form-input"
value={form.description}
onChange={e => setForm({ ...form, description: e.target.value })}
rows={4}
/>
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
<button className="btn btn-primary" onClick={handleSave}>Speichern</button>
<button className="btn" onClick={() => setEditing(false)}>Abbrechen</button>
</div>
</div>
)
}
function TrainingTypeDetail({ trainingType, onUpdate }) {
const [editing, setEditing] = useState(false)
const [form, setForm] = useState({
name: trainingType.name,
abbreviation: trainingType.abbreviation || '',
description: trainingType.description || '',
focus_area_id: trainingType.focus_area_id
})
async function handleSave() {
try {
await api.updateTrainingType(trainingType.id, form)
setEditing(false)
onUpdate()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
async function handleDelete() {
if (!confirm(`Trainingstyp "${trainingType.name}" wirklich löschen?`)) return
try {
await api.deleteTrainingType(trainingType.id)
onUpdate()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
if (!editing) {
return (
<div>
<h2>{trainingType.name}</h2>
{trainingType.abbreviation && <p style={{ color: 'var(--text2)' }}>Kürzel: {trainingType.abbreviation}</p>}
{trainingType.description && <p style={{ color: 'var(--text2)' }}>{trainingType.description}</p>}
<div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
<button className="btn btn-primary" onClick={() => setEditing(true)}>Bearbeiten</button>
<button className="btn" onClick={handleDelete}>Löschen</button>
</div>
</div>
)
}
return (
<div>
<h2>{trainingType.name}</h2>
{trainingType.abbreviation && <p style={{ color: 'var(--text2)' }}>Kürzel: {trainingType.abbreviation}</p>}
{trainingType.description && <p style={{ color: 'var(--text2)' }}>{trainingType.description}</p>}
<button className="btn btn-primary" style={{ marginTop: '16px' }}>
Bearbeiten
</button>
<h2>Trainingstyp bearbeiten</h2>
<div className="form-row">
<label className="form-label">Name</label>
<input
className="form-input"
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
/>
</div>
<div className="form-row">
<label className="form-label">Kürzel</label>
<input
className="form-input"
value={form.abbreviation}
onChange={e => setForm({ ...form, abbreviation: e.target.value })}
/>
</div>
<div className="form-row">
<label className="form-label">Beschreibung</label>
<textarea
className="form-input"
value={form.description}
onChange={e => setForm({ ...form, description: e.target.value })}
rows={4}
/>
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
<button className="btn btn-primary" onClick={handleSave}>Speichern</button>
<button className="btn" onClick={() => setEditing(false)}>Abbrechen</button>
</div>
</div>
)
}