mitai-jinkendo/frontend/src/pages/AdminTierLimitsPage.jsx

275 lines
9.2 KiB
React
Raw Normal View History

import { useState, useEffect } from 'react'
import { api } from '../utils/api'
export default function AdminTierLimitsPage() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const [matrix, setMatrix] = useState({ tiers: [], features: [], limits: {} })
const [changes, setChanges] = useState({})
const [saving, setSaving] = useState(false)
useEffect(() => {
loadMatrix()
}, [])
async function loadMatrix() {
try {
setLoading(true)
const data = await api.getTierLimitsMatrix()
setMatrix(data)
setChanges({})
setError('')
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
function handleChange(tierId, featureId, value) {
const key = `${tierId}:${featureId}`
const newChanges = { ...changes }
// Parse value
let parsedValue = null
if (value === '' || value === 'unlimited' || value === '∞') {
parsedValue = null // unlimited
} else if (value === '0' || value === 'disabled') {
parsedValue = 0 // disabled
} else {
const num = parseInt(value)
if (!isNaN(num) && num >= 0) {
parsedValue = num
} else {
return // invalid input
}
}
newChanges[key] = { tierId, featureId, value: parsedValue }
setChanges(newChanges)
}
async function saveChanges() {
if (Object.keys(changes).length === 0) {
setSuccess('Keine Änderungen')
return
}
try {
setSaving(true)
setError('')
setSuccess('')
const updates = Object.values(changes).map(c => ({
tier_id: c.tierId,
feature_id: c.featureId,
limit_value: c.value
}))
await api.updateTierLimitsBatch(updates)
setSuccess(`${updates.length} Limits gespeichert`)
await loadMatrix()
} catch (e) {
setError(e.message)
} finally {
setSaving(false)
}
}
function getCurrentValue(tierId, featureId) {
const key = `${tierId}:${featureId}`
if (key in changes) {
return changes[key].value
}
return matrix.limits[key] ?? null
}
function formatValue(val) {
if (val === null) return '∞'
if (val === 0) return '❌'
return val.toString()
}
function groupFeaturesByCategory() {
const groups = {}
matrix.features.forEach(f => {
if (!groups[f.category]) groups[f.category] = []
groups[f.category].push(f)
})
return groups
}
if (loading) return (
<div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
<div className="spinner" />
</div>
)
const hasChanges = Object.keys(changes).length > 0
const categoryGroups = groupFeaturesByCategory()
const categoryIcons = { data: '📊', ai: '🤖', export: '📤', integration: '🔗' }
const categoryNames = { data: 'DATEN', ai: 'KI', export: 'EXPORT', integration: 'INTEGRATIONEN' }
return (
<div style={{ paddingBottom: 80 }}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: 20
}}>
<div>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text1)' }}>
Tier Limits Matrix
</div>
<div style={{ fontSize: 13, color: 'var(--text3)', marginTop: 4 }}>
Konfiguriere Feature-Limits pro Tier (∞ = unbegrenzt, ❌ = deaktiviert)
</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
{hasChanges && (
<button
className="btn btn-secondary"
onClick={() => { setChanges({}); setSuccess(''); setError('') }}
disabled={saving}
>
Zurücksetzen
</button>
)}
<button
className="btn btn-primary"
onClick={saveChanges}
disabled={saving || !hasChanges}
>
{saving ? 'Speichern...' : hasChanges ? `${Object.keys(changes).length} Änderungen speichern` : 'Speichern'}
</button>
</div>
</div>
{/* Messages */}
{error && (
<div style={{
padding: 12, background: 'var(--danger)', color: 'white',
borderRadius: 8, marginBottom: 16, fontSize: 14
}}>
{error}
</div>
)}
{success && (
<div style={{
padding: 12, background: 'var(--accent)', color: 'white',
borderRadius: 8, marginBottom: 16, fontSize: 14
}}>
{success}
</div>
)}
{/* Matrix Table */}
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
<table style={{
width: '100%', borderCollapse: 'collapse', fontSize: 13,
minWidth: 800
}}>
<thead>
<tr style={{ background: 'var(--surface2)' }}>
<th style={{
textAlign: 'left', padding: '12px 16px', fontWeight: 600,
position: 'sticky', left: 0, background: 'var(--surface2)', zIndex: 10,
borderRight: '1px solid var(--border)'
}}>
Feature
</th>
{matrix.tiers.map(tier => (
<th key={tier.id} style={{
textAlign: 'center', padding: '12px 16px', fontWeight: 600,
minWidth: 100
}}>
{tier.name}
</th>
))}
</tr>
</thead>
<tbody>
{Object.entries(categoryGroups).map(([category, features]) => (
<>
{/* Category Header */}
<tr key={`cat-${category}`} style={{ background: 'var(--accent-light)' }}>
<td colSpan={matrix.tiers.length + 1} style={{
padding: '8px 16px', fontWeight: 600, fontSize: 11,
textTransform: 'uppercase', letterSpacing: '0.5px',
color: 'var(--accent-dark)'
}}>
{categoryIcons[category]} {categoryNames[category] || category}
</td>
</tr>
{/* Feature Rows */}
{features.map((feature, idx) => (
<tr key={feature.id} style={{
borderBottom: idx === features.length - 1 ? '2px solid var(--border)' : '1px solid var(--border)'
}}>
<td style={{
padding: '8px 16px', fontWeight: 500,
position: 'sticky', left: 0, background: 'var(--bg)', zIndex: 5,
borderRight: '1px solid var(--border)'
}}>
<div>{feature.name}</div>
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
{feature.limit_type === 'boolean' ? '(boolean)' : `(count, reset: ${feature.reset_period})`}
</div>
</td>
{matrix.tiers.map(tier => {
const currentValue = getCurrentValue(tier.id, feature.id)
const isChanged = `${tier.id}:${feature.id}` in changes
return (
<td key={`${tier.id}-${feature.id}`} style={{
textAlign: 'center', padding: 8,
background: isChanged ? 'var(--accent-light)' : 'transparent'
}}>
<input
type="text"
value={formatValue(currentValue)}
onChange={(e) => handleChange(tier.id, feature.id, e.target.value)}
style={{
width: '80px',
padding: '4px 8px',
border: `1px solid ${isChanged ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 4,
textAlign: 'center',
fontSize: 13,
fontWeight: isChanged ? 600 : 400,
background: 'var(--bg)',
color: currentValue === 0 ? 'var(--danger)' :
currentValue === null ? 'var(--accent)' : 'var(--text1)'
}}
placeholder={feature.limit_type === 'boolean' ? '0/1' : '0-999'}
/>
</td>
)
})}
</tr>
))}
</>
))}
</tbody>
</table>
</div>
{/* Legend */}
<div style={{
marginTop: 16, padding: 12, background: 'var(--surface2)',
borderRadius: 8, fontSize: 12, color: 'var(--text3)'
}}>
<strong>Legende:</strong>
<div style={{ marginTop: 8, display: 'flex', gap: 24, flexWrap: 'wrap' }}>
<span><strong style={{ color: 'var(--accent)' }}>∞</strong> = Unbegrenzt (NULL)</span>
<span><strong style={{ color: 'var(--danger)' }}>❌</strong> = Deaktiviert (0)</span>
<span><strong>1-999</strong> = Limit-Wert</span>
<span>Boolean: 0 = deaktiviert, 1 = aktiviert</span>
</div>
</div>
</div>
)
}