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 ( +
+
+ Lade Kontingente... +
+ ) + } + + if (error) { + return ( +
+ ⚠️ {error} +
+ ) + } + + if (features.length === 0) { + return ( +
+ Keine Features gefunden +
+ ) + } + + return ( +
+ {Object.entries(byCategory).map(([category, items]) => ( +
+
+ {CATEGORY_LABELS[category] || category} +
+
+ {items.map(feature => ( +
+
+
{feature.name}
+
+ {feature.limit === null ? ( + Unbegrenzt + ) : feature.limit_type === 'boolean' ? ( + + {feature.allowed ? '✓ Aktiviert' : '✗ Deaktiviert'} + + ) : ( + + {feature.used} / {feature.limit} + + )} +
+
+ {feature.limit_type === 'count' && feature.limit !== null && ( +
+ + Reset: {RESET_PERIOD_LABELS[feature.reset_period] || feature.reset_period} + + {feature.reset_at && ( + + Nächster Reset: {formatResetDate(feature.reset_at)} + + )} +
+ )} +
+ ))} +
+
+ ))} +
+ ) +} diff --git a/frontend/src/components/UsageBadge.css b/frontend/src/components/UsageBadge.css new file mode 100644 index 0000000..8675461 --- /dev/null +++ b/frontend/src/components/UsageBadge.css @@ -0,0 +1,43 @@ +/** + * UsageBadge Styles + * + * Small, visually distinct inline badge for usage indicators + * Phase 3: Frontend Display + */ + +.usage-badge { + display: inline-block; + font-size: 0.75rem; + font-weight: 500; + padding: 2px 6px; + border-radius: 4px; + margin-left: 6px; + opacity: 0.8; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.usage-badge--ok { + color: var(--accent, #1D9E75); + background: rgba(29, 158, 117, 0.1); +} + +.usage-badge--warning { + color: #d97706; + background: rgba(217, 119, 6, 0.1); +} + +.usage-badge--exceeded { + color: var(--danger, #D85A30); + background: rgba(216, 90, 48, 0.1); + font-weight: 600; +} + +/* Responsive: Even smaller on mobile */ +@media (max-width: 640px) { + .usage-badge { + font-size: 0.7rem; + padding: 1px 4px; + margin-left: 4px; + } +} diff --git a/frontend/src/components/UsageBadge.jsx b/frontend/src/components/UsageBadge.jsx new file mode 100644 index 0000000..8fdccb4 --- /dev/null +++ b/frontend/src/components/UsageBadge.jsx @@ -0,0 +1,31 @@ +/** + * UsageBadge - Small inline usage indicator + * + * Shows usage quota in format: (used/limit) + * Color-coded: green (ok), yellow (warning), red (exceeded) + * + * Phase 3: Frontend Display + */ +import './UsageBadge.css' + +export default function UsageBadge({ used, limit, remaining, allowed }) { + // Don't show badge if unlimited + if (limit === null || limit === undefined) { + return null + } + + // Determine status for color coding + let status = 'ok' + if (!allowed || remaining < 0) { + status = 'exceeded' + } else if (limit > 0 && remaining <= Math.ceil(limit * 0.2)) { + // Warning at 20% or less remaining + status = 'warning' + } + + return ( + + ({used}/{limit}) + + ) +} diff --git a/frontend/src/pages/SettingsPage.jsx b/frontend/src/pages/SettingsPage.jsx index b5a0de0..704d84a 100644 --- a/frontend/src/pages/SettingsPage.jsx +++ b/frontend/src/pages/SettingsPage.jsx @@ -1,10 +1,11 @@ import { useState } from 'react' -import { Save, Download, Upload, Trash2, Plus, Check, Pencil, X, LogOut, Shield, Key } from 'lucide-react' +import { Save, Download, Upload, Trash2, Plus, Check, Pencil, X, LogOut, Shield, Key, BarChart3 } from 'lucide-react' import { useProfile } from '../context/ProfileContext' import { useAuth } from '../context/AuthContext' import { Avatar } from './ProfileSelect' import { api } from '../utils/api' import AdminPanel from './AdminPanel' +import FeatureUsageOverview from '../components/FeatureUsageOverview' const COLORS = ['#1D9E75','#378ADD','#D85A30','#EF9F27','#7F77DD','#D4537E','#639922','#888780'] @@ -326,6 +327,17 @@ export default function SettingsPage() {
+ {/* Feature Usage Overview (Phase 3) */} +
+
+ Kontingente +
+

+ Übersicht über deine Feature-Nutzung und verfügbare Kontingente. +

+ +
+ {/* Admin Panel */} {isAdmin && (
diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 6569c5a..62a5181 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -144,6 +144,7 @@ export const api = { getMyUsage: () => req('/subscription/usage'), getMyLimits: () => req('/subscription/limits'), redeemCoupon: (code) => req('/coupons/redeem',json({code})), + getFeatureUsage: () => req('/features/usage'), // Phase 3: Usage overview // Admin: Features listFeatures: () => req('/features'),