484 lines
16 KiB
JavaScript
484 lines
16 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { Plus, Pencil, Trash2, Save, X, Eye, EyeOff } from 'lucide-react'
|
|
import { api } from '../utils/api'
|
|
import EmojiIconPicker from '../components/EmojiIconPicker'
|
|
|
|
const CATEGORIES = [
|
|
{ value: 'body_composition', label: 'Körperzusammensetzung' },
|
|
{ value: 'training', label: 'Training' },
|
|
{ value: 'endurance', label: 'Ausdauer' },
|
|
{ value: 'coordination', label: 'Koordination' },
|
|
{ value: 'mental', label: 'Mental' },
|
|
{ value: 'recovery', label: 'Erholung' },
|
|
{ value: 'health', label: 'Gesundheit' },
|
|
{ value: 'custom', label: 'Eigene' }
|
|
]
|
|
|
|
export default function AdminFocusAreasPage() {
|
|
const [data, setData] = useState({ areas: [], grouped: {}, total: 0 })
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState(null)
|
|
const [showInactive, setShowInactive] = useState(false)
|
|
const [editingId, setEditingId] = useState(null)
|
|
const [creating, setCreating] = useState(false)
|
|
const [formData, setFormData] = useState({
|
|
key: '',
|
|
name_de: '',
|
|
name_en: '',
|
|
icon: '',
|
|
description: '',
|
|
category: 'custom'
|
|
})
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [showInactive])
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const result = await api.listFocusAreaDefinitions(showInactive)
|
|
setData(result)
|
|
setError(null)
|
|
} catch (err) {
|
|
console.error('Failed to load focus areas:', err)
|
|
setError(err.message)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleCreate = async () => {
|
|
if (!formData.key || !formData.name_de) {
|
|
setError('Key und Name (DE) sind erforderlich')
|
|
return
|
|
}
|
|
|
|
try {
|
|
await api.createFocusAreaDefinition(formData)
|
|
setCreating(false)
|
|
setFormData({
|
|
key: '',
|
|
name_de: '',
|
|
name_en: '',
|
|
icon: '',
|
|
description: '',
|
|
category: 'custom'
|
|
})
|
|
await loadData()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
}
|
|
}
|
|
|
|
const handleUpdate = async (id) => {
|
|
try {
|
|
const area = data.areas.find(a => a.id === id)
|
|
await api.updateFocusAreaDefinition(id, {
|
|
name_de: area.name_de,
|
|
name_en: area.name_en,
|
|
icon: area.icon,
|
|
description: area.description,
|
|
category: area.category,
|
|
is_active: area.is_active
|
|
})
|
|
setEditingId(null)
|
|
await loadData()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
}
|
|
}
|
|
|
|
const handleDelete = async (id) => {
|
|
if (!confirm('Focus Area wirklich löschen?')) return
|
|
|
|
try {
|
|
await api.deleteFocusAreaDefinition(id)
|
|
await loadData()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
}
|
|
}
|
|
|
|
const handleToggleActive = async (id) => {
|
|
const area = data.areas.find(a => a.id === id)
|
|
try {
|
|
await api.updateFocusAreaDefinition(id, {
|
|
is_active: !area.is_active
|
|
})
|
|
await loadData()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
}
|
|
}
|
|
|
|
const updateField = (id, field, value) => {
|
|
setData(prev => ({
|
|
...prev,
|
|
areas: prev.areas.map(a =>
|
|
a.id === id ? { ...a, [field]: value } : a
|
|
)
|
|
}))
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ padding: 20, textAlign: 'center' }}>
|
|
<div className="spinner" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div style={{ padding: 16, paddingBottom: 80 }}>
|
|
{/* Header */}
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: 16
|
|
}}>
|
|
<h1 style={{ fontSize: 24, fontWeight: 700, margin: 0 }}>
|
|
🎯 Focus Areas ({data.total})
|
|
</h1>
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={() => setShowInactive(!showInactive)}
|
|
style={{ padding: '8px 12px', fontSize: 13 }}
|
|
>
|
|
{showInactive ? <Eye size={14} /> : <EyeOff size={14} />}
|
|
{showInactive ? 'Inaktive ausblenden' : 'Inaktive anzeigen'}
|
|
</button>
|
|
<button
|
|
className="btn-primary"
|
|
onClick={() => setCreating(true)}
|
|
style={{ padding: '8px 16px' }}
|
|
>
|
|
<Plus size={16} /> Neue Focus Area
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div style={{
|
|
padding: 12,
|
|
background: '#FEE2E2',
|
|
color: '#991B1B',
|
|
borderRadius: 8,
|
|
marginBottom: 16,
|
|
fontSize: 14
|
|
}}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Create Form */}
|
|
{creating && (
|
|
<div className="card" style={{ marginBottom: 16, background: 'var(--accent-light)' }}>
|
|
<h3 style={{ fontSize: 16, marginBottom: 12, color: 'var(--accent)' }}>
|
|
Neue Focus Area
|
|
</h3>
|
|
|
|
<div style={{ display: 'grid', gap: 12 }}>
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
|
Key (Eindeutig, z.B. "explosive_power")
|
|
</label>
|
|
<input
|
|
className="form-input"
|
|
value={formData.key}
|
|
onChange={(e) => setFormData({ ...formData, key: e.target.value })}
|
|
placeholder="explosive_power"
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
|
Name (Deutsch) *
|
|
</label>
|
|
<input
|
|
className="form-input"
|
|
value={formData.name_de}
|
|
onChange={(e) => setFormData({ ...formData, name_de: e.target.value })}
|
|
placeholder="Explosivkraft"
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
|
Name (English)
|
|
</label>
|
|
<input
|
|
className="form-input"
|
|
value={formData.name_en}
|
|
onChange={(e) => setFormData({ ...formData, name_en: e.target.value })}
|
|
placeholder="Explosive Power"
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="admin-focus-area-new-icon"
|
|
style={{ fontSize: 13, fontWeight: 600, display: 'block', marginBottom: 4 }}
|
|
>
|
|
Icon (Emoji)
|
|
</label>
|
|
<EmojiIconPicker
|
|
id="admin-focus-area-new-icon"
|
|
value={formData.icon}
|
|
onChange={(icon) => setFormData({ ...formData, icon })}
|
|
placeholder="💥"
|
|
maxLength={10}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
|
Kategorie
|
|
</label>
|
|
<select
|
|
className="form-input"
|
|
value={formData.category}
|
|
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
|
|
style={{ width: '100%' }}
|
|
>
|
|
{CATEGORIES.map(cat => (
|
|
<option key={cat.value} value={cat.value}>{cat.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
|
Beschreibung
|
|
</label>
|
|
<textarea
|
|
className="form-input"
|
|
value={formData.description}
|
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
placeholder="Kraft in kürzester Zeit explosiv entfalten"
|
|
rows={2}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button className="btn-primary" onClick={handleCreate} style={{ flex: 1 }}>
|
|
<Save size={14} /> Erstellen
|
|
</button>
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={() => {
|
|
setCreating(false)
|
|
setFormData({
|
|
key: '',
|
|
name_de: '',
|
|
name_en: '',
|
|
icon: '',
|
|
description: '',
|
|
category: 'custom'
|
|
})
|
|
}}
|
|
style={{ flex: 1 }}
|
|
>
|
|
<X size={14} /> Abbrechen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Grouped Areas */}
|
|
{Object.entries(data.grouped).map(([category, areas]) => (
|
|
<div key={category} style={{ marginBottom: 24 }}>
|
|
<h2 style={{
|
|
fontSize: 14,
|
|
fontWeight: 600,
|
|
color: 'var(--text2)',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.05em',
|
|
marginBottom: 12
|
|
}}>
|
|
{CATEGORIES.find(c => c.value === category)?.label || category} ({areas.length})
|
|
</h2>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
{areas.map(area => {
|
|
const isEditing = editingId === area.id
|
|
|
|
return (
|
|
<div
|
|
key={area.id}
|
|
className="card"
|
|
style={{
|
|
opacity: area.is_active ? 1 : 0.5,
|
|
borderLeft: area.is_active
|
|
? '4px solid var(--accent)'
|
|
: '4px solid var(--border)'
|
|
}}
|
|
>
|
|
{isEditing ? (
|
|
<div style={{ display: 'grid', gap: 12 }}>
|
|
<div>
|
|
<label style={{ fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
|
Name (DE)
|
|
</label>
|
|
<input
|
|
className="form-input"
|
|
value={area.name_de}
|
|
onChange={(e) => updateField(area.id, 'name_de', e.target.value)}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor={`admin-focus-area-icon-${area.id}`}
|
|
style={{ fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 }}
|
|
>
|
|
Icon
|
|
</label>
|
|
<EmojiIconPicker
|
|
id={`admin-focus-area-icon-${area.id}`}
|
|
value={area.icon || ''}
|
|
onChange={(icon) => updateField(area.id, 'icon', icon)}
|
|
placeholder="💥"
|
|
maxLength={10}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
|
Beschreibung
|
|
</label>
|
|
<textarea
|
|
className="form-input"
|
|
value={area.description || ''}
|
|
onChange={(e) => updateField(area.id, 'description', e.target.value)}
|
|
rows={2}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button
|
|
className="btn-primary"
|
|
onClick={() => handleUpdate(area.id)}
|
|
style={{ flex: 1 }}
|
|
>
|
|
<Save size={14} /> Speichern
|
|
</button>
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={() => {
|
|
setEditingId(null)
|
|
loadData()
|
|
}}
|
|
style={{ flex: 1 }}
|
|
>
|
|
<X size={14} /> Abbrechen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
gap: 12
|
|
}}>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{
|
|
fontSize: 16,
|
|
fontWeight: 600,
|
|
marginBottom: 4,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8
|
|
}}>
|
|
{area.icon && <span>{area.icon}</span>}
|
|
<span>{area.name_de}</span>
|
|
{!area.is_active && (
|
|
<span style={{
|
|
fontSize: 11,
|
|
padding: '2px 6px',
|
|
background: 'var(--border)',
|
|
borderRadius: 4,
|
|
color: 'var(--text3)'
|
|
}}>
|
|
Inaktiv
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--text3)', marginBottom: 4 }}>
|
|
Key: <code style={{
|
|
background: 'var(--surface2)',
|
|
padding: '2px 4px',
|
|
borderRadius: 4
|
|
}}>
|
|
{area.key}
|
|
</code>
|
|
</div>
|
|
|
|
{area.description && (
|
|
<div style={{ fontSize: 13, color: 'var(--text2)', marginTop: 4 }}>
|
|
{area.description}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={() => handleToggleActive(area.id)}
|
|
style={{ padding: '6px 12px' }}
|
|
title={area.is_active ? 'Deaktivieren' : 'Aktivieren'}
|
|
>
|
|
{area.is_active ? <EyeOff size={14} /> : <Eye size={14} />}
|
|
</button>
|
|
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={() => setEditingId(area.id)}
|
|
style={{ padding: '6px 12px' }}
|
|
title="Bearbeiten"
|
|
>
|
|
<Pencil size={14} />
|
|
</button>
|
|
|
|
<button
|
|
className="btn-secondary"
|
|
onClick={() => handleDelete(area.id)}
|
|
style={{ padding: '6px 12px', color: '#DC2626' }}
|
|
title="Löschen"
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{data.areas.length === 0 && (
|
|
<div style={{
|
|
padding: 40,
|
|
textAlign: 'center',
|
|
color: 'var(--text3)'
|
|
}}>
|
|
Keine Focus Areas vorhanden
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|