Files
igny8/frontend/src/components/billing/CreditInsightsCharts.tsx
IGNY8 VPS (Salman) 75deda304e reanme purple to info
2026-01-24 15:27:51 +00:00

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>
);
}