Files
igny8/frontend/src/components/billing/BillingUsagePanel.tsx
2026-01-19 16:51:58 +00:00

277 lines
13 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { getCreditTransactions, CreditTransaction as BillingTransaction } from '../../services/billing.api';
import { useBillingStore } from '../../store/billingStore';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import { CompactPagination } from '../ui/pagination';
import { formatDateTime } from '../../utils/date';
// Credit costs per operation (copied from Billing usage page)
const CREDIT_COSTS: Record<string, { cost: number | string; description: string }> = {
clustering: { cost: 10, description: 'Per clustering request' },
idea_generation: { cost: 15, description: 'Per cluster → ideas request' },
content_generation: { cost: '1 per 100 words', description: 'Per 100 words generated' },
image_prompt_extraction: { cost: 2, description: 'Per content piece' },
image_generation: { cost: 5, description: 'Per image generated' },
linking: { cost: 8, description: 'Per content piece' },
optimization: { cost: '1 per 200 words', description: 'Per 200 words optimized' },
site_structure_generation: { cost: 50, description: 'Per site blueprint' },
site_page_generation: { cost: 20, description: 'Per page generated' },
};
interface BillingUsagePanelProps {
showOnlyActivity?: boolean;
}
export default function BillingUsagePanel({ showOnlyActivity = false }: BillingUsagePanelProps) {
const toast = useToast();
const [transactions, setTransactions] = useState<BillingTransaction[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const { balance, usageLimits, loadBalance, loadUsageLimits } = useBillingStore();
useEffect(() => {
loadUsage();
loadBalance();
loadUsageLimits();
}, [loadBalance, loadUsageLimits]);
const loadUsage = async () => {
try {
setLoading(true);
const txnData = await getCreditTransactions();
setTransactions(txnData.results || []);
} catch (error: any) {
toast.error(`Failed to load credit usage: ${error.message}`);
} finally {
setLoading(false);
}
};
const totalPages = Math.max(1, Math.ceil(transactions.length / pageSize));
const paginated = transactions.slice((page - 1) * pageSize, page * pageSize);
if (loading) {
return (
<div className="p-4">
<div className="text-gray-500">Loading credit usage...</div>
</div>
);
}
// If only showing activity table, render just that
if (showOnlyActivity) {
return (
<div>
<Card className="p-6">
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Credit Activity</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Complete history of credit transactions
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Date</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Type</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Amount</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Description</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Reference</th>
</tr>
</thead>
<tbody>
{paginated.map((txn) => (
<tr key={txn.id} className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{formatDateTime(txn.created_at)}</td>
<td className="py-3 px-4">
<Badge variant="soft" tone={txn.amount >= 0 ? 'success' : 'danger'}>{txn.transaction_type}</Badge>
</td>
<td className={`py-3 px-4 text-sm font-medium ${txn.amount >= 0 ? 'text-success-600 dark:text-success-400' : 'text-error-600 dark:text-error-400'}`}>
{txn.amount >= 0 ? '+' : ''}{txn.amount}
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{txn.description}</td>
<td className="py-3 px-4 text-sm text-gray-500 dark:text-gray-500">{txn.reference_id || '-'}</td>
</tr>
))}
{transactions.length === 0 && (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500 dark:text-gray-400">No transactions yet</td>
</tr>
)}
</tbody>
</table>
</div>
{transactions.length > 0 && (
<div className="mt-4 flex justify-between items-center">
<div className="text-sm text-gray-500 dark:text-gray-400">
Showing {(page - 1) * pageSize + 1}-{Math.min(page * pageSize, transactions.length)} of {transactions.length}
</div>
<CompactPagination
currentPage={page}
totalPages={totalPages}
pageSize={pageSize}
onPageChange={(p) => setPage(p)}
onPageSizeChange={(size) => {
setPageSize(size);
setPage(1);
}}
/>
</div>
)}
</Card>
</div>
);
}
return (
<div className="space-y-6">
{balance && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<Card className="p-6">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Current Balance</h3>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{(balance?.credits ?? 0).toLocaleString()}
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Available credits</p>
</Card>
<Card className="p-6">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Monthly Allocation</h3>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{(balance?.plan_credits_per_month ?? 0).toLocaleString()}
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{(balance as any)?.subscription_plan || 'No plan'}
</p>
</Card>
<Card className="p-6">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Status</h3>
<div className="mt-2">
<Badge variant="light" className="text-lg">
{(balance as any)?.subscription_status || 'No subscription'}
</Badge>
</div>
</Card>
</div>
)}
{usageLimits && (
<Card className="p-6">
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Plan Limits</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-600 dark:text-gray-400">Monthly Credits</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{usageLimits.plan_credits_per_month?.toLocaleString?.() || 0}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{usageLimits.credits_used_this_month?.toLocaleString?.() || 0} used
</div>
</div>
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-600 dark:text-gray-400">Remaining</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{usageLimits.credits_remaining?.toLocaleString?.() || 0}
</div>
{usageLimits.approaching_limit && (
<div className="text-sm text-warning-600 dark:text-warning-400 mt-1">Approaching limit</div>
)}
</div>
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-600 dark:text-gray-400">Usage %</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{usageLimits.percentage_used?.toFixed?.(0) || 0}%
</div>
</div>
</div>
</Card>
)}
<Card className="p-6">
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Credit Costs per Operation</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">Understanding how credits are consumed for each operation type</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(CREDIT_COSTS).map(([operation, info]) => (
<div key={operation} className="flex items-start justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-1">
<div className="font-medium text-gray-900 dark:text-white capitalize">
{operation.replace(/_/g, ' ')}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{info.description}
</div>
</div>
<div className="ml-4 text-right">
<Badge variant="light" color="primary" className="font-semibold">
{typeof info.cost === 'number' ? `${info.cost} credits` : info.cost}
</Badge>
</div>
</div>
))}
</div>
</Card>
<div>
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Credit Activity</h2>
<Card className="p-6">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Date</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Type</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Amount</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Description</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Reference</th>
</tr>
</thead>
<tbody>
{paginated.map((txn) => (
<tr key={txn.id} className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{formatDateTime(txn.created_at)}</td>
<td className="py-3 px-4">
<Badge variant="light" color={txn.amount >= 0 ? 'success' : 'error'}>{txn.transaction_type}</Badge>
</td>
<td className={`py-3 px-4 text-sm font-medium ${txn.amount >= 0 ? 'text-success-600 dark:text-success-400' : 'text-error-600 dark:text-error-400'}`}>
{txn.amount >= 0 ? '+' : ''}{txn.amount}
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{txn.description}</td>
<td className="py-3 px-4 text-sm text-gray-500 dark:text-gray-500">{txn.reference_id || '-'}</td>
</tr>
))}
{transactions.length === 0 && (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500 dark:text-gray-400">No transactions yet</td>
</tr>
)}
</tbody>
</table>
</div>
{transactions.length > 0 && (
<div className="mt-4 flex justify-between items-center">
<div className="text-sm text-gray-500 dark:text-gray-400">
Showing {(page - 1) * pageSize + 1}-{Math.min(page * pageSize, transactions.length)} of {transactions.length}
</div>
<CompactPagination
currentPage={page}
totalPages={totalPages}
pageSize={pageSize}
onPageChange={(p) => setPage(p)}
onPageSizeChange={(size) => {
setPageSize(size);
setPage(1);
}}
/>
</div>
)}
</Card>
</div>
</div>
);
}