diff --git a/frontend/src/pages/AdminTierLimitsPage.jsx b/frontend/src/pages/AdminTierLimitsPage.jsx
index 1010fe0..ab2bc89 100644
--- a/frontend/src/pages/AdminTierLimitsPage.jsx
+++ b/frontend/src/pages/AdminTierLimitsPage.jsx
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'
+import { Save, RotateCcw, ChevronDown, ChevronUp } from 'lucide-react'
import { api } from '../utils/api'
export default function AdminTierLimitsPage() {
@@ -8,9 +9,13 @@ export default function AdminTierLimitsPage() {
const [matrix, setMatrix] = useState({ tiers: [], features: [], limits: {} })
const [changes, setChanges] = useState({})
const [saving, setSaving] = useState(false)
+ const [isMobile, setIsMobile] = useState(window.innerWidth < 768)
useEffect(() => {
loadMatrix()
+ const handleResize = () => setIsMobile(window.innerWidth < 768)
+ window.addEventListener('resize', handleResize)
+ return () => window.removeEventListener('resize', handleResize)
}, [])
async function loadMatrix() {
@@ -31,9 +36,16 @@ export default function AdminTierLimitsPage() {
const key = `${tierId}:${featureId}`
const newChanges = { ...changes }
+ // Allow temporary empty input for better UX
+ if (value === '') {
+ newChanges[key] = { tierId, featureId, value: '', tempValue: '' }
+ setChanges(newChanges)
+ return
+ }
+
// Parse value
let parsedValue = null
- if (value === '' || value === 'unlimited' || value === '∞') {
+ if (value === 'unlimited' || value === '∞') {
parsedValue = null // unlimited
} else if (value === '0' || value === 'disabled') {
parsedValue = 0 // disabled
@@ -42,16 +54,19 @@ export default function AdminTierLimitsPage() {
if (!isNaN(num) && num >= 0) {
parsedValue = num
} else {
- return // invalid input
+ return // invalid input, ignore
}
}
- newChanges[key] = { tierId, featureId, value: parsedValue }
+ newChanges[key] = { tierId, featureId, value: parsedValue, tempValue: value }
setChanges(newChanges)
}
async function saveChanges() {
- if (Object.keys(changes).length === 0) {
+ // Filter out empty temporary values
+ const validChanges = Object.values(changes).filter(c => c.value !== '')
+
+ if (validChanges.length === 0) {
setSuccess('Keine Änderungen')
return
}
@@ -61,7 +76,7 @@ export default function AdminTierLimitsPage() {
setError('')
setSuccess('')
- const updates = Object.values(changes).map(c => ({
+ const updates = validChanges.map(c => ({
tier_id: c.tierId,
feature_id: c.featureId,
limit_value: c.value
@@ -80,14 +95,16 @@ export default function AdminTierLimitsPage() {
function getCurrentValue(tierId, featureId) {
const key = `${tierId}:${featureId}`
if (key in changes) {
- return changes[key].value
+ // Return temp value for display
+ return changes[key].tempValue !== undefined ? changes[key].tempValue : changes[key].value
}
return matrix.limits[key] ?? null
}
function formatValue(val) {
- if (val === null) return '∞'
- if (val === 0) return '❌'
+ if (val === '' || val === null || val === undefined) return ''
+ if (val === '∞' || val === 'unlimited') return '∞'
+ if (val === 0 || val === '0') return '0'
return val.toString()
}
@@ -106,11 +123,100 @@ export default function AdminTierLimitsPage() {
)
- const hasChanges = Object.keys(changes).length > 0
+ const hasChanges = Object.keys(changes).filter(k => changes[k].value !== '').length > 0
const categoryGroups = groupFeaturesByCategory()
const categoryIcons = { data: '📊', ai: '🤖', export: '📤', integration: '🔗' }
const categoryNames = { data: 'DATEN', ai: 'KI', export: 'EXPORT', integration: 'INTEGRATIONEN' }
+ // Mobile: Card-based view
+ if (isMobile) {
+ return (
+
+ {/* Header */}
+
+
+ Tier Limits
+
+
+ Limits pro Tier konfigurieren
+
+
+
+ {/* Messages */}
+ {error && (
+
+ {error}
+
+ )}
+ {success && (
+
+ {success}
+
+ )}
+
+ {/* Mobile: Feature Cards */}
+ {Object.entries(categoryGroups).map(([category, features]) => (
+
+ {/* Category Header */}
+
+ {categoryIcons[category]} {categoryNames[category] || category}
+
+
+ {/* Features */}
+ {features.map(feature => (
+
+ ))}
+
+ ))}
+
+ {/* Fixed Bottom Bar */}
+
+ {hasChanges && (
+
+ )}
+
+
+
+ )
+ }
+
+ // Desktop: Table view
return (
{/* Header */}
@@ -123,7 +229,7 @@ export default function AdminTierLimitsPage() {
Tier Limits Matrix
- Konfiguriere Feature-Limits pro Tier (∞ = unbegrenzt, ❌ = deaktiviert)
+ Feature-Limits pro Tier (leer = unbegrenzt, 0 = deaktiviert)
@@ -133,7 +239,7 @@ export default function AdminTierLimitsPage() {
onClick={() => { setChanges({}); setSuccess(''); setError('') }}
disabled={saving}
>
- Zurücksetzen
+ Zurücksetzen
)}
@@ -184,7 +290,10 @@ export default function AdminTierLimitsPage() {
textAlign: 'center', padding: '12px 16px', fontWeight: 600,
minWidth: 100
}}>
- {tier.name}
+ {tier.name}
+
+ {tier.id}
+
))}
@@ -215,12 +324,12 @@ export default function AdminTierLimitsPage() {
}}>
{feature.name}
- {feature.limit_type === 'boolean' ? '(boolean)' : `(count, reset: ${feature.reset_period})`}
+ {feature.limit_type === 'boolean' ? '(ja/nein)' : `(count, reset: ${feature.reset_period})`}
{matrix.tiers.map(tier => {
const currentValue = getCurrentValue(tier.id, feature.id)
- const isChanged = `${tier.id}:${feature.id}` in changes
+ const isChanged = `${tier.id}:${feature.id}` in changes && changes[`${tier.id}:${feature.id}`].value !== ''
return (
handleChange(tier.id, feature.id, e.target.value)}
+ placeholder="∞"
style={{
width: '80px',
- padding: '4px 8px',
- border: `1px solid ${isChanged ? 'var(--accent)' : 'var(--border)'}`,
- borderRadius: 4,
+ padding: '6px 8px',
+ border: `1.5px solid ${isChanged ? 'var(--accent)' : 'var(--border)'}`,
+ borderRadius: 6,
textAlign: 'center',
fontSize: 13,
fontWeight: isChanged ? 600 : 400,
background: 'var(--bg)',
- color: currentValue === 0 ? 'var(--danger)' :
- currentValue === null ? 'var(--accent)' : 'var(--text1)'
+ color: currentValue === 0 || currentValue === '0' ? 'var(--danger)' :
+ currentValue === null || currentValue === '' || currentValue === '∞' ? 'var(--accent)' : 'var(--text1)'
}}
- placeholder={feature.limit_type === 'boolean' ? '0/1' : '0-999'}
/>
|
)
@@ -261,14 +370,72 @@ export default function AdminTierLimitsPage() {
marginTop: 16, padding: 12, background: 'var(--surface2)',
borderRadius: 8, fontSize: 12, color: 'var(--text3)'
}}>
- Legende:
+ Eingabe:
- ∞ = Unbegrenzt (NULL)
- ❌ = Deaktiviert (0)
- 1-999 = Limit-Wert
- Boolean: 0 = deaktiviert, 1 = aktiviert
+ leer oder ∞ = Unbegrenzt
+ 0 = Deaktiviert
+ 1-999999 = Limit-Wert
)
}
+
+// Mobile Card Component
+function FeatureMobileCard({ feature, tiers, getCurrentValue, handleChange, changes }) {
+ const [expanded, setExpanded] = useState(false)
+
+ return (
+
+ {/* Feature Header */}
+
setExpanded(!expanded)}
+ style={{
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
+ cursor: 'pointer', padding: '4px 0'
+ }}
+ >
+
+
{feature.name}
+
+ {feature.limit_type === 'boolean' ? '(ja/nein)' : `(${feature.reset_period})`}
+
+
+ {expanded ?
: }
+
+
+ {/* Tier Inputs (Expanded) */}
+ {expanded && (
+
+ {tiers.map(tier => {
+ const currentValue = getCurrentValue(tier.id, feature.id)
+ const isChanged = `${tier.id}:${feature.id}` in changes && changes[`${tier.id}:${feature.id}`].value !== ''
+
+ return (
+
+
+ handleChange(tier.id, feature.id, e.target.value)}
+ placeholder="∞"
+ style={{
+ border: `1.5px solid ${isChanged ? 'var(--accent)' : 'var(--border)'}`,
+ background: isChanged ? 'var(--accent-light)' : 'var(--bg)',
+ color: currentValue === 0 ? 'var(--danger)' :
+ currentValue === null || currentValue === '' ? 'var(--accent)' : 'var(--text1)',
+ fontWeight: isChanged ? 600 : 400
+ }}
+ />
+
+ {currentValue === null || currentValue === '' ? '∞' : currentValue === 0 ? '❌' : '✓'}
+
+
+ )
+ })}
+
+ )}
+
+ )
+}