fina autoamtiona adn billing and credits
This commit is contained in:
470
frontend/src/pages/Admin/AdminBilling.tsx
Normal file
470
frontend/src/pages/Admin/AdminBilling.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* Admin Billing Management Page
|
||||
* Admin-only interface for managing credits, billing, and user accounts
|
||||
*/
|
||||
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,
|
||||
UserIcon,
|
||||
DollarLineIcon,
|
||||
PlugInIcon,
|
||||
CheckCircleIcon,
|
||||
TimeIcon
|
||||
} from '../../icons';
|
||||
|
||||
interface UserAccount {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
credits: number;
|
||||
subscription_plan: string;
|
||||
is_active: boolean;
|
||||
date_joined: string;
|
||||
}
|
||||
|
||||
interface CreditCostConfig {
|
||||
id: number;
|
||||
model_name: string;
|
||||
operation_type: string;
|
||||
cost: number;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface SystemStats {
|
||||
total_users: number;
|
||||
active_users: number;
|
||||
total_credits_issued: number;
|
||||
total_credits_used: number;
|
||||
}
|
||||
|
||||
const AdminBilling: React.FC = () => {
|
||||
const toast = useToast();
|
||||
const [stats, setStats] = useState<SystemStats | null>(null);
|
||||
const [users, setUsers] = useState<UserAccount[]>([]);
|
||||
const [creditConfigs, setCreditConfigs] = useState<CreditCostConfig[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'pricing'>('overview');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedUser, setSelectedUser] = useState<UserAccount | null>(null);
|
||||
const [creditAmount, setCreditAmount] = useState('');
|
||||
const [adjustmentReason, setAdjustmentReason] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [statsData, usersData, configsData] = await Promise.all([
|
||||
fetchAPI('/v1/admin/billing/stats/'),
|
||||
fetchAPI('/v1/admin/users/?limit=100'),
|
||||
fetchAPI('/v1/admin/credit-costs/'),
|
||||
]);
|
||||
|
||||
setStats(statsData);
|
||||
setUsers(usersData.results || []);
|
||||
setCreditConfigs(configsData.results || []);
|
||||
} catch (error: any) {
|
||||
toast?.error(error?.message || 'Failed to load admin data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdjustCredits = async () => {
|
||||
if (!selectedUser || !creditAmount) {
|
||||
toast?.error('Please select a user and enter amount');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchAPI(`/v1/admin/users/${selectedUser.id}/adjust-credits/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
amount: parseInt(creditAmount),
|
||||
reason: adjustmentReason || 'Admin adjustment',
|
||||
}),
|
||||
});
|
||||
|
||||
toast?.success(`Credits adjusted for ${selectedUser.username}`);
|
||||
setCreditAmount('');
|
||||
setAdjustmentReason('');
|
||||
setSelectedUser(null);
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
toast?.error(error?.message || 'Failed to adjust credits');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateCreditCost = async (configId: number, newCost: number) => {
|
||||
try {
|
||||
await fetchAPI(`/v1/admin/credit-costs/${configId}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ cost: newCost }),
|
||||
});
|
||||
|
||||
toast?.success('Credit cost updated successfully');
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
toast?.error(error?.message || 'Failed to update credit cost');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredUsers = users.filter(user =>
|
||||
user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Admin - Billing Management" description="Manage billing and credits" />
|
||||
<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 admin data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Admin - Billing Management" description="Manage billing and credits" />
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Billing Management</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Admin controls for credits, pricing, and user billing
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/admin/igny8_core/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<PlugInIcon className="w-4 h-4 mr-2" />
|
||||
Django Admin
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* System Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<EnhancedMetricCard
|
||||
title="Total Users"
|
||||
value={stats?.total_users || 0}
|
||||
icon={UserIcon}
|
||||
color="blue"
|
||||
iconColor="text-blue-500"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Active Users"
|
||||
value={stats?.active_users || 0}
|
||||
icon={CheckCircleIcon}
|
||||
color="green"
|
||||
iconColor="text-green-500"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Credits Issued"
|
||||
value={stats?.total_credits_issued || 0}
|
||||
icon={DollarLineIcon}
|
||||
color="amber"
|
||||
iconColor="text-amber-500"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Credits Used"
|
||||
value={stats?.total_credits_used || 0}
|
||||
icon={BoltIcon}
|
||||
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('users')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'users'
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
User Management ({users.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('pricing')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'pricing'
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
Credit Pricing ({creditConfigs.length})
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<ComponentCard title="Quick Actions">
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
fullWidth
|
||||
onClick={() => setActiveTab('users')}
|
||||
>
|
||||
<UserIcon className="w-4 h-4 mr-2" />
|
||||
Manage User Credits
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={() => setActiveTab('pricing')}
|
||||
>
|
||||
<DollarLineIcon className="w-4 h-4 mr-2" />
|
||||
Update Credit Costs
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
fullWidth
|
||||
onClick={() => window.open('/admin/igny8_core/creditcostconfig/', '_blank')}
|
||||
>
|
||||
<PlugInIcon className="w-4 h-4 mr-2" />
|
||||
Full Admin Panel
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Recent Activity">
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
Activity log coming soon
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'users' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<ComponentCard title="User Accounts">
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="Search by username or email..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<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-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
User
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Plan
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Credits
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{filteredUsers.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td className="px-4 py-4">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{user.username}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{user.email}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<Badge variant="info">{user.subscription_plan || 'Free'}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-right font-bold text-amber-600 dark:text-amber-400">
|
||||
{user.credits}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedUser(user)}
|
||||
>
|
||||
Adjust
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ComponentCard title="Adjust Credits">
|
||||
{selectedUser ? (
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{selectedUser.username}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Current: {selectedUser.credits} credits
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Amount</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="Enter credits (use - for deduction)"
|
||||
value={creditAmount}
|
||||
onChange={(e) => setCreditAmount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Reason</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="e.g., Bonus credits, Refund, etc."
|
||||
value={adjustmentReason}
|
||||
onChange={(e) => setAdjustmentReason(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
fullWidth
|
||||
onClick={handleAdjustCredits}
|
||||
>
|
||||
Apply Adjustment
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSelectedUser(null);
|
||||
setCreditAmount('');
|
||||
setAdjustmentReason('');
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
Select a user to adjust credits
|
||||
</div>
|
||||
)}
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'pricing' && (
|
||||
<ComponentCard title="Credit Cost Configuration">
|
||||
<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">
|
||||
Model
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Operation
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Cost (Credits)
|
||||
</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{creditConfigs.map((config) => (
|
||||
<tr key={config.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||
{config.model_name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{config.operation_type}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-bold text-amber-600 dark:text-amber-400">
|
||||
{config.cost}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<Badge variant={config.is_active ? 'success' : 'warning'}>
|
||||
{config.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`/admin/igny8_core/creditcostconfig/${config.id}/change/`, '_blank')}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
To add new credit costs or modify these settings, use the{' '}
|
||||
<a
|
||||
href="/admin/igny8_core/creditcostconfig/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 dark:text-primary-400 hover:underline"
|
||||
>
|
||||
Django Admin Panel
|
||||
</a>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminBilling;
|
||||
Reference in New Issue
Block a user