refactor: change AdminFeaturesPage to configuration-only interface
Philosophy change: - Features are registered via code/migrations, not UI - AdminFeaturesPage now only configures existing features - No create/delete functionality Changes: - Removed "Neues Feature" button and create form - Removed delete functionality - Feature ID now read-only in edit mode - Added info box explaining feature registration - Improved status display (Aktiv/Inaktiv) - Added legend for limit types and reset periods - Focus on configuration: limit type, reset period, defaults, active state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
07a802dff6
commit
69b6f38c89
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Save, Plus, Edit2, Trash2, X } from 'lucide-react'
|
||||
import { Save, Edit2, X, Info } from 'lucide-react'
|
||||
import { api } from '../utils/api'
|
||||
|
||||
export default function AdminFeaturesPage() {
|
||||
|
|
@ -8,9 +8,7 @@ export default function AdminFeaturesPage() {
|
|||
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: '',
|
||||
|
|
@ -42,7 +40,6 @@ export default function AdminFeaturesPage() {
|
|||
|
||||
function resetForm() {
|
||||
setFormData({
|
||||
id: '',
|
||||
name: '',
|
||||
category: 'data',
|
||||
description: '',
|
||||
|
|
@ -55,12 +52,10 @@ export default function AdminFeaturesPage() {
|
|||
active: true
|
||||
})
|
||||
setEditingId(null)
|
||||
setShowAddForm(false)
|
||||
}
|
||||
|
||||
function startEdit(feature) {
|
||||
setFormData({
|
||||
id: feature.id,
|
||||
name: feature.name,
|
||||
category: feature.category,
|
||||
description: feature.description || '',
|
||||
|
|
@ -73,7 +68,6 @@ export default function AdminFeaturesPage() {
|
|||
active: feature.active
|
||||
})
|
||||
setEditingId(feature.id)
|
||||
setShowAddForm(false)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
|
|
@ -81,7 +75,6 @@ export default function AdminFeaturesPage() {
|
|||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
// Validation
|
||||
if (!formData.name.trim()) {
|
||||
setError('Name erforderlich')
|
||||
return
|
||||
|
|
@ -100,21 +93,8 @@ export default function AdminFeaturesPage() {
|
|||
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 api.updateFeature(editingId, payload)
|
||||
setSuccess('Feature aktualisiert')
|
||||
await loadFeatures()
|
||||
resetForm()
|
||||
} catch (e) {
|
||||
|
|
@ -122,18 +102,6 @@ export default function AdminFeaturesPage() {
|
|||
}
|
||||
}
|
||||
|
||||
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" />
|
||||
|
|
@ -148,7 +116,7 @@ export default function AdminFeaturesPage() {
|
|||
]
|
||||
|
||||
const resetPeriodOptions = [
|
||||
{ value: 'never', label: 'Nie' },
|
||||
{ value: 'never', label: 'Nie (akkumuliert)' },
|
||||
{ value: 'daily', label: 'Täglich' },
|
||||
{ value: 'monthly', label: 'Monatlich' }
|
||||
]
|
||||
|
|
@ -161,26 +129,27 @@ export default function AdminFeaturesPage() {
|
|||
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 style={{ marginBottom: 20 }}>
|
||||
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text1)' }}>
|
||||
Feature-Konfiguration
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--text3)', marginTop: 4 }}>
|
||||
Limitierungs-Einstellungen für registrierte Features
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div style={{
|
||||
padding: 12, background: 'var(--accent-light)', borderRadius: 8,
|
||||
marginBottom: 16, fontSize: 12, color: 'var(--accent-dark)',
|
||||
display: 'flex', gap: 8, alignItems: 'flex-start'
|
||||
}}>
|
||||
<Info size={16} style={{ marginTop: 2, flexShrink: 0 }} />
|
||||
<div>
|
||||
<strong>Hinweis:</strong> Features werden automatisch via Code registriert.
|
||||
Hier können nur Basis-Einstellungen (Limit-Typ, Reset-Periode, Standards) angepasst werden.
|
||||
Neue Features hinzuzufügen erfordert Code-Änderungen im Backend.
|
||||
</div>
|
||||
{!showAddForm && !editingId && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowAddForm(true)}
|
||||
>
|
||||
<Plus size={16} /> Neues Feature
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
|
|
@ -201,15 +170,15 @@ export default function AdminFeaturesPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Add/Edit Form */}
|
||||
{(showAddForm || editingId) && (
|
||||
{/* Edit Form */}
|
||||
{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'}
|
||||
Feature konfigurieren
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
|
|
@ -221,18 +190,16 @@ export default function AdminFeaturesPage() {
|
|||
</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>
|
||||
)}
|
||||
{/* Feature ID (read-only) */}
|
||||
<div className="form-row">
|
||||
<label className="form-label">Feature ID</label>
|
||||
<input
|
||||
className="form-input"
|
||||
value={editingId}
|
||||
disabled
|
||||
style={{ background: 'var(--surface2)', color: 'var(--text3)', cursor: 'not-allowed' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className="form-row">
|
||||
|
|
@ -261,7 +228,7 @@ export default function AdminFeaturesPage() {
|
|||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">Typ</label>
|
||||
<label className="form-label">Limit-Typ</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={formData.limit_type}
|
||||
|
|
@ -322,6 +289,9 @@ export default function AdminFeaturesPage() {
|
|||
onChange={(e) => setFormData({ ...formData, default_limit: e.target.value })}
|
||||
placeholder="Leer = unbegrenzt"
|
||||
/>
|
||||
<span className="form-unit" style={{ fontSize: 11, color: 'var(--text3)' }}>
|
||||
Fallback wenn kein Tier-Limit gesetzt
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
|
|
@ -351,14 +321,14 @@ export default function AdminFeaturesPage() {
|
|||
checked={formData.active}
|
||||
onChange={(e) => setFormData({ ...formData, active: e.target.checked })}
|
||||
/>
|
||||
Aktiv
|
||||
Feature aktiviert
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<button className="btn btn-primary" onClick={handleSave}>
|
||||
<Save size={14} /> {editingId ? 'Aktualisieren' : 'Erstellen'}
|
||||
<Save size={14} /> Speichern
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={resetForm}>
|
||||
Abbrechen
|
||||
|
|
@ -373,21 +343,20 @@ export default function AdminFeaturesPage() {
|
|||
<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 }}>Feature</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 }}>Limit-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>
|
||||
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Standard</th>
|
||||
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Status</th>
|
||||
<th style={{ textAlign: 'right', padding: '12px 16px', fontWeight: 600 }}>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{features.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} style={{ textAlign: 'center', padding: 40, color: 'var(--text3)' }}>
|
||||
Keine Features vorhanden
|
||||
<td colSpan={7} style={{ textAlign: 'center', padding: 40, color: 'var(--text3)' }}>
|
||||
Keine Features registriert
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
|
@ -396,20 +365,21 @@ export default function AdminFeaturesPage() {
|
|||
key={feature.id}
|
||||
style={{
|
||||
borderBottom: idx === features.length - 1 ? 'none' : '1px solid var(--border)',
|
||||
background: feature.active ? 'transparent' : 'var(--surface)'
|
||||
background: feature.active ? 'transparent' : 'var(--surface)',
|
||||
opacity: feature.active ? 1 : 0.6
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: '12px 16px', fontWeight: 500 }}>
|
||||
{feature.name}
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<div style={{ fontWeight: 500 }}>{feature.name}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2, fontFamily: 'monospace' }}>
|
||||
{feature.id}
|
||||
</div>
|
||||
{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,
|
||||
|
|
@ -418,45 +388,59 @@ export default function AdminFeaturesPage() {
|
|||
{feature.category}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px', textAlign: 'center', fontSize: 11 }}>
|
||||
{feature.limit_type === 'boolean' ? '✓/✗' : '123'}
|
||||
<td style={{ padding: '12px 16px', textAlign: 'center' }}>
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 4, fontSize: 11,
|
||||
background: feature.limit_type === 'boolean' ? 'var(--surface2)' : 'var(--surface2)',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
{feature.limit_type === 'boolean' ? '✓/✗' : '123'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px', textAlign: 'center', fontSize: 11, color: 'var(--text3)' }}>
|
||||
{feature.reset_period}
|
||||
{feature.reset_period === 'never' ? '∞' : feature.reset_period === 'daily' ? '1d' : '1m'}
|
||||
</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(--accent)', fontWeight: 600 }}>✓ Aktiv</span>
|
||||
) : (
|
||||
<span style={{ color: 'var(--text3)' }}>✗</span>
|
||||
<span style={{ color: 'var(--text3)' }}>✗ Inaktiv</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>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => startEdit(feature)}
|
||||
style={{ padding: '6px 12px', fontSize: 12 }}
|
||||
>
|
||||
<Edit2 size={14} /> Konfigurieren
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div style={{
|
||||
marginTop: 16, padding: 12, background: 'var(--surface2)',
|
||||
borderRadius: 8, fontSize: 12, color: 'var(--text3)'
|
||||
}}>
|
||||
<strong>Limit-Typ:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<strong>Boolean (✓/✗):</strong> Feature ist entweder verfügbar oder nicht (z.B. "KI aktiviert")
|
||||
</div>
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<strong>Count (123):</strong> Feature hat ein Nutzungs-Limit (z.B. "max. 50 Einträge")
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<strong>Reset-Periode:</strong> ∞ = nie, 1d = täglich, 1m = monatlich
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user