shinkan-jinkendo/frontend/src/components/admin/CatalogsTab.jsx
Lars 657f73d2c5
Some checks failed
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 7s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 27s
feat: enhance admin catalog UI and functionality
- Added new CSS styles for admin catalog sections, improving layout and responsiveness.
- Implemented icon support for catalog section titles, enhancing visual clarity.
- Refactored loading and error states for better user experience in the CatalogsTab and HierarchyTab components.
- Updated AdminCatalogsPage to utilize new styles and improve tab navigation.
- Enhanced accessibility with appropriate ARIA roles and attributes for better usability.
2026-05-06 11:12:59 +02:00

241 lines
7.7 KiB
JavaScript

import React, { useState } from 'react'
import { Target, Tags, Dumbbell } from 'lucide-react'
import { api } from '../../utils/api'
function CatalogsTab({ targetGroups, skillCategories, trainingCharacters, loading, error, onUpdate }) {
if (loading) {
return (
<div className="empty-state" style={{ padding: '2.5rem' }}>
<div className="spinner" />
</div>
)
}
return (
<div className="admin-catalog-stack">
{error && <div className="admin-matrix-alert">{error}</div>}
<CatalogSection
title="Zielgruppen"
Icon={Target}
items={targetGroups}
onUpdate={onUpdate}
createFn={api.createTargetGroup}
updateFn={api.updateTargetGroup}
deleteFn={api.deleteTargetGroup}
fields={[
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea' },
{ key: 'min_age', label: 'Min. Alter', type: 'number' },
{ key: 'max_age', label: 'Max. Alter', type: 'number' }
]}
/>
<CatalogSection
title="Fähigkeitskategorien"
Icon={Tags}
items={skillCategories}
onUpdate={onUpdate}
createFn={api.createSkillCategory}
updateFn={api.updateSkillCategory}
deleteFn={api.deleteSkillCategory}
fields={[
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea' }
]}
/>
<CatalogSection
title="Trainingscharakter"
Icon={Dumbbell}
items={trainingCharacters}
onUpdate={onUpdate}
createFn={api.createTrainingCharacter}
updateFn={api.updateTrainingCharacter}
deleteFn={api.deleteTrainingCharacter}
fields={[
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea' }
]}
/>
</div>
)
}
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 (
<div className="admin-catalog-section">
<div className="admin-catalog-section__head">
<h3 className="admin-catalog-section__title">
{Icon ? (
<Icon className="admin-catalog-section__icon" size={20} strokeWidth={2} aria-hidden />
) : null}
{title}
</h3>
<button type="button" className="btn btn-primary btn-small" onClick={startCreate}>
+ Neu
</button>
</div>
{creating && (
<div className="admin-catalog-inline-form">
<h4>Neu erstellen</h4>
{fields.map((field) => (
<div key={field.key} className="form-row">
<label className="form-label">
{field.label} {field.required && '*'}
</label>
{field.type === 'textarea' ? (
<textarea
className="form-input"
value={form[field.key] || ''}
onChange={(e) => setForm({ ...form, [field.key]: e.target.value })}
rows={3}
/>
) : (
<input
className="form-input"
type={field.type}
value={form[field.key] || ''}
onChange={(e) => setForm({ ...form, [field.key]: e.target.value })}
/>
)}
</div>
))}
<div className="admin-catalog-actions">
<button type="button" className="btn btn-primary" onClick={handleCreate}>
Erstellen
</button>
<button type="button" className="btn btn-secondary" onClick={() => setCreating(false)}>
Abbrechen
</button>
</div>
</div>
)}
<div className="admin-catalog-list">
{items.map((item) => (
<div key={item.id} className="admin-catalog-item">
{editing === item.id ? (
<div>
{fields.map((field) => (
<div key={field.key} className="form-row">
<label className="form-label">{field.label}</label>
{field.type === 'textarea' ? (
<textarea
className="form-input"
value={form[field.key] || ''}
onChange={(e) => setForm({ ...form, [field.key]: e.target.value })}
rows={3}
/>
) : (
<input
className="form-input"
type={field.type}
value={form[field.key] || ''}
onChange={(e) => setForm({ ...form, [field.key]: e.target.value })}
/>
)}
</div>
))}
<div className="admin-catalog-actions">
<button type="button" className="btn btn-primary" onClick={() => handleUpdate(item.id)}>
Speichern
</button>
<button type="button" className="btn btn-secondary" onClick={() => setEditing(null)}>
Abbrechen
</button>
</div>
</div>
) : (
<div>
<div className="admin-catalog-item__name-row">
<strong>{item.name}</strong>
{item.min_age != null && item.max_age != null && (
<span className="admin-catalog-meta">
Alter: {item.min_age}-{item.max_age}
</span>
)}
</div>
{item.description ? (
<p className="admin-catalog-desc">{item.description}</p>
) : null}
<div className="admin-catalog-actions">
<button type="button" className="btn btn-secondary btn-small" onClick={() => startEdit(item)}>
Bearbeiten
</button>
<button type="button" className="btn btn-danger btn-small" onClick={() => handleDelete(item.id, item.name)}>
Löschen
</button>
</div>
</div>
)}
</div>
))}
{items.length === 0 && !creating && (
<div className="admin-catalog-empty">Noch keine Einträge vorhanden</div>
)}
</div>
</div>
)
}
export default CatalogsTab