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
- 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.
241 lines
7.7 KiB
JavaScript
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
|