refactor: change AdminFeaturesPage to configuration-only interface
All checks were successful
Deploy Development / deploy (push) Successful in 56s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 13s

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:
Lars 2026-03-20 06:46:04 +01:00
parent 07a802dff6
commit 69b6f38c89

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' 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' import { api } from '../utils/api'
export default function AdminFeaturesPage() { export default function AdminFeaturesPage() {
@ -8,9 +8,7 @@ export default function AdminFeaturesPage() {
const [success, setSuccess] = useState('') const [success, setSuccess] = useState('')
const [features, setFeatures] = useState([]) const [features, setFeatures] = useState([])
const [editingId, setEditingId] = useState(null) const [editingId, setEditingId] = useState(null)
const [showAddForm, setShowAddForm] = useState(false)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
id: '',
name: '', name: '',
category: 'data', category: 'data',
description: '', description: '',
@ -42,7 +40,6 @@ export default function AdminFeaturesPage() {
function resetForm() { function resetForm() {
setFormData({ setFormData({
id: '',
name: '', name: '',
category: 'data', category: 'data',
description: '', description: '',
@ -55,12 +52,10 @@ export default function AdminFeaturesPage() {
active: true active: true
}) })
setEditingId(null) setEditingId(null)
setShowAddForm(false)
} }
function startEdit(feature) { function startEdit(feature) {
setFormData({ setFormData({
id: feature.id,
name: feature.name, name: feature.name,
category: feature.category, category: feature.category,
description: feature.description || '', description: feature.description || '',
@ -73,7 +68,6 @@ export default function AdminFeaturesPage() {
active: feature.active active: feature.active
}) })
setEditingId(feature.id) setEditingId(feature.id)
setShowAddForm(false)
} }
async function handleSave() { async function handleSave() {
@ -81,7 +75,6 @@ export default function AdminFeaturesPage() {
setError('') setError('')
setSuccess('') setSuccess('')
// Validation
if (!formData.name.trim()) { if (!formData.name.trim()) {
setError('Name erforderlich') setError('Name erforderlich')
return return
@ -100,21 +93,8 @@ export default function AdminFeaturesPage() {
active: formData.active active: formData.active
} }
if (editingId) { await api.updateFeature(editingId, payload)
// Update existing setSuccess('Feature aktualisiert')
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() await loadFeatures()
resetForm() resetForm()
} catch (e) { } 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 ( if (loading) return (
<div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}> <div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
<div className="spinner" /> <div className="spinner" />
@ -148,7 +116,7 @@ export default function AdminFeaturesPage() {
] ]
const resetPeriodOptions = [ const resetPeriodOptions = [
{ value: 'never', label: 'Nie' }, { value: 'never', label: 'Nie (akkumuliert)' },
{ value: 'daily', label: 'Täglich' }, { value: 'daily', label: 'Täglich' },
{ value: 'monthly', label: 'Monatlich' } { value: 'monthly', label: 'Monatlich' }
] ]
@ -161,26 +129,27 @@ export default function AdminFeaturesPage() {
return ( return (
<div style={{ paddingBottom: 80 }}> <div style={{ paddingBottom: 80 }}>
{/* Header */} {/* Header */}
<div style={{ <div style={{ marginBottom: 20 }}>
display: 'flex', alignItems: 'center', justifyContent: 'space-between', <div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text1)' }}>
marginBottom: 20 Feature-Konfiguration
}}> </div>
<div> <div style={{ fontSize: 13, color: 'var(--text3)', marginTop: 4 }}>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text1)' }}> Limitierungs-Einstellungen für registrierte Features
Feature-Registry </div>
</div> </div>
<div style={{ fontSize: 13, color: 'var(--text3)', marginTop: 4 }}>
Verfügbare Features verwalten {/* Info Box */}
</div> <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> </div>
{!showAddForm && !editingId && (
<button
className="btn btn-primary"
onClick={() => setShowAddForm(true)}
>
<Plus size={16} /> Neues Feature
</button>
)}
</div> </div>
{/* Messages */} {/* Messages */}
@ -201,15 +170,15 @@ export default function AdminFeaturesPage() {
</div> </div>
)} )}
{/* Add/Edit Form */} {/* Edit Form */}
{(showAddForm || editingId) && ( {editingId && (
<div className="card" style={{ padding: 20, marginBottom: 20 }}> <div className="card" style={{ padding: 20, marginBottom: 20 }}>
<div style={{ <div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: 16 marginBottom: 16
}}> }}>
<div style={{ fontSize: 16, fontWeight: 600 }}> <div style={{ fontSize: 16, fontWeight: 600 }}>
{editingId ? 'Feature bearbeiten' : 'Neues Feature erstellen'} Feature konfigurieren
</div> </div>
<button <button
className="btn btn-secondary" className="btn btn-secondary"
@ -221,18 +190,16 @@ export default function AdminFeaturesPage() {
</div> </div>
<div style={{ display: 'grid', gap: 12 }}> <div style={{ display: 'grid', gap: 12 }}>
{/* ID (nur bei Neuanlage) */} {/* Feature ID (read-only) */}
{!editingId && ( <div className="form-row">
<div className="form-row"> <label className="form-label">Feature ID</label>
<label className="form-label">ID (Slug) *</label> <input
<input className="form-input"
className="form-input" value={editingId}
value={formData.id} disabled
onChange={(e) => setFormData({ ...formData, id: e.target.value })} style={{ background: 'var(--surface2)', color: 'var(--text3)', cursor: 'not-allowed' }}
placeholder="z.B. weight_entries" />
/> </div>
</div>
)}
{/* Name */} {/* Name */}
<div className="form-row"> <div className="form-row">
@ -261,7 +228,7 @@ export default function AdminFeaturesPage() {
</div> </div>
<div className="form-row"> <div className="form-row">
<label className="form-label">Typ</label> <label className="form-label">Limit-Typ</label>
<select <select
className="form-input" className="form-input"
value={formData.limit_type} value={formData.limit_type}
@ -322,6 +289,9 @@ export default function AdminFeaturesPage() {
onChange={(e) => setFormData({ ...formData, default_limit: e.target.value })} onChange={(e) => setFormData({ ...formData, default_limit: e.target.value })}
placeholder="Leer = unbegrenzt" placeholder="Leer = unbegrenzt"
/> />
<span className="form-unit" style={{ fontSize: 11, color: 'var(--text3)' }}>
Fallback wenn kein Tier-Limit gesetzt
</span>
</div> </div>
<div className="form-row"> <div className="form-row">
@ -351,14 +321,14 @@ export default function AdminFeaturesPage() {
checked={formData.active} checked={formData.active}
onChange={(e) => setFormData({ ...formData, active: e.target.checked })} onChange={(e) => setFormData({ ...formData, active: e.target.checked })}
/> />
Aktiv Feature aktiviert
</label> </label>
</div> </div>
{/* Actions */} {/* Actions */}
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}> <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<button className="btn btn-primary" onClick={handleSave}> <button className="btn btn-primary" onClick={handleSave}>
<Save size={14} /> {editingId ? 'Aktualisieren' : 'Erstellen'} <Save size={14} /> Speichern
</button> </button>
<button className="btn btn-secondary" onClick={resetForm}> <button className="btn btn-secondary" onClick={resetForm}>
Abbrechen Abbrechen
@ -373,21 +343,20 @@ export default function AdminFeaturesPage() {
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}> <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead> <thead>
<tr style={{ background: 'var(--surface2)' }}> <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 }}>Feature</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: '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 }}>Reset</th>
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Std-Limit</th> <th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Standard</th>
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Aktiv</th> <th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Status</th>
<th style={{ textAlign: 'right', padding: '12px 16px', fontWeight: 600 }}>Aktionen</th> <th style={{ textAlign: 'right', padding: '12px 16px', fontWeight: 600 }}>Aktion</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{features.length === 0 && ( {features.length === 0 && (
<tr> <tr>
<td colSpan={8} style={{ textAlign: 'center', padding: 40, color: 'var(--text3)' }}> <td colSpan={7} style={{ textAlign: 'center', padding: 40, color: 'var(--text3)' }}>
Keine Features vorhanden Keine Features registriert
</td> </td>
</tr> </tr>
)} )}
@ -396,20 +365,21 @@ export default function AdminFeaturesPage() {
key={feature.id} key={feature.id}
style={{ style={{
borderBottom: idx === features.length - 1 ? 'none' : '1px solid var(--border)', 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 }}> <td style={{ padding: '12px 16px' }}>
{feature.name} <div style={{ fontWeight: 500 }}>{feature.name}</div>
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2, fontFamily: 'monospace' }}>
{feature.id}
</div>
{feature.description && ( {feature.description && (
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}> <div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
{feature.description} {feature.description}
</div> </div>
)} )}
</td> </td>
<td style={{ padding: '12px 16px', fontSize: 11, color: 'var(--text3)', fontFamily: 'monospace' }}>
{feature.id}
</td>
<td style={{ padding: '12px 16px' }}> <td style={{ padding: '12px 16px' }}>
<span style={{ <span style={{
padding: '2px 8px', borderRadius: 4, fontSize: 11, padding: '2px 8px', borderRadius: 4, fontSize: 11,
@ -418,45 +388,59 @@ export default function AdminFeaturesPage() {
{feature.category} {feature.category}
</span> </span>
</td> </td>
<td style={{ padding: '12px 16px', textAlign: 'center', fontSize: 11 }}> <td style={{ padding: '12px 16px', textAlign: 'center' }}>
{feature.limit_type === 'boolean' ? '✓/✗' : '123'} <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>
<td style={{ padding: '12px 16px', textAlign: 'center', fontSize: 11, color: 'var(--text3)' }}> <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>
<td style={{ padding: '12px 16px', textAlign: 'center', fontWeight: 500 }}> <td style={{ padding: '12px 16px', textAlign: 'center', fontWeight: 500 }}>
{feature.default_limit === null ? '∞' : feature.default_limit} {feature.default_limit === null ? '∞' : feature.default_limit}
</td> </td>
<td style={{ padding: '12px 16px', textAlign: 'center' }}> <td style={{ padding: '12px 16px', textAlign: 'center' }}>
{feature.active ? ( {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>
<td style={{ padding: '12px 16px', textAlign: 'right' }}> <td style={{ padding: '12px 16px', textAlign: 'right' }}>
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}> <button
<button className="btn btn-secondary"
className="btn btn-secondary" onClick={() => startEdit(feature)}
onClick={() => startEdit(feature)} style={{ padding: '6px 12px', fontSize: 12 }}
style={{ padding: '4px 8px', fontSize: 11 }} >
> <Edit2 size={14} /> Konfigurieren
<Edit2 size={12} /> </button>
</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> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </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> </div>
) )
} }