143 lines
4.1 KiB
React
143 lines
4.1 KiB
React
|
|
/**
|
|||
|
|
* FeatureUsageOverview - Full feature usage table for Settings page
|
|||
|
|
*
|
|||
|
|
* Shows all features with usage, limits, reset period, and next reset date
|
|||
|
|
* Phase 3: Frontend Display
|
|||
|
|
*/
|
|||
|
|
import { useState, useEffect } from 'react'
|
|||
|
|
import { api } from '../utils/api'
|
|||
|
|
import './FeatureUsageOverview.css'
|
|||
|
|
|
|||
|
|
const RESET_PERIOD_LABELS = {
|
|||
|
|
'never': 'Niemals',
|
|||
|
|
'daily': 'Täglich',
|
|||
|
|
'monthly': 'Monatlich'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const CATEGORY_LABELS = {
|
|||
|
|
'data': 'Daten',
|
|||
|
|
'ai': 'KI',
|
|||
|
|
'export': 'Export',
|
|||
|
|
'import': 'Import',
|
|||
|
|
'integration': 'Integration'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function FeatureUsageOverview() {
|
|||
|
|
const [features, setFeatures] = useState([])
|
|||
|
|
const [loading, setLoading] = useState(true)
|
|||
|
|
const [error, setError] = useState(null)
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadFeatures()
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
const loadFeatures = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true)
|
|||
|
|
const data = await api.getFeatureUsage()
|
|||
|
|
setFeatures(data)
|
|||
|
|
setError(null)
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('Failed to load feature usage:', err)
|
|||
|
|
setError('Fehler beim Laden der Kontingente')
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const formatResetDate = (resetAt) => {
|
|||
|
|
if (!resetAt) return '—'
|
|||
|
|
try {
|
|||
|
|
const date = new Date(resetAt)
|
|||
|
|
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
|||
|
|
} catch {
|
|||
|
|
return '—'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getStatusClass = (feature) => {
|
|||
|
|
if (!feature.allowed || feature.remaining < 0) return 'exceeded'
|
|||
|
|
if (feature.limit && feature.remaining <= Math.ceil(feature.limit * 0.2)) return 'warning'
|
|||
|
|
return 'ok'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Group by category
|
|||
|
|
const byCategory = features.reduce((acc, f) => {
|
|||
|
|
const cat = f.category || 'other'
|
|||
|
|
if (!acc[cat]) acc[cat] = []
|
|||
|
|
acc[cat].push(f)
|
|||
|
|
return acc
|
|||
|
|
}, {})
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<div className="feature-usage-loading">
|
|||
|
|
<div className="spinner" />
|
|||
|
|
Lade Kontingente...
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error) {
|
|||
|
|
return (
|
|||
|
|
<div className="feature-usage-error">
|
|||
|
|
⚠️ {error}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (features.length === 0) {
|
|||
|
|
return (
|
|||
|
|
<div className="feature-usage-empty">
|
|||
|
|
Keine Features gefunden
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="feature-usage-overview">
|
|||
|
|
{Object.entries(byCategory).map(([category, items]) => (
|
|||
|
|
<div key={category} className="feature-category">
|
|||
|
|
<div className="feature-category-label">
|
|||
|
|
{CATEGORY_LABELS[category] || category}
|
|||
|
|
</div>
|
|||
|
|
<div className="feature-list">
|
|||
|
|
{items.map(feature => (
|
|||
|
|
<div key={feature.feature_id} className={`feature-item feature-item--${getStatusClass(feature)}`}>
|
|||
|
|
<div className="feature-main">
|
|||
|
|
<div className="feature-name">{feature.name}</div>
|
|||
|
|
<div className="feature-usage">
|
|||
|
|
{feature.limit === null ? (
|
|||
|
|
<span className="usage-unlimited">Unbegrenzt</span>
|
|||
|
|
) : feature.limit_type === 'boolean' ? (
|
|||
|
|
<span className={`usage-boolean ${feature.allowed ? 'enabled' : 'disabled'}`}>
|
|||
|
|
{feature.allowed ? '✓ Aktiviert' : '✗ Deaktiviert'}
|
|||
|
|
</span>
|
|||
|
|
) : (
|
|||
|
|
<span className="usage-count">
|
|||
|
|
<strong>{feature.used}</strong> / {feature.limit}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
{feature.limit_type === 'count' && feature.limit !== null && (
|
|||
|
|
<div className="feature-meta">
|
|||
|
|
<span className="meta-reset">
|
|||
|
|
Reset: {RESET_PERIOD_LABELS[feature.reset_period] || feature.reset_period}
|
|||
|
|
</span>
|
|||
|
|
{feature.reset_at && (
|
|||
|
|
<span className="meta-next-reset">
|
|||
|
|
Nächster Reset: {formatResetDate(feature.reset_at)}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|