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 ? '❌' : '✓'} + +
+ ) + })} +
+ )} +
+ ) +}