fix: complete rewrite of AdminUserRestrictionsPage
Fixed all reported bugs: 1. Initial values now correct (empty = no override, not defaults) 2. Save/Reset buttons always visible (fixed bottom bar) 3. Toggle buttons work correctly (can be toggled multiple times) 4. Simplified table columns (removed confusing Tier-Limit/Aktiv/Aktion) New logic: - Empty input = no override (user uses tier standard) - Value entered = override set - Change tracking with 3 actions: set, remove, toggle - Clear status display: "Override aktiv" vs "Tier-Standard" Simplified table structure: - Feature (name + type) - Override-Wert (input/toggle) - Status (has override yes/no) Better UX: - Placeholder text explains empty = tier standard - Status badge shows if override is active - Fixed bottom bar always present - Buttons disabled only when no changes - Legend explains all input options Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
72d8dd8df7
commit
365fe3d068
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Save, User, AlertCircle, X } from 'lucide-react'
|
||||
import { Save, AlertCircle, X, RotateCcw } from 'lucide-react'
|
||||
import { api } from '../utils/api'
|
||||
|
||||
export default function AdminUserRestrictionsPage() {
|
||||
|
|
@ -21,6 +21,10 @@ export default function AdminUserRestrictionsPage() {
|
|||
useEffect(() => {
|
||||
if (selectedUserId) {
|
||||
loadUserData(selectedUserId)
|
||||
} else {
|
||||
setSelectedUser(null)
|
||||
setRestrictions([])
|
||||
setChanges({})
|
||||
}
|
||||
}, [selectedUserId])
|
||||
|
||||
|
|
@ -51,40 +55,43 @@ export default function AdminUserRestrictionsPage() {
|
|||
setRestrictions(restrictionsData)
|
||||
setChanges({})
|
||||
setError('')
|
||||
setSuccess('')
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(featureId, value, enabled) {
|
||||
const key = featureId
|
||||
function handleChange(featureId, value) {
|
||||
const newChanges = { ...changes }
|
||||
|
||||
// Empty string means: remove override (if exists) or do nothing
|
||||
if (value === '') {
|
||||
newChanges[featureId] = { action: 'remove' }
|
||||
} else {
|
||||
// Parse value
|
||||
let parsedValue = null
|
||||
if (value === '' || value === 'unlimited' || value === '∞') {
|
||||
parsedValue = null // unlimited
|
||||
} else if (value === '0' || value === 'disabled') {
|
||||
parsedValue = 0 // disabled
|
||||
if (value === 'unlimited' || value === '∞') {
|
||||
parsedValue = null
|
||||
} else if (value === '0') {
|
||||
parsedValue = 0
|
||||
} else {
|
||||
const num = parseInt(value)
|
||||
if (!isNaN(num) && num >= 0) {
|
||||
parsedValue = num
|
||||
} else {
|
||||
return // invalid input
|
||||
return // invalid
|
||||
}
|
||||
}
|
||||
newChanges[featureId] = { action: 'set', value: parsedValue }
|
||||
}
|
||||
|
||||
newChanges[key] = { featureId, value: parsedValue, enabled }
|
||||
setChanges(newChanges)
|
||||
}
|
||||
|
||||
function toggleEnabled(featureId) {
|
||||
const restriction = restrictions.find(r => r.feature_id === featureId)
|
||||
const currentEnabled = restriction ? restriction.enabled : true
|
||||
const currentValue = restriction ? restriction.limit_value : null
|
||||
|
||||
handleChange(featureId, currentValue === null ? '' : currentValue, !currentEnabled)
|
||||
function handleToggle(featureId, currentEnabled) {
|
||||
const newChanges = { ...changes }
|
||||
newChanges[featureId] = { action: 'toggle', enabled: !currentEnabled }
|
||||
setChanges(newChanges)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
|
|
@ -95,29 +102,56 @@ export default function AdminUserRestrictionsPage() {
|
|||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
// Process changes
|
||||
let changeCount = 0
|
||||
|
||||
for (const [featureId, change] of Object.entries(changes)) {
|
||||
const existingRestriction = restrictions.find(r => r.feature_id === featureId)
|
||||
|
||||
if (change.action === 'remove') {
|
||||
// Remove restriction if exists
|
||||
if (existingRestriction) {
|
||||
await api.deleteUserRestriction(existingRestriction.id)
|
||||
changeCount++
|
||||
}
|
||||
} else if (change.action === 'set') {
|
||||
// Create or update
|
||||
if (existingRestriction) {
|
||||
// Update existing restriction
|
||||
await api.updateUserRestriction(existingRestriction.id, {
|
||||
limit_value: change.value,
|
||||
enabled: change.enabled
|
||||
enabled: true
|
||||
})
|
||||
} else {
|
||||
// Create new restriction
|
||||
await api.createUserRestriction({
|
||||
profile_id: selectedUserId,
|
||||
feature_id: featureId,
|
||||
limit_value: change.value,
|
||||
enabled: change.enabled,
|
||||
enabled: true,
|
||||
reason: 'Admin override'
|
||||
})
|
||||
}
|
||||
changeCount++
|
||||
} else if (change.action === 'toggle') {
|
||||
// Toggle enabled state
|
||||
if (existingRestriction) {
|
||||
await api.updateUserRestriction(existingRestriction.id, {
|
||||
enabled: change.enabled
|
||||
})
|
||||
changeCount++
|
||||
} else {
|
||||
// Create new restriction with toggle state
|
||||
await api.createUserRestriction({
|
||||
profile_id: selectedUserId,
|
||||
feature_id: featureId,
|
||||
limit_value: change.enabled ? 1 : 0,
|
||||
enabled: true,
|
||||
reason: 'Admin override'
|
||||
})
|
||||
changeCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSuccess(`${Object.keys(changes).length} Overrides gespeichert`)
|
||||
setSuccess(`${changeCount} Änderung(en) gespeichert`)
|
||||
await loadUserData(selectedUserId)
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
|
|
@ -126,52 +160,41 @@ export default function AdminUserRestrictionsPage() {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleRemoveRestriction(featureId) {
|
||||
if (!confirm('Override wirklich entfernen? User erhält dann das Standard-Tier-Limit.')) return
|
||||
|
||||
try {
|
||||
const restriction = restrictions.find(r => r.feature_id === featureId)
|
||||
if (restriction) {
|
||||
await api.deleteUserRestriction(restriction.id)
|
||||
setSuccess('Override entfernt')
|
||||
await loadUserData(selectedUserId)
|
||||
// Remove from changes if exists
|
||||
const newChanges = { ...changes }
|
||||
delete newChanges[featureId]
|
||||
setChanges(newChanges)
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentValue(featureId) {
|
||||
// Check if there's a pending change
|
||||
function getDisplayValue(featureId) {
|
||||
// Check pending changes first
|
||||
if (featureId in changes) {
|
||||
return changes[featureId].value
|
||||
const change = changes[featureId]
|
||||
if (change.action === 'remove') return ''
|
||||
if (change.action === 'set') return change.value === null ? '' : change.value
|
||||
if (change.action === 'toggle') return change.enabled ? 1 : 0
|
||||
}
|
||||
|
||||
// Show existing restriction value (or empty if no restriction)
|
||||
const restriction = restrictions.find(r => r.feature_id === featureId)
|
||||
if (!restriction) return '' // No override = empty input
|
||||
return restriction.limit_value === null ? '' : restriction.limit_value
|
||||
}
|
||||
|
||||
function getToggleState(featureId) {
|
||||
// Check pending changes first
|
||||
if (featureId in changes && changes[featureId].action === 'toggle') {
|
||||
return changes[featureId].enabled
|
||||
}
|
||||
|
||||
// Check existing restriction
|
||||
const restriction = restrictions.find(r => r.feature_id === featureId)
|
||||
return restriction ? restriction.limit_value : null
|
||||
if (!restriction) return true // Default: enabled
|
||||
|
||||
// For boolean features: limit_value determines state
|
||||
return restriction.limit_value !== 0
|
||||
}
|
||||
|
||||
function isEnabled(featureId) {
|
||||
if (featureId in changes) {
|
||||
return changes[featureId].enabled
|
||||
}
|
||||
|
||||
const restriction = restrictions.find(r => r.feature_id === featureId)
|
||||
return restriction ? restriction.enabled : true
|
||||
}
|
||||
|
||||
function hasRestriction(featureId) {
|
||||
function hasOverride(featureId) {
|
||||
return restrictions.some(r => r.feature_id === featureId)
|
||||
}
|
||||
|
||||
function formatValue(val) {
|
||||
if (val === null || val === undefined) return ''
|
||||
return val.toString()
|
||||
function isChanged(featureId) {
|
||||
return featureId in changes
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
|
|
@ -210,8 +233,9 @@ export default function AdminUserRestrictionsPage() {
|
|||
}}>
|
||||
<AlertCircle size={16} style={{ marginTop: 2, flexShrink: 0 }} />
|
||||
<div>
|
||||
<strong>Hinweis:</strong> User-Overrides haben höchste Priorität und überschreiben Tier-Limits.
|
||||
Nutze dies sparsam für Sonderfälle (z.B. Beta-Tester, Support-Anfragen).
|
||||
<strong>Hinweis:</strong> User-Overrides überschreiben Tier-Limits.
|
||||
Leere Felder = kein Override (User nutzt Tier-Standard).
|
||||
Wert eingeben = Override setzen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -283,16 +307,20 @@ export default function AdminUserRestrictionsPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features List */}
|
||||
<div className="card" style={{ padding: 0, overflow: 'auto', marginBottom: 16 }}>
|
||||
{/* Features Table */}
|
||||
<div className="card" style={{ padding: 0, overflow: 'auto', marginBottom: 80 }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--surface2)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '12px 16px', fontWeight: 600 }}>Feature</th>
|
||||
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Tier-Limit</th>
|
||||
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Override</th>
|
||||
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600 }}>Aktiv</th>
|
||||
<th style={{ textAlign: 'right', padding: '12px 16px', fontWeight: 600 }}>Aktion</th>
|
||||
<th style={{ textAlign: 'left', padding: '12px 16px', fontWeight: 600, width: '40%' }}>
|
||||
Feature
|
||||
</th>
|
||||
<th style={{ textAlign: 'center', padding: '12px 16px', fontWeight: 600, width: '30%' }}>
|
||||
Override-Wert
|
||||
</th>
|
||||
<th style={{ textAlign: 'right', padding: '12px 16px', fontWeight: 600, width: '30%' }}>
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -300,7 +328,7 @@ export default function AdminUserRestrictionsPage() {
|
|||
<>
|
||||
{/* Category Header */}
|
||||
<tr key={`cat-${category}`} style={{ background: 'var(--accent-light)' }}>
|
||||
<td colSpan={5} style={{
|
||||
<td colSpan={3} style={{
|
||||
padding: '8px 16px', fontWeight: 600, fontSize: 11,
|
||||
textTransform: 'uppercase', letterSpacing: '0.5px',
|
||||
color: 'var(--accent-dark)'
|
||||
|
|
@ -311,82 +339,79 @@ export default function AdminUserRestrictionsPage() {
|
|||
|
||||
{/* Feature Rows */}
|
||||
{categoryFeatures.map(feature => {
|
||||
const currentValue = getCurrentValue(feature.id)
|
||||
const enabled = isEnabled(feature.id)
|
||||
const hasOverride = hasRestriction(feature.id)
|
||||
const isChanged = feature.id in changes
|
||||
const displayValue = getDisplayValue(feature.id)
|
||||
const toggleState = getToggleState(feature.id)
|
||||
const override = hasOverride(feature.id)
|
||||
const changed = isChanged(feature.id)
|
||||
|
||||
return (
|
||||
<tr key={feature.id} style={{
|
||||
borderBottom: '1px solid var(--border)',
|
||||
background: isChanged ? 'var(--accent-light)' : hasOverride ? 'var(--surface)' : 'transparent'
|
||||
background: changed ? 'var(--accent-light)' : 'transparent'
|
||||
}}>
|
||||
<td style={{ padding: '8px 16px' }}>
|
||||
{/* Feature Name */}
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<div style={{ fontWeight: 500 }}>{feature.name}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
|
||||
{feature.limit_type === 'boolean' ? '(ja/nein)' : `(${feature.reset_period})`}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '8px 16px', textAlign: 'center', color: 'var(--text3)' }}>
|
||||
{/* TODO: Show actual tier limit - for now just show "Standard" */}
|
||||
Standard
|
||||
</td>
|
||||
<td style={{ padding: '8px 16px', textAlign: 'center' }}>
|
||||
|
||||
{/* Override Input */}
|
||||
<td style={{ padding: '12px 16px', textAlign: 'center' }}>
|
||||
{feature.limit_type === 'boolean' ? (
|
||||
<button
|
||||
onClick={() => toggleEnabled(feature.id)}
|
||||
onClick={() => handleToggle(feature.id, toggleState)}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
border: `2px solid ${enabled ? 'var(--accent)' : 'var(--border)'}`,
|
||||
border: `2px solid ${toggleState ? 'var(--accent)' : 'var(--border)'}`,
|
||||
borderRadius: 20,
|
||||
background: enabled ? 'var(--accent)' : 'var(--surface)',
|
||||
color: enabled ? 'white' : 'var(--text3)',
|
||||
background: toggleState ? 'var(--accent)' : 'var(--surface)',
|
||||
color: toggleState ? 'white' : 'var(--text3)',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
minWidth: 70
|
||||
minWidth: 80
|
||||
}}
|
||||
>
|
||||
{enabled ? '✓ AN' : '✗ AUS'}
|
||||
{toggleState ? '✓ AN' : '✗ AUS'}
|
||||
</button>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={formatValue(currentValue)}
|
||||
onChange={(e) => handleChange(feature.id, e.target.value, enabled)}
|
||||
placeholder="∞"
|
||||
value={displayValue}
|
||||
onChange={(e) => handleChange(feature.id, e.target.value)}
|
||||
placeholder="leer = Tier-Standard"
|
||||
style={{
|
||||
width: '80px',
|
||||
width: '140px',
|
||||
padding: '6px 8px',
|
||||
border: `1.5px solid ${isChanged ? 'var(--accent)' : hasOverride ? 'var(--border)' : 'var(--border)'}`,
|
||||
border: `1.5px solid ${changed ? 'var(--accent)' : 'var(--border)'}`,
|
||||
borderRadius: 6,
|
||||
textAlign: 'center',
|
||||
fontSize: 13,
|
||||
fontWeight: isChanged ? 600 : 400,
|
||||
background: hasOverride ? 'var(--surface2)' : 'var(--bg)',
|
||||
color: currentValue === 0 ? 'var(--danger)' :
|
||||
currentValue === null ? 'var(--accent)' : 'var(--text1)'
|
||||
fontWeight: changed ? 600 : 400,
|
||||
background: 'var(--bg)',
|
||||
color: displayValue === 0 || displayValue === '0' ? 'var(--danger)' : 'var(--text1)'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '8px 16px', textAlign: 'center' }}>
|
||||
{hasOverride ? (
|
||||
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>✓</span>
|
||||
|
||||
{/* Status */}
|
||||
<td style={{ padding: '12px 16px', textAlign: 'right' }}>
|
||||
{override ? (
|
||||
<span style={{
|
||||
padding: '4px 8px', borderRadius: 4, fontSize: 11,
|
||||
background: 'var(--accent-light)', color: 'var(--accent-dark)',
|
||||
fontWeight: 600
|
||||
}}>
|
||||
✓ Override aktiv
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: 'var(--text3)' }}>-</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '8px 16px', textAlign: 'right' }}>
|
||||
{hasOverride && (
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => handleRemoveRestriction(feature.id)}
|
||||
style={{ padding: '4px 8px', fontSize: 11, color: 'var(--danger)' }}
|
||||
>
|
||||
<X size={12} /> Entfernen
|
||||
</button>
|
||||
<span style={{ fontSize: 11, color: 'var(--text3)' }}>
|
||||
Tier-Standard
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -398,8 +423,7 @@ export default function AdminUserRestrictionsPage() {
|
|||
</table>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
{hasChanges && (
|
||||
{/* Fixed Bottom Bar */}
|
||||
<div style={{
|
||||
position: 'fixed', bottom: 0, left: 0, right: 0,
|
||||
background: 'var(--bg)', borderTop: '1px solid var(--border)',
|
||||
|
|
@ -409,36 +433,33 @@ export default function AdminUserRestrictionsPage() {
|
|||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setChanges({})}
|
||||
disabled={saving}
|
||||
disabled={!hasChanges || saving}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Zurücksetzen
|
||||
<RotateCcw size={14} /> Zurücksetzen
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
disabled={!hasChanges || saving}
|
||||
style={{ flex: 2 }}
|
||||
>
|
||||
{saving ? '...' : `${Object.keys(changes).length} Overrides speichern`}
|
||||
{saving ? 'Speichern...' : hasChanges ? `${Object.keys(changes).length} Änderung(en) speichern` : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Legend */}
|
||||
<div style={{
|
||||
marginTop: 16, padding: 12, background: 'var(--surface2)',
|
||||
borderRadius: 8, fontSize: 12, color: 'var(--text3)'
|
||||
borderRadius: 8, fontSize: 12, color: 'var(--text3)', marginBottom: 100
|
||||
}}>
|
||||
<strong>Eingabe:</strong>
|
||||
<div style={{ marginTop: 8, display: 'flex', gap: 24, flexWrap: 'wrap' }}>
|
||||
<span><strong style={{ color: 'var(--accent)' }}>leer oder ∞</strong> = Unbegrenzt</span>
|
||||
<span><strong style={{ color: 'var(--danger)' }}>0</strong> = Deaktiviert</span>
|
||||
<span><strong>Leer</strong> = Tier-Standard nutzen (kein Override)</span>
|
||||
<span><strong style={{ color: 'var(--danger)' }}>0</strong> = Feature deaktiviert</span>
|
||||
<span><strong>∞ oder leer</strong> = Unbegrenzt (bei Count-Features)</span>
|
||||
<span><strong>1-999999</strong> = Limit-Wert</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<strong>✓ Aktiv</strong> = Override gesetzt, überschreibt Tier-Limit
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user