feat: complete admin hierarchy - edit/delete + responsive design
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:
parent
599d696321
commit
af89546022
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user