fina autoamtiona adn billing and credits

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-04 15:54:15 +00:00
parent f8a9293196
commit 40dfe20ead
40 changed files with 5680 additions and 18 deletions

View File

@@ -0,0 +1,362 @@
/**
* Credits & Billing Page
* User-facing credits usage, transactions, and billing information
*/
import React, { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import ComponentCard from '../../components/common/ComponentCard';
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import Button from '../../components/ui/button/Button';
import Badge from '../../components/ui/badge/Badge';
import {
BoltIcon,
DollarLineIcon,
ClockIcon,
CheckCircleIcon
} from '../../icons';
interface CreditTransaction {
id: number;
transaction_type: string;
amount: number;
balance_after: number;
description: string;
created_at: string;
}
interface CreditUsageLog {
id: number;
operation_type: string;
credits_used: number;
model_used: string;
created_at: string;
metadata: any;
}
interface AccountBalance {
credits: number;
subscription_plan: string;
monthly_credits_included: number;
bonus_credits: number;
}
const CreditsAndBilling: React.FC = () => {
const toast = useToast();
const [balance, setBalance] = useState<AccountBalance | null>(null);
const [transactions, setTransactions] = useState<CreditTransaction[]>([]);
const [usageLogs, setUsageLogs] = useState<CreditUsageLog[]>([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<'overview' | 'transactions' | 'usage'>('overview');
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
const [balanceData, transactionsData, usageData] = await Promise.all([
fetchAPI('/v1/billing/account_balance/'),
fetchAPI('/v1/billing/transactions/?limit=50'),
fetchAPI('/v1/billing/usage/?limit=50'),
]);
setBalance(balanceData);
setTransactions(transactionsData.results || []);
setUsageLogs(usageData.results || []);
} catch (error: any) {
toast?.error(error?.message || 'Failed to load billing data');
} finally {
setLoading(false);
}
};
const getTransactionTypeColor = (type: string) => {
switch (type) {
case 'purchase': return 'success';
case 'grant': return 'info';
case 'deduction': return 'warning';
case 'refund': return 'primary';
case 'adjustment': return 'secondary';
default: return 'default';
}
};
const formatOperationType = (type: string) => {
return type.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Credits & Billing" description="Manage your credits and billing" />
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading billing data...</p>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Credits & Billing" description="Manage your credits and billing" />
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Credits & Billing</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage your credits, view transactions, and monitor usage
</p>
</div>
<Button variant="primary" onClick={() => {
// TODO: Link to purchase credits page
toast?.info('Purchase credits feature coming soon');
}}>
<DollarLineIcon className="w-4 h-4 mr-2" />
Purchase Credits
</Button>
</div>
{/* Credit Balance Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<EnhancedMetricCard
title="Current Balance"
value={balance?.credits || 0}
icon={BoltIcon}
color="amber"
iconColor="text-amber-500"
/>
<EnhancedMetricCard
title="Monthly Included"
value={balance?.monthly_credits_included || 0}
subtitle={balance?.subscription_plan || 'Free'}
icon={CheckCircleIcon}
color="green"
iconColor="text-green-500"
/>
<EnhancedMetricCard
title="Bonus Credits"
value={balance?.bonus_credits || 0}
icon={DollarLineIcon}
color="blue"
iconColor="text-blue-500"
/>
<EnhancedMetricCard
title="Total This Month"
value={usageLogs.reduce((sum, log) => sum + log.credits_used, 0)}
icon={ClockIcon}
color="purple"
iconColor="text-purple-500"
/>
</div>
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('overview')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'overview'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
Overview
</button>
<button
onClick={() => setActiveTab('transactions')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'transactions'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
Transactions ({transactions.length})
</button>
<button
onClick={() => setActiveTab('usage')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'usage'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
Usage History ({usageLogs.length})
</button>
</nav>
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Recent Transactions */}
<ComponentCard title="Recent Transactions">
<div className="space-y-3">
{transactions.slice(0, 5).map((transaction) => (
<div key={transaction.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-1">
<div className="flex items-center gap-2">
<Badge variant={getTransactionTypeColor(transaction.transaction_type)}>
{transaction.transaction_type}
</Badge>
<span className="text-sm text-gray-900 dark:text-white">
{transaction.description}
</span>
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{new Date(transaction.created_at).toLocaleString()}
</div>
</div>
<div className="text-right">
<div className={`font-bold ${transaction.amount > 0 ? 'text-green-600' : 'text-red-600'}`}>
{transaction.amount > 0 ? '+' : ''}{transaction.amount}
</div>
<div className="text-xs text-gray-500">
Balance: {transaction.balance_after}
</div>
</div>
</div>
))}
{transactions.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
No transactions yet
</div>
)}
</div>
</ComponentCard>
{/* Recent Usage */}
<ComponentCard title="Recent Usage">
<div className="space-y-3">
{usageLogs.slice(0, 5).map((log) => (
<div key={log.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-1">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{formatOperationType(log.operation_type)}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{log.model_used} {new Date(log.created_at).toLocaleString()}
</div>
</div>
<div className="text-right">
<div className="font-bold text-amber-600 dark:text-amber-400">
{log.credits_used} credits
</div>
</div>
</div>
))}
{usageLogs.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
No usage history yet
</div>
)}
</div>
</ComponentCard>
</div>
)}
{activeTab === 'transactions' && (
<ComponentCard title="All Transactions">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Description
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Balance
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
{transactions.map((transaction) => (
<tr key={transaction.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{new Date(transaction.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<Badge variant={getTransactionTypeColor(transaction.transaction_type)}>
{transaction.transaction_type}
</Badge>
</td>
<td className="px-6 py-4 text-sm text-gray-900 dark:text-white">
{transaction.description}
</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm text-right font-bold ${
transaction.amount > 0 ? 'text-green-600' : 'text-red-600'
}`}>
{transaction.amount > 0 ? '+' : ''}{transaction.amount}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white text-right">
{transaction.balance_after}
</td>
</tr>
))}
</tbody>
</table>
</div>
</ComponentCard>
)}
{activeTab === 'usage' && (
<ComponentCard title="Usage History">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Operation
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Model
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Credits
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
{usageLogs.map((log) => (
<tr key={log.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{new Date(log.created_at).toLocaleString()}
</td>
<td className="px-6 py-4 text-sm text-gray-900 dark:text-white">
{formatOperationType(log.operation_type)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{log.model_used || 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-right font-bold text-amber-600 dark:text-amber-400">
{log.credits_used}
</td>
</tr>
))}
</tbody>
</table>
</div>
</ComponentCard>
)}
</div>
);
};
export default CreditsAndBilling;