fix: AdminUserRestrictionsPage - use exact TierLimitsPage input system
All checks were successful
Deploy Development / deploy (push) Successful in 58s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 12s

- formatValue: NULL → '' (empty field with placeholder ∞)
- handleChange: Accept ONLY '∞' or 'unlimited' (no other formats)
- Input styling: Green only for '∞', empty fields normal color
- Simplified legend: Only ∞ or unlimited accepted
- Boolean features: Toggle buttons with 1/0 values
- Add package-lock.json to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-20 11:34:48 +01:00
parent 917c8937cf
commit 85f5938d7d
2 changed files with 67 additions and 59 deletions

2
.gitignore vendored
View File

@ -61,4 +61,4 @@ tmp/
#.claude Konfiguration #.claude Konfiguration
.claude/ .claude/
.claude/settings.local.json .claude/settings.local.jsonfrontend/package-lock.json

View File

@ -77,37 +77,48 @@ export default function AdminUserRestrictionsPage() {
function handleChange(featureId, value) { function handleChange(featureId, value) {
const newChanges = { ...changes } const newChanges = { ...changes }
// Empty string means: remove override (if exists) or do nothing // Empty string means: remove override
if (value === '') { if (value === '') {
newChanges[featureId] = { action: 'remove' } newChanges[featureId] = { action: 'remove', tempValue: '' }
} else { setChanges(newChanges)
// Parse value return
let parsedValue = null }
const lowerValue = value.toLowerCase().trim()
// Accept multiple formats for unlimited // Parse value (EXACTLY like TierLimitsPage)
if (lowerValue === 'unlimited' || lowerValue === 'unbegrenzt' || let parsedValue = null
value === '∞' || lowerValue === 'inf' || lowerValue === '999999') { if (value === 'unlimited' || value === '∞') {
parsedValue = null parsedValue = null // unlimited
} else if (value === '0') { } else if (value === '0' || value === 'disabled') {
parsedValue = 0 parsedValue = 0 // disabled
} else { } else {
const num = parseInt(value) const num = parseInt(value)
if (!isNaN(num) && num >= 0) { if (!isNaN(num) && num >= 0) {
parsedValue = num parsedValue = num
} else { } else {
return // invalid return // invalid input, ignore
} }
} }
newChanges[featureId] = { action: 'set', value: parsedValue }
}
newChanges[featureId] = { action: 'set', value: parsedValue, tempValue: value }
setChanges(newChanges) setChanges(newChanges)
} }
function handleToggle(featureId, currentEnabled) { function handleToggle(featureId) {
// Get current state
const restriction = restrictions.find(r => r.feature_id === featureId)
let currentValue = restriction?.limit_value ?? null
// Check if there's a pending change
if (featureId in changes && changes[featureId].action === 'set') {
currentValue = changes[featureId].value
}
// Toggle between 1 (enabled) and 0 (disabled)
const isCurrentlyEnabled = currentValue !== 0 && currentValue !== '0'
const newValue = isCurrentlyEnabled ? 0 : 1
const newChanges = { ...changes } const newChanges = { ...changes }
newChanges[featureId] = { action: 'toggle', enabled: !currentEnabled } newChanges[featureId] = { action: 'set', value: newValue, tempValue: newValue.toString() }
setChanges(newChanges) setChanges(newChanges)
} }
@ -147,24 +158,6 @@ export default function AdminUserRestrictionsPage() {
}) })
} }
changeCount++ 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++
}
} }
} }
@ -182,28 +175,42 @@ export default function AdminUserRestrictionsPage() {
if (featureId in changes) { if (featureId in changes) {
const change = changes[featureId] const change = changes[featureId]
if (change.action === 'remove') return '' if (change.action === 'remove') return ''
if (change.action === 'set') return change.value === null ? '' : change.value if (change.action === 'set') {
if (change.action === 'toggle') return change.enabled ? 1 : 0 // Use tempValue for display if available, otherwise format the value
return change.tempValue !== undefined ? change.tempValue : formatValue(change.value)
}
} }
// Show existing restriction value (or empty if no restriction) // Show existing restriction value (or empty if no restriction)
const restriction = restrictions.find(r => r.feature_id === featureId) const restriction = restrictions.find(r => r.feature_id === featureId)
if (!restriction) return '' // No override = empty input if (!restriction) return '' // No override = empty input
return restriction.limit_value === null ? '' : restriction.limit_value return formatValue(restriction.limit_value)
}
function formatValue(val) {
if (val === '' || val === null || val === undefined) return ''
if (val === '∞' || val === 'unlimited') return '∞'
if (val === 0 || val === '0') return '0'
return val.toString()
} }
function getToggleState(featureId) { function getToggleState(featureId) {
// Check pending changes first // Check pending changes first
if (featureId in changes && changes[featureId].action === 'toggle') { if (featureId in changes && changes[featureId].action === 'set') {
return changes[featureId].enabled const val = changes[featureId].value
return val !== 0 && val !== '0'
} }
// Check existing restriction // Check existing restriction
const restriction = restrictions.find(r => r.feature_id === featureId) const restriction = restrictions.find(r => r.feature_id === featureId)
if (!restriction) return true // Default: enabled if (!restriction) {
// No override: use tier default
const tierLimit = tierLimits[featureId]
return tierLimit !== 0 && tierLimit !== '0'
}
// For boolean features: limit_value determines state // For boolean features: limit_value determines state
return restriction.limit_value !== 0 return restriction.limit_value !== 0 && restriction.limit_value !== '0'
} }
function hasOverride(featureId) { function hasOverride(featureId) {
@ -427,7 +434,7 @@ export default function AdminUserRestrictionsPage() {
<td style={{ padding: '12px 16px', textAlign: 'center' }}> <td style={{ padding: '12px 16px', textAlign: 'center' }}>
{feature.limit_type === 'boolean' ? ( {feature.limit_type === 'boolean' ? (
<button <button
onClick={() => handleToggle(feature.id, toggleState)} onClick={() => handleToggle(feature.id)}
style={{ style={{
padding: '6px 16px', padding: '6px 16px',
border: `2px solid ${toggleState ? 'var(--accent)' : 'var(--border)'}`, border: `2px solid ${toggleState ? 'var(--accent)' : 'var(--border)'}`,
@ -448,7 +455,7 @@ export default function AdminUserRestrictionsPage() {
type="text" type="text"
value={displayValue} value={displayValue}
onChange={(e) => handleChange(feature.id, e.target.value)} onChange={(e) => handleChange(feature.id, e.target.value)}
placeholder={override ? "Wert..." : ""} placeholder="∞"
style={{ style={{
width: '100px', width: '100px',
padding: '6px 8px', padding: '6px 8px',
@ -457,8 +464,9 @@ export default function AdminUserRestrictionsPage() {
textAlign: 'center', textAlign: 'center',
fontSize: 13, fontSize: 13,
fontWeight: override || changed ? 600 : 400, fontWeight: override || changed ? 600 : 400,
background: override ? 'var(--accent-light)' : 'var(--bg)', background: override || changed ? 'var(--accent-light)' : 'var(--bg)',
color: displayValue === 0 || displayValue === '0' ? 'var(--danger)' : 'var(--text1)' color: displayValue === '0' ? 'var(--danger)' :
displayValue === '∞' ? 'var(--accent)' : 'var(--text1)'
}} }}
/> />
)} )}
@ -495,12 +503,12 @@ export default function AdminUserRestrictionsPage() {
marginTop: 16, padding: 12, background: 'var(--surface2)', marginTop: 16, padding: 12, background: 'var(--surface2)',
borderRadius: 8, fontSize: 12, color: 'var(--text3)' borderRadius: 8, fontSize: 12, color: 'var(--text3)'
}}> }}>
<strong>Eingabe:</strong> <strong>Eingabe (Count-Features):</strong>
<div style={{ marginTop: 8, display: 'flex', gap: 24, flexWrap: 'wrap' }}> <div style={{ marginTop: 8, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<span><strong>Leer</strong> = Tier-Standard nutzen (kein Override)</span> <span><strong style={{ color: 'var(--accent)' }}></strong> oder <strong>unlimited</strong> = Unbegrenzt</span>
<span><strong style={{ color: 'var(--danger)' }}>0</strong> = Feature deaktiviert</span> <span><strong style={{ color: 'var(--danger)' }}>0</strong> = Feature deaktiviert</span>
<span><strong>unbegrenzt, inf, 999999</strong> = Unbegrenzt</span> <span><strong>1+</strong> = Limit-Wert</span>
<span><strong>1-999998</strong> = Limit-Wert</span> <span><strong>Leer</strong> = kein Override (Tier-Standard)</span>
</div> </div>
</div> </div>
</> </>