281 lines
12 KiB
TypeScript
281 lines
12 KiB
TypeScript
/**
|
|
* Usage & Analytics Page - Refactored
|
|
* Organized tabs: Plan Limits & Usage, Credit Activity, API Usage
|
|
* Tab selection driven by URL path for sidebar navigation
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useLocation } from 'react-router-dom';
|
|
import { TrendingUp, Activity, BarChart3, Zap, Calendar } from 'lucide-react';
|
|
import PageMeta from '../../components/common/PageMeta';
|
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
|
import { getUsageAnalytics, UsageAnalytics, getCreditBalance, type CreditBalance } from '../../services/billing.api';
|
|
import { Card } from '../../components/ui/card';
|
|
import Badge from '../../components/ui/badge/Badge';
|
|
import BillingUsagePanel from '../../components/billing/BillingUsagePanel';
|
|
import UsageLimitsPanel from '../../components/billing/UsageLimitsPanel';
|
|
import Button from '../../components/ui/button/Button';
|
|
|
|
type TabType = 'limits' | 'activity' | 'api';
|
|
|
|
// Map URL paths to tab types
|
|
function getTabFromPath(pathname: string): TabType {
|
|
if (pathname.includes('/credits')) return 'activity';
|
|
if (pathname.includes('/activity')) return 'api';
|
|
return 'limits';
|
|
}
|
|
|
|
export default function UsageAnalyticsPage() {
|
|
const toast = useToast();
|
|
const location = useLocation();
|
|
// Derive active tab from URL path
|
|
const activeTab = getTabFromPath(location.pathname);
|
|
const [analytics, setAnalytics] = useState<UsageAnalytics | null>(null);
|
|
const [creditBalance, setCreditBalance] = useState<CreditBalance | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [period, setPeriod] = useState(30);
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, [period]);
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [analyticsData, balanceData] = await Promise.all([
|
|
getUsageAnalytics(period),
|
|
getCreditBalance(),
|
|
]);
|
|
setAnalytics(analyticsData);
|
|
setCreditBalance(balanceData);
|
|
} catch (error: any) {
|
|
toast.error(`Failed to load usage data: ${error.message}`);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-6">
|
|
<PageMeta title="Usage & Analytics" description="Monitor your plan limits and usage" />
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500"></div>
|
|
<div className="text-gray-500 dark:text-gray-400">Loading usage data...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const tabTitles: Record<TabType, string> = {
|
|
limits: 'Limits & Usage',
|
|
activity: 'Credit History',
|
|
api: 'Activity Log',
|
|
};
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<PageMeta title="Usage & Analytics" description="Monitor your plan limits and usage" />
|
|
|
|
{/* Page Header */}
|
|
<div className="mb-6">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">
|
|
Usage & Analytics / {tabTitles[activeTab]}
|
|
</div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{tabTitles[activeTab]}</h1>
|
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
{activeTab === 'limits' && 'See how much you\'re using - Track your credits and content limits'}
|
|
{activeTab === 'activity' && 'See where your credits go - Track credit usage history'}
|
|
{activeTab === 'api' && 'Technical requests - Monitor API activity and usage'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Quick Stats Overview */}
|
|
{creditBalance && (
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
<Card className="p-4 bg-gradient-to-br from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/10 border-brand-200 dark:border-brand-700">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-brand-500 rounded-lg">
|
|
<Zap className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-brand-700 dark:text-brand-300">Credits Left</div>
|
|
<div className="text-2xl font-bold text-brand-600 dark:text-brand-400">
|
|
{creditBalance.credits.toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-brand-600 dark:text-brand-400 mt-1">available</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4 bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/10 border-purple-200 dark:border-purple-700">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-purple-500 rounded-lg">
|
|
<TrendingUp className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-purple-700 dark:text-purple-300">Credits Used This Month</div>
|
|
<div className="text-2xl font-bold text-purple-600 dark:text-purple-400">
|
|
{creditBalance.credits_used_this_month.toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-purple-600 dark:text-purple-400 mt-1">spent so far</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4 bg-gradient-to-br from-success-50 to-success-100 dark:from-success-900/20 dark:to-success-800/10 border-success-200 dark:border-success-700">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-success-500 rounded-lg">
|
|
<BarChart3 className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-success-700 dark:text-success-300">Your Monthly Limit</div>
|
|
<div className="text-2xl font-bold text-success-600 dark:text-success-400">
|
|
{creditBalance.plan_credits_per_month.toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-success-600 dark:text-success-400 mt-1">total each month</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4 bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/10 border-purple-200 dark:border-purple-700">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-purple-500 rounded-lg">
|
|
<Calendar className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-purple-700 dark:text-purple-300">Usage %</div>
|
|
<div className="text-2xl font-bold text-purple-600 dark:text-purple-400">
|
|
{creditBalance.plan_credits_per_month > 0
|
|
? Math.round((creditBalance.credits_used_this_month / creditBalance.plan_credits_per_month) * 100)
|
|
: 0}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Period Selector (only show on activity and api tabs) */}
|
|
{(activeTab === 'activity' || activeTab === 'api') && (
|
|
<div className="mb-6 flex justify-end">
|
|
<div className="flex gap-2">
|
|
{[7, 30, 90].map((value) => {
|
|
const isActive = period === value;
|
|
return (
|
|
<Button
|
|
key={value}
|
|
size="sm"
|
|
variant={isActive ? 'primary' : 'outline'}
|
|
tone={isActive ? 'brand' : 'neutral'}
|
|
onClick={() => setPeriod(value)}
|
|
>
|
|
{value} Days
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tab Content */}
|
|
<div className="mt-6">
|
|
{/* Plan Limits & Usage Tab */}
|
|
{activeTab === 'limits' && (
|
|
<div className="space-y-6">
|
|
<UsageLimitsPanel />
|
|
</div>
|
|
)}
|
|
|
|
{/* Credit Activity Tab */}
|
|
{activeTab === 'activity' && (
|
|
<div className="space-y-6">
|
|
<BillingUsagePanel showOnlyActivity={true} />
|
|
</div>
|
|
)}
|
|
|
|
{/* API Usage Tab */}
|
|
{activeTab === 'api' && (
|
|
<div className="space-y-6">
|
|
{/* API Stats Cards - Using real analytics data */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
|
|
<Activity className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
|
</div>
|
|
<div className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Operations</div>
|
|
</div>
|
|
<div className="text-3xl font-bold text-brand-600 dark:text-brand-400">
|
|
{analytics?.usage_by_type.reduce((sum, item) => sum + item.count, 0).toLocaleString() || 0}
|
|
</div>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">in last {period} days</div>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
|
<BarChart3 className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<div className="text-sm font-medium text-gray-600 dark:text-gray-400">Avg Operations/Day</div>
|
|
</div>
|
|
<div className="text-3xl font-bold text-purple-600 dark:text-purple-400">
|
|
{Math.round((analytics?.usage_by_type.reduce((sum, item) => sum + item.count, 0) || 0) / period)}
|
|
</div>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">daily average</div>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
|
|
<TrendingUp className="w-5 h-5 text-success-600 dark:text-success-400" />
|
|
</div>
|
|
<div className="text-sm font-medium text-gray-600 dark:text-gray-400">Credits Used</div>
|
|
</div>
|
|
<div className="text-3xl font-bold text-success-600 dark:text-success-400">
|
|
{analytics?.total_usage?.toLocaleString() || 0}
|
|
</div>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">in last {period} days</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Operations by Type */}
|
|
<Card className="p-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
Operations by Type
|
|
</h2>
|
|
{analytics?.usage_by_type && analytics.usage_by_type.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{analytics.usage_by_type.map((item, index) => (
|
|
<div key={index} className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors">
|
|
<div className="flex-1">
|
|
<div className="font-medium text-gray-900 dark:text-white capitalize">
|
|
{item.transaction_type.replace(/_/g, ' ')}
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
{Math.abs(item.total).toLocaleString()} credits
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-xl font-bold text-gray-900 dark:text-white">{item.count}</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">operations</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
<Activity className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
|
<p>No operations recorded in the selected period</p>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|