diff --git a/frontend/src/components/FeatureUsageOverview.css b/frontend/src/components/FeatureUsageOverview.css new file mode 100644 index 0000000..f3db96c --- /dev/null +++ b/frontend/src/components/FeatureUsageOverview.css @@ -0,0 +1,163 @@ +/** + * FeatureUsageOverview Styles + * Phase 3: Frontend Display + */ + +.feature-usage-overview { + display: flex; + flex-direction: column; + gap: 16px; +} + +.feature-usage-loading, +.feature-usage-error, +.feature-usage-empty { + padding: 20px; + text-align: center; + color: var(--text2); + font-size: 14px; +} + +.feature-usage-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.feature-usage-error { + background: rgba(216, 90, 48, 0.1); + color: var(--danger); + border-radius: 8px; +} + +/* Category grouping */ +.feature-category { + display: flex; + flex-direction: column; + gap: 8px; +} + +.feature-category-label { + font-size: 11px; + font-weight: 600; + color: var(--text3); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.feature-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Feature item */ +.feature-item { + padding: 12px; + background: var(--surface2); + border-radius: 8px; + border: 1px solid var(--border); + transition: all 0.2s; +} + +.feature-item--exceeded { + border-color: var(--danger); + background: rgba(216, 90, 48, 0.05); +} + +.feature-item--warning { + border-color: #d97706; + background: rgba(217, 119, 6, 0.05); +} + +.feature-item--ok { + border-color: var(--border); +} + +.feature-main { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.feature-name { + font-size: 14px; + font-weight: 500; + color: var(--text1); +} + +.feature-usage { + font-size: 14px; + white-space: nowrap; +} + +.usage-unlimited { + color: var(--accent); + font-weight: 500; +} + +.usage-boolean { + font-weight: 500; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; +} + +.usage-boolean.enabled { + color: var(--accent); + background: rgba(29, 158, 117, 0.1); +} + +.usage-boolean.disabled { + color: var(--text3); + background: rgba(136, 135, 128, 0.1); +} + +.usage-count { + color: var(--text1); + font-variant-numeric: tabular-nums; +} + +.usage-count strong { + font-weight: 600; +} + +.feature-item--exceeded .usage-count { + color: var(--danger); +} + +.feature-item--warning .usage-count { + color: #d97706; +} + +/* Meta info */ +.feature-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; + font-size: 11px; + color: var(--text3); +} + +.meta-reset, +.meta-next-reset { + padding: 2px 6px; + background: var(--surface); + border-radius: 4px; +} + +/* Responsive */ +@media (max-width: 640px) { + .feature-main { + flex-direction: column; + align-items: flex-start; + gap: 6px; + } + + .feature-usage { + width: 100%; + } +} diff --git a/frontend/src/components/FeatureUsageOverview.jsx b/frontend/src/components/FeatureUsageOverview.jsx new file mode 100644 index 0000000..b68d1cc --- /dev/null +++ b/frontend/src/components/FeatureUsageOverview.jsx @@ -0,0 +1,142 @@ +/** + * 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 ( +
+ Übersicht über deine Feature-Nutzung und verfügbare Kontingente. +
+