451 lines
14 KiB
TypeScript
451 lines
14 KiB
TypeScript
/**
|
|
* Credit Insights Charts Component
|
|
* Displays credit usage analytics with visual charts
|
|
* - Donut chart: Credits by operation type
|
|
* - Line chart: Daily credit usage timeline
|
|
* - Bar chart: Top credit-consuming operations
|
|
*/
|
|
|
|
import Chart from 'react-apexcharts';
|
|
import { ApexOptions } from 'apexcharts';
|
|
import { Card } from '../ui/card';
|
|
import { ActivityIcon, TrendingUpIcon, PieChartIcon, BarChart3Icon } from '../../icons';
|
|
import type { UsageAnalytics } from '../../services/billing.api';
|
|
|
|
interface CreditInsightsChartsProps {
|
|
analytics: UsageAnalytics | null;
|
|
loading?: boolean;
|
|
period: number;
|
|
}
|
|
|
|
// Friendly names for operation types
|
|
const OPERATION_LABELS: Record<string, string> = {
|
|
content_generation: 'Content Generation',
|
|
image_generation: 'Image Generation',
|
|
keyword_clustering: 'Keyword Clustering',
|
|
content_analysis: 'Content Analysis',
|
|
subscription: 'Subscription',
|
|
purchase: 'Credit Purchase',
|
|
refund: 'Refund',
|
|
adjustment: 'Adjustment',
|
|
grant: 'Credit Grant',
|
|
deduction: 'Deduction',
|
|
};
|
|
|
|
const CHART_COLORS = [
|
|
'var(--color-brand-500)',
|
|
'var(--color-info-500)',
|
|
'var(--color-success-500)',
|
|
'var(--color-warning-500)',
|
|
'var(--color-error-500)',
|
|
'var(--color-info-500)',
|
|
'#6366f1', // indigo
|
|
'#ec4899', // pink
|
|
'#14b8a6', // teal
|
|
'#f97316', // orange
|
|
];
|
|
|
|
export default function CreditInsightsCharts({ analytics, loading, period }: CreditInsightsChartsProps) {
|
|
if (loading) {
|
|
return (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{[1, 2, 3, 4].map((i) => (
|
|
<Card key={i} className="p-6 animate-pulse">
|
|
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/3 mb-4"></div>
|
|
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!analytics) {
|
|
return (
|
|
<Card className="p-6">
|
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
<PieChartIcon className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
|
<p>No analytics data available</p>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Prepare data for donut chart (credits by operation type)
|
|
const usageByType = analytics.usage_by_type.filter(item => Math.abs(item.total) > 0);
|
|
const donutLabels = usageByType.map(item => OPERATION_LABELS[item.transaction_type] || item.transaction_type.replace(/_/g, ' '));
|
|
const donutSeries = usageByType.map(item => Math.abs(item.total));
|
|
|
|
const donutOptions: ApexOptions = {
|
|
chart: {
|
|
type: 'donut',
|
|
fontFamily: 'Outfit, sans-serif',
|
|
},
|
|
labels: donutLabels,
|
|
colors: CHART_COLORS.slice(0, donutLabels.length),
|
|
legend: {
|
|
position: 'bottom',
|
|
fontFamily: 'Outfit',
|
|
labels: {
|
|
colors: 'var(--color-gray-600)',
|
|
},
|
|
},
|
|
plotOptions: {
|
|
pie: {
|
|
donut: {
|
|
size: '65%',
|
|
labels: {
|
|
show: true,
|
|
name: {
|
|
show: true,
|
|
fontSize: '14px',
|
|
fontFamily: 'Outfit',
|
|
color: 'var(--color-gray-600)',
|
|
},
|
|
value: {
|
|
show: true,
|
|
fontSize: '24px',
|
|
fontFamily: 'Outfit',
|
|
fontWeight: 600,
|
|
color: 'var(--color-gray-900)',
|
|
formatter: (val: string) => parseInt(val).toLocaleString(),
|
|
},
|
|
total: {
|
|
show: true,
|
|
label: 'Total Credits',
|
|
fontSize: '14px',
|
|
fontFamily: 'Outfit',
|
|
color: 'var(--color-gray-600)',
|
|
formatter: () => analytics.total_usage.toLocaleString(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dataLabels: {
|
|
enabled: false,
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: (val: number) => `${val.toLocaleString()} credits`,
|
|
},
|
|
},
|
|
responsive: [
|
|
{
|
|
breakpoint: 480,
|
|
options: {
|
|
chart: {
|
|
width: 300,
|
|
},
|
|
legend: {
|
|
position: 'bottom',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
// Prepare data for timeline chart (daily usage)
|
|
const dailyData = analytics.daily_usage || [];
|
|
const timelineCategories = dailyData.map(d => {
|
|
const date = new Date(d.date);
|
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
});
|
|
const usageSeries = dailyData.map(d => Math.abs(d.usage));
|
|
const purchasesSeries = dailyData.map(d => d.purchases);
|
|
|
|
const timelineOptions: ApexOptions = {
|
|
chart: {
|
|
type: 'area',
|
|
fontFamily: 'Outfit, sans-serif',
|
|
height: 300,
|
|
toolbar: {
|
|
show: false,
|
|
},
|
|
zoom: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
colors: ['var(--color-brand-500)', 'var(--color-success-500)'],
|
|
dataLabels: {
|
|
enabled: false,
|
|
},
|
|
stroke: {
|
|
curve: 'smooth',
|
|
width: 2,
|
|
},
|
|
fill: {
|
|
type: 'gradient',
|
|
gradient: {
|
|
shadeIntensity: 1,
|
|
opacityFrom: 0.4,
|
|
opacityTo: 0.1,
|
|
stops: [0, 90, 100],
|
|
},
|
|
},
|
|
xaxis: {
|
|
categories: timelineCategories,
|
|
axisBorder: {
|
|
show: false,
|
|
},
|
|
axisTicks: {
|
|
show: false,
|
|
},
|
|
labels: {
|
|
style: {
|
|
colors: 'var(--color-gray-500)',
|
|
fontFamily: 'Outfit',
|
|
},
|
|
rotate: -45,
|
|
rotateAlways: dailyData.length > 14,
|
|
},
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
style: {
|
|
colors: 'var(--color-gray-500)',
|
|
fontFamily: 'Outfit',
|
|
},
|
|
formatter: (val: number) => val.toLocaleString(),
|
|
},
|
|
},
|
|
grid: {
|
|
borderColor: 'var(--color-gray-200)',
|
|
strokeDashArray: 4,
|
|
},
|
|
legend: {
|
|
position: 'top',
|
|
horizontalAlign: 'right',
|
|
fontFamily: 'Outfit',
|
|
labels: {
|
|
colors: 'var(--color-gray-600)',
|
|
},
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: (val: number) => `${val.toLocaleString()} credits`,
|
|
},
|
|
},
|
|
};
|
|
|
|
const timelineSeries = [
|
|
{ name: 'Credits Used', data: usageSeries },
|
|
{ name: 'Credits Added', data: purchasesSeries },
|
|
];
|
|
|
|
// Prepare data for bar chart (top operations by count)
|
|
const operationsByCount = [...analytics.usage_by_type]
|
|
.filter(item => item.count > 0)
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 8);
|
|
|
|
const barCategories = operationsByCount.map(item =>
|
|
OPERATION_LABELS[item.transaction_type] || item.transaction_type.replace(/_/g, ' ')
|
|
);
|
|
const barSeries = operationsByCount.map(item => item.count);
|
|
|
|
const barOptions: ApexOptions = {
|
|
chart: {
|
|
type: 'bar',
|
|
fontFamily: 'Outfit, sans-serif',
|
|
height: 300,
|
|
toolbar: {
|
|
show: false,
|
|
},
|
|
},
|
|
colors: ['var(--color-info-500)'],
|
|
plotOptions: {
|
|
bar: {
|
|
horizontal: true,
|
|
borderRadius: 4,
|
|
barHeight: '60%',
|
|
},
|
|
},
|
|
dataLabels: {
|
|
enabled: false,
|
|
},
|
|
xaxis: {
|
|
categories: barCategories,
|
|
labels: {
|
|
style: {
|
|
colors: 'var(--color-gray-500)',
|
|
fontFamily: 'Outfit',
|
|
},
|
|
},
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
style: {
|
|
colors: 'var(--color-gray-600)',
|
|
fontFamily: 'Outfit',
|
|
},
|
|
},
|
|
},
|
|
grid: {
|
|
borderColor: 'var(--color-gray-200)',
|
|
strokeDashArray: 4,
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: (val: number) => `${val.toLocaleString()} operations`,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Summary stats
|
|
const avgDailyUsage = dailyData.length > 0
|
|
? Math.round(dailyData.reduce((sum, d) => sum + Math.abs(d.usage), 0) / dailyData.length)
|
|
: 0;
|
|
|
|
const peakUsage = dailyData.length > 0
|
|
? Math.max(...dailyData.map(d => Math.abs(d.usage)))
|
|
: 0;
|
|
|
|
const topOperation = usageByType.length > 0
|
|
? usageByType.reduce((max, item) => Math.abs(item.total) > Math.abs(max.total) ? item : max, usageByType[0])
|
|
: null;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Summary Stats */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<Card className="p-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
|
|
<TrendingUpIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-gray-600 dark:text-gray-400">Avg Daily Usage</div>
|
|
<div className="text-xl font-bold text-brand-600 dark:text-brand-400">
|
|
{avgDailyUsage.toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">credits/day</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-info-100 dark:bg-info-900/30 rounded-lg">
|
|
<BarChart3Icon className="w-5 h-5 text-info-600 dark:text-info-400" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-gray-600 dark:text-gray-400">Peak Usage</div>
|
|
<div className="text-xl font-bold text-info-600 dark:text-info-400">
|
|
{peakUsage.toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">credits in one day</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
|
|
<ActivityIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-gray-600 dark:text-gray-400">Top Operation</div>
|
|
<div className="text-base font-bold text-success-600 dark:text-success-400 truncate max-w-[150px]">
|
|
{topOperation
|
|
? (OPERATION_LABELS[topOperation.transaction_type] || topOperation.transaction_type.replace(/_/g, ' '))
|
|
: 'N/A'
|
|
}
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
{topOperation ? `${Math.abs(topOperation.total).toLocaleString()} credits` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Charts Grid */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Usage by Type - Donut Chart */}
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
|
|
<PieChartIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Credits by Type
|
|
</h3>
|
|
</div>
|
|
{donutSeries.length > 0 ? (
|
|
<Chart
|
|
options={donutOptions}
|
|
series={donutSeries}
|
|
type="donut"
|
|
height={320}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-64 text-gray-500 dark:text-gray-400">
|
|
<div className="text-center">
|
|
<PieChartIcon className="w-12 h-12 mx-auto mb-2 opacity-30" />
|
|
<p>No usage data for this period</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Operations by Count - Bar Chart */}
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="p-2 bg-info-100 dark:bg-info-900/30 rounded-lg">
|
|
<BarChart3Icon className="w-5 h-5 text-info-600 dark:text-info-400" />
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Operations Count
|
|
</h3>
|
|
</div>
|
|
{barSeries.length > 0 ? (
|
|
<Chart
|
|
options={barOptions}
|
|
series={[{ name: 'Operations', data: barSeries }]}
|
|
type="bar"
|
|
height={320}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-64 text-gray-500 dark:text-gray-400">
|
|
<div className="text-center">
|
|
<BarChart3Icon className="w-12 h-12 mx-auto mb-2 opacity-30" />
|
|
<p>No operations in this period</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Daily Timeline - Full Width */}
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
|
|
<TrendingUpIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Credit Activity Timeline
|
|
</h3>
|
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
Last {period} days
|
|
</span>
|
|
</div>
|
|
{dailyData.length > 0 ? (
|
|
<Chart
|
|
options={timelineOptions}
|
|
series={timelineSeries}
|
|
type="area"
|
|
height={300}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-64 text-gray-500 dark:text-gray-400">
|
|
<div className="text-center">
|
|
<TrendingUpIcon className="w-12 h-12 mx-auto mb-2 opacity-30" />
|
|
<p>No daily activity data available</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|