195 lines
7.1 KiB
React
195 lines
7.1 KiB
React
|
|
import React, { useState } from 'react'
|
||
|
|
import { api } from '../../utils/api'
|
||
|
|
|
||
|
|
function CatalogsTab({ targetGroups, skillCategories, trainingCharacters, loading, error, onUpdate }) {
|
||
|
|
if (loading) {
|
||
|
|
return <div style={{ textAlign: 'center', padding: '40px' }}><div className="spinner"></div></div>
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '24px' }}>
|
||
|
|
{error && <div style={{ color: 'var(--danger)', padding: '16px', background: 'var(--surface)', borderRadius: '8px' }}>{error}</div>}
|
||
|
|
|
||
|
|
<CatalogSection
|
||
|
|
title="Zielgruppen"
|
||
|
|
icon="🎯"
|
||
|
|
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="⚡"
|
||
|
|
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="💪"
|
||
|
|
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 style={{ background: 'var(--surface)', borderRadius: '12px', padding: '20px' }}>
|
||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||
|
|
<h3 style={{ margin: 0 }}>{icon} {title}</h3>
|
||
|
|
<button className="btn btn-primary" onClick={startCreate}>+ Neu</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{creating && (
|
||
|
|
<div style={{ marginBottom: '20px', padding: '16px', background: 'var(--surface2)', borderRadius: '8px' }}>
|
||
|
|
<h4 style={{ marginTop: 0 }}>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 style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
|
||
|
|
<button className="btn btn-primary" onClick={handleCreate}>Erstellen</button>
|
||
|
|
<button className="btn" onClick={() => setCreating(false)}>Abbrechen</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div style={{ display: 'grid', gap: '12px' }}>
|
||
|
|
{items.map(item => (
|
||
|
|
<div key={item.id} style={{ padding: '12px', background: 'var(--surface2)', borderRadius: '8px' }}>
|
||
|
|
{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 style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
|
||
|
|
<button className="btn btn-primary" onClick={() => handleUpdate(item.id)}>Speichern</button>
|
||
|
|
<button className="btn" onClick={() => setEditing(null)}>Abbrechen</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div>
|
||
|
|
<div style={{ marginBottom: '8px' }}>
|
||
|
|
<strong>{item.name}</strong>
|
||
|
|
{item.min_age !== null && item.max_age !== null && (
|
||
|
|
<span style={{ marginLeft: '12px', color: 'var(--text3)', fontSize: '14px' }}>
|
||
|
|
Alter: {item.min_age}-{item.max_age}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{item.description && <p style={{ color: 'var(--text2)', fontSize: '14px', margin: '8px 0' }}>{item.description}</p>}
|
||
|
|
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
|
||
|
|
<button className="btn" onClick={() => startEdit(item)}>Bearbeiten</button>
|
||
|
|
<button className="btn" onClick={() => handleDelete(item.id, item.name)}>Löschen</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
{items.length === 0 && !creating && (
|
||
|
|
<div style={{ textAlign: 'center', color: 'var(--text3)', padding: '20px' }}>
|
||
|
|
Noch keine Einträge vorhanden
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default CatalogsTab
|