mitai-jinkendo/frontend/src/pages/AdminFeaturesPage.jsx

463 lines
16 KiB
React
Raw Normal View History

import { useState, useEffect } from 'react'
import { Save, Plus, Edit2, Trash2, X } from 'lucide-react'
import { api } from '../utils/api'
export default function AdminFeaturesPage() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const [features, setFeatures] = useState([])
const [editingId, setEditingId] = useState(null)
const [showAddForm, setShowAddForm] = useState(false)
const [formData, setFormData] = useState({
id: '',
name: '',
category: 'data',
description: '',
unit: 'count',
limit_type: 'count',
default_limit: '',
reset_period: 'never',
visible_in_admin: true,
sort_order: 50,
active: true
})
useEffect(() => {
loadFeatures()
}, [])
async function loadFeatures() {
try {
setLoading(true)
const data = await api.listFeatures()
setFeatures(data)
setError('')
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
function resetForm() {
setFormData({
id: '',
name: '',
category: 'data',
description: '',
unit: 'count',
limit_type: 'count',
default_limit: '',
reset_period: 'never',
visible_in_admin: true,
sort_order: 50,
active: true
})
setEditingId(null)
setShowAddForm(false)
}
function startEdit(feature) {
setFormData({
id: feature.id,
name: feature.name,
category: feature.category,
description: feature.description || '',
unit: feature.unit || 'count',
limit_type: feature.limit_type,
default_limit: feature.default_limit === null ? '' : feature.default_limit,
reset_period: feature.reset_period,
visible_in_admin: feature.visible_in_admin,
sort_order: feature.sort_order || 50,
active: feature.active
})
setEditingId(feature.id)
setShowAddForm(false)
}
async function handleSave() {
try {
setError('')
setSuccess('')
// Validation
if (!formData.name.trim()) {
setError('Name erforderlich')
return
}
const payload = {
name: formData.name.trim(),
category: formData.category,
description: formData.description.trim(),
unit: formData.unit,
limit_type: formData.limit_type,
default_limit: formData.default_limit === '' ? null : parseInt(formData.default_limit),
reset_period: formData.reset_period,
visible_in_admin: formData.visible_in_admin,
sort_order: formData.sort_order,
active: formData.active
}
if (editingId) {
// Update existing
await api.updateFeature(editingId, payload)
setSuccess('Feature aktualisiert')
} else {
// Create new
if (!formData.id.trim()) {
setError('ID erforderlich')
return
}
payload.id = formData.id.trim()
await api.createFeature(payload)
setSuccess('Feature erstellt')
}
await loadFeatures()
resetForm()
} catch (e) {
setError(e.message)
}
}
async function handleDelete(featureId) {
if (!confirm('Feature wirklich löschen?')) return
try {
setError('')
await api.deleteFeature(featureId)
setSuccess('Feature gelöscht')
await loadFeatures()
} catch (e) {
setError(e.message)
}
}
if (loading) return (
<div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
<div className="spinner" />
</div>
)
const categoryOptions = [
{ value: 'data', label: 'Daten' },
{ value: 'ai', label: 'KI' },
{ value: 'export', label: 'Export' },
{ value: 'integration', label: 'Integrationen' }
]
const resetPeriodOptions = [
{ value: 'never', label: 'Nie' },
{ value: 'daily', label: 'Täglich' },
{ value: 'monthly', label: 'Monatlich' }
]
const limitTypeOptions = [
{ value: 'count', label: 'Anzahl (Count)' },
{ value: 'boolean', label: 'Ja/Nein (Boolean)' }
]
return (
<div style={{ paddingBottom: 80 }}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: 20
}}>
<div>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text1)' }}>
Feature-Registry
</div>
<div style={{ fontSize: 13, color: 'var(--text3)', marginTop: 4 }}>
Verfügbare Features verwalten
</div>
</div>
{!showAddForm && !editingId && (
<button
className="btn btn-primary"
onClick={() => setShowAddForm(true)}
>
<Plus size={16} /> Neues Feature
</button>
)}
</div>
{/* Messages */}
{error && (
<div style={{
padding: 12, background: 'var(--danger)', color: 'white',
borderRadius: 8, marginBottom: 16, fontSize: 14
}}>
{error}
</div>
)}
{success && (
<div style={{
padding: 12, background: 'var(--accent)', color: 'white',
borderRadius: 8, marginBottom: 16, fontSize: 14
}}>
{success}
</div>
)}
{/* Add/Edit Form */}
{(showAddForm || editingId) && (
<div className="card" style={{ padding: 20, marginBottom: 20 }}>
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: 16
}}>
<div style={{ fontSize: 16, fontWeight: 600 }}>
{editingId ? 'Feature bearbeiten' : 'Neues Feature erstellen'}
</div>
<button
className="btn btn-secondary"
onClick={resetForm}
style={{ padding: '6px 12px' }}
>
<X size={16} />
</button>
</div>
<div style={{ display: 'grid', gap: 12 }}>
{/* ID (nur bei Neuanlage) */}
{!editingId && (
<div className="form-row">
<label className="form-label">ID (Slug) *</label>
<input
className="form-input"
value={formData.id}
onChange={(e) => setFormData({ ...formData, id: e.target.value })}
placeholder="z.B. weight_entries"
/>
</div>
)}
{/* Name */}
<div className="form-row">
<label className="form-label">Name *</label>
<input
className="form-input"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="z.B. Gewichtseinträge"
/>
</div>
{/* Category + Limit Type */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div className="form-row">
<label className="form-label">Kategorie</label>
<select
className="form-input"
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
>
{categoryOptions.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
<div className="form-row">
<label className="form-label">Typ</label>
<select
className="form-input"
value={formData.limit_type}
onChange={(e) => setFormData({ ...formData, limit_type: e.target.value })}
>
{limitTypeOptions.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
</div>
{/* Description */}
<div className="form-row">
<label className="form-label">Beschreibung</label>
<input
className="form-input"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Optionale Beschreibung"
/>
</div>
{/* Unit + Reset Period */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div className="form-row">
<label className="form-label">Einheit</label>
<input
className="form-input"
value={formData.unit}
onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
placeholder="z.B. count, calls"
/>
</div>
<div className="form-row">
<label className="form-label">Reset-Periode</label>
<select
className="form-input"
value={formData.reset_period}
onChange={(e) => setFormData({ ...formData, reset_period: e.target.value })}
>
{resetPeriodOptions.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
</div>
{/* Default Limit + Sort Order */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div className="form-row">
<label className="form-label">Standard-Limit</label>
<input
className="form-input"
type="number"
value={formData.default_limit}
onChange={(e) => setFormData({ ...formData, default_limit: e.target.value })}
placeholder="Leer = unbegrenzt"
/>
</div>
<div className="form-row">
<label className="form-label">Sortierung</label>
<input
className="form-input"
type="number"
value={formData.sort_order}
onChange={(e) => setFormData({ ...formData, sort_order: parseInt(e.target.value) || 50 })}
/>
</div>
</div>
{/* Checkboxes */}
<div style={{ display: 'flex', gap: 16 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
<input
type="checkbox"
checked={formData.visible_in_admin}
onChange={(e) => setFormData({ ...formData, visible_in_admin: e.target.checked })}
/>
Im Admin sichtbar
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
<input
type="checkbox"
checked={formData.active}
onChange={(e) => setFormData({ ...formData, active: e.target.checked })}
/>
Aktiv
</label>
</div>
{/* Actions */}
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<button className="btn btn-primary" onClick={handleSave}>
<Save size={14} /> {editingId ? 'Aktualisieren' : 'Erstellen'}
</button>
<button className="btn btn-secondary" onClick={resetForm}>
Abbrechen
</button>
</div>
</div>
</div>
)}
{/* Features List */}
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr style={{ background: 'var(--surface2)' }}>
<th style={{ textAlign: 'left', padding: '12px 16px', fontWeight: 600 }}>Name</th>
<th style={{ textAlign: 'left', padding: '12px 16px', fontWeight: 600 }}>ID</th>
<th style={{ textAlign: 'left', padding: '12px 16px', fontWeight: 600 }}>Kategorie</th>
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Typ</th>
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Reset</th>
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Std-Limit</th>
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Aktiv</th>
<th style={{ textAlign: 'right', padding: '12px 16px', fontWeight: 600 }}>Aktionen</th>
</tr>
</thead>
<tbody>
{features.length === 0 && (
<tr>
<td colSpan={8} style={{ textAlign: 'center', padding: 40, color: 'var(--text3)' }}>
Keine Features vorhanden
</td>
</tr>
)}
{features.map((feature, idx) => (
<tr
key={feature.id}
style={{
borderBottom: idx === features.length - 1 ? 'none' : '1px solid var(--border)',
background: feature.active ? 'transparent' : 'var(--surface)'
}}
>
<td style={{ padding: '12px 16px', fontWeight: 500 }}>
{feature.name}
{feature.description && (
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
{feature.description}
</div>
)}
</td>
<td style={{ padding: '12px 16px', fontSize: 11, color: 'var(--text3)', fontFamily: 'monospace' }}>
{feature.id}
</td>
<td style={{ padding: '12px 16px' }}>
<span style={{
padding: '2px 8px', borderRadius: 4, fontSize: 11,
background: 'var(--accent-light)', color: 'var(--accent-dark)', fontWeight: 600
}}>
{feature.category}
</span>
</td>
<td style={{ padding: '12px 16px', textAlign: 'center', fontSize: 11 }}>
{feature.limit_type === 'boolean' ? '✓/✗' : '123'}
</td>
<td style={{ padding: '12px 16px', textAlign: 'center', fontSize: 11, color: 'var(--text3)' }}>
{feature.reset_period}
</td>
<td style={{ padding: '12px 16px', textAlign: 'center', fontWeight: 500 }}>
{feature.default_limit === null ? '∞' : feature.default_limit}
</td>
<td style={{ padding: '12px 16px', textAlign: 'center' }}>
{feature.active ? (
<span style={{ color: 'var(--accent)' }}>✓</span>
) : (
<span style={{ color: 'var(--text3)' }}>✗</span>
)}
</td>
<td style={{ padding: '12px 16px', textAlign: 'right' }}>
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
<button
className="btn btn-secondary"
onClick={() => startEdit(feature)}
style={{ padding: '4px 8px', fontSize: 11 }}
>
<Edit2 size={12} />
</button>
<button
className="btn btn-secondary"
onClick={() => handleDelete(feature.id)}
style={{ padding: '4px 8px', fontSize: 11, color: 'var(--danger)' }}
>
<Trash2 size={12} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}