import { useState, useEffect } from 'react' import { Save, AlertCircle, X, RotateCcw } from 'lucide-react' import { api } from '../utils/api' export default function AdminUserRestrictionsPage() { const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [success, setSuccess] = useState('') const [users, setUsers] = useState([]) const [features, setFeatures] = useState([]) const [selectedUserId, setSelectedUserId] = useState('') const [selectedUser, setSelectedUser] = useState(null) const [restrictions, setRestrictions] = useState([]) const [tierLimits, setTierLimits] = useState({}) const [changes, setChanges] = useState({}) const [saving, setSaving] = useState(false) useEffect(() => { loadInitialData() }, []) useEffect(() => { if (selectedUserId) { loadUserData(selectedUserId) } else { setSelectedUser(null) setRestrictions([]) setChanges({}) } }, [selectedUserId]) async function loadInitialData() { try { setLoading(true) const [usersData, featuresData] = await Promise.all([ api.adminListProfiles(), api.listFeatures() ]) setUsers(usersData) setFeatures(featuresData.filter(f => f.active)) setError('') } catch (e) { setError(e.message) } finally { setLoading(false) } } async function loadUserData(userId) { try { const [user, restrictionsData, limitsMatrix] = await Promise.all([ api.adminListProfiles().then(users => users.find(u => u.id === userId)), api.listUserRestrictions(userId), api.getTierLimitsMatrix() ]) setSelectedUser(user) setRestrictions(restrictionsData) // Build tier limits lookup for this user's tier const userTier = user.tier || 'free' const limits = {} features.forEach(feature => { const key = `${userTier}:${feature.id}` // Use same fallback logic as TierLimitsPage: undefined → null (unlimited) limits[feature.id] = limitsMatrix.limits[key] ?? null }) setTierLimits(limits) setChanges({}) setError('') setSuccess('') } catch (e) { setError(e.message) } } function handleChange(featureId, value) { const newChanges = { ...changes } const tierLimit = tierLimits[featureId] // Parse value (EXACTLY like TierLimitsPage) let parsedValue = null if (value === 'unlimited' || value === '∞') { parsedValue = null // unlimited } else if (value === '0' || value === 'disabled') { parsedValue = 0 // disabled } else if (value === '') { parsedValue = null // empty → unlimited } else { const num = parseInt(value) if (!isNaN(num) && num >= 0) { parsedValue = num } else { return // invalid input, ignore } } // Check if value equals tier limit → remove override if (parsedValue === tierLimit) { newChanges[featureId] = { action: 'remove', tempValue: value } } else { // Different from tier default → set override newChanges[featureId] = { action: 'set', value: parsedValue, tempValue: value } } setChanges(newChanges) } 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 } newChanges[featureId] = { action: 'set', value: newValue, tempValue: newValue.toString() } setChanges(newChanges) } async function handleSave() { if (!selectedUserId) return try { setSaving(true) setError('') setSuccess('') 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) { await api.updateUserRestriction(existingRestriction.id, { limit_value: change.value, enabled: true }) } else { await api.createUserRestriction({ profile_id: selectedUserId, feature_id: featureId, limit_value: change.value, enabled: true, reason: 'Admin override' }) } changeCount++ } } setSuccess(`${changeCount} Änderung(en) gespeichert`) await loadUserData(selectedUserId) } catch (e) { setError(e.message) } finally { setSaving(false) } } function getDisplayValue(featureId) { // Check pending changes first if (featureId in changes) { const change = changes[featureId] if (change.action === 'remove') { // Returning to tier default return formatValue(tierLimits[featureId]) } if (change.action === 'set') { // Use tempValue for display if available, otherwise format the value return change.tempValue !== undefined ? change.tempValue : formatValue(change.value) } } // Show override if exists, otherwise tier limit (= effective value) const restriction = restrictions.find(r => r.feature_id === featureId) if (restriction) { return formatValue(restriction.limit_value) } // No override: show tier limit as default return formatValue(tierLimits[featureId]) } function formatValue(val) { if (val === null || val === undefined) return 'unlimited' if (val === '' ) return '' if (val === '∞' || val === 'unlimited') return 'unlimited' if (val === 0 || val === '0') return '0' return val.toString() } function getToggleState(featureId) { // Check pending changes first if (featureId in changes && changes[featureId].action === 'set') { const val = changes[featureId].value return val !== 0 && val !== '0' } // Check existing restriction const restriction = restrictions.find(r => r.feature_id === featureId) if (!restriction) { // No override: use tier default const tierLimit = tierLimits[featureId] return tierLimit !== 0 && tierLimit !== '0' } // For boolean features: limit_value determines state return restriction.limit_value !== 0 && restriction.limit_value !== '0' } function hasOverride(featureId) { return restrictions.some(r => r.feature_id === featureId) } function isChanged(featureId) { return featureId in changes } if (loading) return (
) const hasChanges = Object.keys(changes).length > 0 const categoryGroups = {} features.forEach(f => { if (!categoryGroups[f.category]) categoryGroups[f.category] = [] categoryGroups[f.category].push(f) }) const categoryIcons = { data: '📊', ai: '🤖', export: '📤', integration: '🔗' } const categoryNames = { data: 'DATEN', ai: 'KI', export: 'EXPORT', integration: 'INTEGRATIONEN' } return (
{/* Header */}
User Feature-Overrides
Individuelle Feature-Limits für einzelne User setzen
{/* Info Box */}
Hinweis: Felder zeigen effektive Werte (Override falls gesetzt, sonst Tier-Standard). Wert ändern → Override wird gesetzt. Wert = Tier-Standard → Override wird entfernt.
{/* Messages */} {error && (
{error}
)} {success && (
{success}
)} {/* User Selection */}
{/* User Info + Features */} {selectedUser && ( <> {/* Action Buttons */}
Feature-Overrides für {selectedUser.name}
{hasChanges && ( )}
{/* User Info Card */}
{selectedUser.name?.charAt(0).toUpperCase()}
{selectedUser.name}
{selectedUser.email || `ID: ${selectedUser.id}`}
Tier: {selectedUser.tier || 'free'}
{/* Features Table */}
{Object.entries(categoryGroups).map(([category, categoryFeatures]) => ( <> {/* Category Header */} {/* Feature Rows */} {categoryFeatures.map(feature => { const displayValue = getDisplayValue(feature.id) const toggleState = getToggleState(feature.id) const override = hasOverride(feature.id) const changed = isChanged(feature.id) return ( {/* Feature Name */} {/* Tier-Limit */} {/* Override Input */} {/* Action */} ) })} ))}
Feature Tier-Limit Override-Wert Aktion
{categoryIcons[category]} {categoryNames[category] || category}
{feature.name}
{feature.limit_type === 'boolean' ? '(ja/nein)' : `(${feature.reset_period})`}
{feature.limit_type === 'boolean' ? ( {tierLimits[feature.id] !== 0 ? '✓ AN' : '✗ AUS'} ) : ( {tierLimits[feature.id] === null ? '∞' : tierLimits[feature.id]} )} {feature.limit_type === 'boolean' ? ( ) : ( handleChange(feature.id, e.target.value)} placeholder="" style={{ width: '120px', padding: '6px 8px', border: `1.5px solid ${changed ? 'var(--accent)' : override ? 'var(--accent)' : 'var(--border)'}`, borderRadius: 6, textAlign: 'center', fontSize: 13, fontWeight: override || changed ? 600 : 400, background: override || changed ? 'var(--accent-light)' : 'var(--bg)', color: displayValue === '0' ? 'var(--danger)' : displayValue === 'unlimited' ? 'var(--accent)' : 'var(--text1)' }} /> )}
{/* Legend */}
Eingabe:
unlimited = Unbegrenzt 0 = Feature deaktiviert 1+ = Limit-Wert
• Feld zeigt effektiven Wert (Override falls gesetzt, sonst Tier-Standard)
• Wert ändern → Override wird gesetzt
• Wert = Tier-Standard → Override wird entfernt
)}
) }