Billing and account fixed - final

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-05 12:56:24 +00:00
parent ee4fa53987
commit 4a16a6a402
10 changed files with 176 additions and 246 deletions

View File

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