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,22 +63,46 @@ function AdminHierarchyPage() {
} }
return ( return (
<div style={{ <>
display: 'grid', <style>{`
gridTemplateColumns: '400px 1fr', .admin-hierarchy-container {
gap: '16px', display: grid;
padding: '16px', grid-template-columns: 1fr;
height: 'calc(100vh - 100px)', gap: 16px;
overflow: 'hidden' padding: 16px;
}}> padding-bottom: 80px;
{/* Left: Tree View */} }
<div style={{
overflowY: 'auto', @media (min-width: 768px) {
.admin-hierarchy-container {
grid-template-columns: 400px 1fr;
}
.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)', border: '1px solid var(--border)',
borderRadius: '12px', borderRadius: '12px',
padding: '16px', padding: '16px',
background: 'var(--surface)' background: 'var(--surface)'
}}> }}
>
<h2 style={{ marginTop: 0 }}>Katalog-Hierarchie</h2> <h2 style={{ marginTop: 0 }}>Katalog-Hierarchie</h2>
{error && <div style={{ color: 'var(--danger)', marginBottom: '16px' }}>{error}</div>} {error && <div style={{ color: 'var(--danger)', marginBottom: '16px' }}>{error}</div>}
@ -95,23 +119,28 @@ function AdminHierarchyPage() {
))} ))}
</div> </div>
{/* Right: Detail Panel */} {/* Detail Panel */}
<div style={{ {selectedItem && (
overflowY: 'auto', <div
style={{
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: '12px', borderRadius: '12px',
padding: '20px', padding: '20px',
background: 'var(--surface)' background: 'var(--surface)'
}}> }}
{selectedItem ? ( >
<button
className="btn admin-back-button"
onClick={() => setSelectedItem(null)}
style={{ marginBottom: '16px' }}
>
Zurück zur Übersicht
</button>
<DetailPanel item={selectedItem} onUpdate={loadHierarchy} /> <DetailPanel item={selectedItem} onUpdate={loadHierarchy} />
) : (
<div style={{ textAlign: 'center', color: 'var(--text2)', paddingTop: '60px' }}>
<p> Wähle ein Element aus dem Baum</p>
</div> </div>
)} )}
</div> </div>
</div> </>
) )
} }
@ -332,6 +361,35 @@ function FocusAreaDetail({ focusArea, onUpdate }) {
} }
function StyleDirectionDetail({ styleDirection, 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 ( return (
<div> <div>
<h2>{styleDirection.name}</h2> <h2>{styleDirection.name}</h2>
@ -351,23 +409,126 @@ function StyleDirectionDetail({ styleDirection, onUpdate }) {
</div> </div>
)} )}
<button className="btn btn-primary" style={{ marginTop: '16px' }}> <div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
Bearbeiten <button className="btn btn-primary" onClick={() => setEditing(true)}>Bearbeiten</button>
</button> <button className="btn" onClick={handleDelete}>Löschen</button>
</div>
</div>
)
}
return (
<div>
<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> </div>
) )
} }
function TrainingTypeDetail({ trainingType, onUpdate }) { 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 ( return (
<div> <div>
<h2>{trainingType.name}</h2> <h2>{trainingType.name}</h2>
{trainingType.abbreviation && <p style={{ color: 'var(--text2)' }}>Kürzel: {trainingType.abbreviation}</p>} {trainingType.abbreviation && <p style={{ color: 'var(--text2)' }}>Kürzel: {trainingType.abbreviation}</p>}
{trainingType.description && <p style={{ color: 'var(--text2)' }}>{trainingType.description}</p>} {trainingType.description && <p style={{ color: 'var(--text2)' }}>{trainingType.description}</p>}
<button className="btn btn-primary" style={{ marginTop: '16px' }}> <div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
Bearbeiten <button className="btn btn-primary" onClick={() => setEditing(true)}>Bearbeiten</button>
</button> <button className="btn" onClick={handleDelete}>Löschen</button>
</div>
</div>
)
}
return (
<div>
<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> </div>
) )
} }