Billing and account fixed - final
This commit is contained in:
@@ -21,23 +21,39 @@ import {
|
||||
|
||||
interface UserAccount {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
username?: string;
|
||||
account_name?: string;
|
||||
credits: number;
|
||||
subscription_plan: string;
|
||||
subscription_plan?: string;
|
||||
is_active: boolean;
|
||||
date_joined: string;
|
||||
}
|
||||
|
||||
interface CreditCostConfig {
|
||||
id: number;
|
||||
model_name: string;
|
||||
operation_type: string;
|
||||
cost: number;
|
||||
display_name: string;
|
||||
credits_cost: number;
|
||||
unit?: string;
|
||||
description?: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface CreditPackageItem {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
credits: number;
|
||||
price: string;
|
||||
discount_percentage: number;
|
||||
is_featured: boolean;
|
||||
description?: string;
|
||||
is_active?: boolean;
|
||||
sort_order?: number;
|
||||
}
|
||||
|
||||
interface SystemStats {
|
||||
total_users: number;
|
||||
active_users: number;
|
||||
@@ -50,8 +66,9 @@ const AdminBilling: React.FC = () => {
|
||||
const [stats, setStats] = useState<SystemStats | null>(null);
|
||||
const [users, setUsers] = useState<UserAccount[]>([]);
|
||||
const [creditConfigs, setCreditConfigs] = useState<CreditCostConfig[]>([]);
|
||||
const [creditPackages, setCreditPackages] = useState<CreditPackageItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'pricing'>('overview');
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'pricing' | 'packages'>('overview');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedUser, setSelectedUser] = useState<UserAccount | null>(null);
|
||||
const [creditAmount, setCreditAmount] = useState('');
|
||||
@@ -65,17 +82,19 @@ const AdminBilling: React.FC = () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [statsData, usersData, configsData] = await Promise.all([
|
||||
// Admin billing stats (credits, activity, revenue)
|
||||
fetchAPI('/v1/billing/admin/stats/'),
|
||||
// Admin billing users list (with credits)
|
||||
fetchAPI('/v1/admin/billing/users/?limit=100'),
|
||||
// Admin billing credit costs
|
||||
fetchAPI('/v1/admin/billing/credit-costs/'),
|
||||
// Admin billing stats (modules admin endpoints)
|
||||
fetchAPI('/v1/admin/billing/stats/'),
|
||||
// Admin users with credits
|
||||
fetchAPI('/v1/admin/users/'),
|
||||
// Admin credit costs (modules billing)
|
||||
fetchAPI('/v1/admin/credit-costs/'),
|
||||
]);
|
||||
const packagesData = await fetchAPI('/v1/billing/credit-packages/');
|
||||
|
||||
setStats(statsData);
|
||||
setUsers(usersData.results || []);
|
||||
setCreditConfigs(configsData.results || []);
|
||||
setCreditPackages(packagesData.results || []);
|
||||
} catch (error: any) {
|
||||
toast?.error(error?.message || 'Failed to load admin data');
|
||||
} finally {
|
||||
@@ -90,7 +109,7 @@ const AdminBilling: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchAPI(`/v1/admin/billing/users/${selectedUser.id}/adjust-credits/`, {
|
||||
await fetchAPI(`/v1/admin/users/${selectedUser.id}/adjust-credits/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
amount: parseInt(creditAmount),
|
||||
@@ -110,9 +129,9 @@ const AdminBilling: React.FC = () => {
|
||||
|
||||
const handleUpdateCreditCost = async (configId: number, newCost: number) => {
|
||||
try {
|
||||
await fetchAPI(`/v1/admin/billing/credit-costs/${configId}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ cost: newCost }),
|
||||
await fetchAPI('/v1/admin/credit-costs/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ updates: [{ id: configId, cost: newCost }] }),
|
||||
});
|
||||
|
||||
toast?.success('Credit cost updated successfully');
|
||||
@@ -123,10 +142,26 @@ const AdminBilling: React.FC = () => {
|
||||
};
|
||||
|
||||
const filteredUsers = users.filter(user =>
|
||||
user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
(user.email || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(user.username || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(user.account_name || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const formatLabel = (value?: string) =>
|
||||
(value || '')
|
||||
.split('_')
|
||||
.map((w) => (w ? w[0].toUpperCase() + w.slice(1) : ''))
|
||||
.join(' ')
|
||||
.trim();
|
||||
|
||||
const updateLocalCost = (id: number, value: string) => {
|
||||
setCreditConfigs((prev) =>
|
||||
prev.map((c) =>
|
||||
c.id === id ? { ...c, credits_cost: value === '' ? ('' as any) : Number(value) } : c
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -220,6 +255,16 @@ const AdminBilling: React.FC = () => {
|
||||
>
|
||||
Credit Pricing ({creditConfigs.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('packages')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'packages'
|
||||
? '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 Packages ({creditPackages.length})
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -399,19 +444,22 @@ const AdminBilling: React.FC = () => {
|
||||
<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-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Display Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Credits Cost
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Unit
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Description
|
||||
</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>
|
||||
@@ -419,27 +467,34 @@ const AdminBilling: React.FC = () => {
|
||||
<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">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||
{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 className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||
{config.display_name || formatLabel(config.operation_type)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<Badge tone={config.is_active ? 'success' : 'warning'}>
|
||||
{config.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
<td className="px-6 py-4">
|
||||
<input
|
||||
type="number"
|
||||
value={config.credits_cost as any}
|
||||
onChange={(e) => updateLocalCost(config.id, e.target.value)}
|
||||
className="w-24 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{formatLabel(config.unit)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400 max-w-md">
|
||||
{config.description || '—'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`/admin/igny8_core/creditcostconfig/${config.id}/change/`, '_blank')}
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
onClick={() => handleUpdateCreditCost(config.id, Number(config.credits_cost) || 0)}
|
||||
>
|
||||
Edit
|
||||
Save
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -447,16 +502,37 @@ const AdminBilling: React.FC = () => {
|
||||
</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>
|
||||
</ComponentCard>
|
||||
)}
|
||||
|
||||
{activeTab === 'packages' && (
|
||||
<ComponentCard title="Credit Packages">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{creditPackages.map((pkg) => (
|
||||
<ComponentCard key={pkg.id} title={pkg.name} desc={pkg.description || ''}>
|
||||
<div className="space-y-3">
|
||||
<div className="text-3xl font-bold text-blue-600">{pkg.credits.toLocaleString()}</div>
|
||||
<div className="text-sm text-gray-500">credits</div>
|
||||
<div className="text-2xl font-semibold text-gray-900 dark:text-white">${pkg.price}</div>
|
||||
{pkg.discount_percentage > 0 && (
|
||||
<div className="text-sm text-green-600">Save {pkg.discount_percentage}%</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="light" color={pkg.is_active ? 'success' : 'warning'}>
|
||||
{pkg.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
{pkg.is_featured && (
|
||||
<Badge variant="light" color="primary">
|
||||
Featured
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
))}
|
||||
{creditPackages.length === 0 && (
|
||||
<div className="col-span-3 text-center py-8 text-gray-500">No credit packages found.</div>
|
||||
)}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user