billing accoutn with all the mess here

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-05 03:59:54 +00:00
parent 6b291671bd
commit 6cf786b03f
41 changed files with 7257 additions and 685 deletions

View File

@@ -14,6 +14,8 @@ import {
CheckCircle,
XCircle,
Clock,
DollarSign,
TrendingUp,
} from 'lucide-react';
import {
getInvoices,
@@ -24,6 +26,7 @@ import {
type Payment,
type CreditBalance,
} from '../../services/billing.api';
import { Card } from '../../components/ui/card';
type TabType = 'overview' | 'invoices' | 'payments';
@@ -53,6 +56,7 @@ export default function AccountBillingPage() {
setPayments(paymentsRes.results);
} catch (err: any) {
setError(err.message || 'Failed to load billing data');
console.error('Billing data load error:', err);
} finally {
setLoading(false);
}
@@ -153,65 +157,81 @@ export default function AccountBillingPage() {
{/* Overview Tab */}
{activeTab === 'overview' && creditBalance && (
<div className="space-y-6">
{/* Credit Balance Card */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-lg shadow-lg p-6 text-white">
<div className="flex items-center justify-between">
<div>
<div className="text-sm opacity-90 mb-1">Current Balance</div>
<div className="text-4xl font-bold">
{creditBalance.balance.toLocaleString()}
</div>
<div className="text-sm opacity-90">credits</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Current Balance</h3>
<CreditCard className="w-5 h-5 text-blue-600" />
</div>
<CreditCard className="w-16 h-16 opacity-20" />
</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{creditBalance?.credits?.toLocaleString() || '0'}
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Available credits</p>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Monthly Allocation</h3>
<TrendingUp className="w-5 h-5 text-green-600" />
</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{creditBalance?.plan_credits_per_month?.toLocaleString() || '0'}
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Credits per month</p>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Used This Month</h3>
<DollarSign className="w-5 h-5 text-red-600" />
</div>
<div className="text-3xl font-bold text-red-600 dark:text-red-400">
{creditBalance?.credits_used_this_month?.toLocaleString() || '0'}
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Credits consumed</p>
</Card>
</div>
{/* Plan Info */}
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Current Plan</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600">Plan:</span>
<span className="font-semibold">{creditBalance.subscription_plan}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Monthly Credits:</span>
<span className="font-semibold">
{creditBalance.monthly_credits.toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Status:</span>
<span>
{getStatusBadge(creditBalance.subscription_status || 'active')}
</span>
</div>
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Quick Actions</h3>
<div className="space-y-2">
<Link
to="/account/purchase-credits"
className="block w-full bg-blue-600 text-white text-center py-2 px-4 rounded hover:bg-blue-700 transition-colors"
>
Purchase Credits
</Link>
<Link
to="/account/usage"
className="block w-full bg-gray-100 text-gray-700 text-center py-2 px-4 rounded hover:bg-gray-200 transition-colors"
>
View Usage Analytics
</Link>
</div>
</div>
</Card>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Recent Activity</h3>
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Account Summary</h3>
<div className="space-y-3">
<div className="text-sm">
<div className="text-gray-600">Total Invoices:</div>
<div className="text-2xl font-bold">{invoices.length}</div>
<div className="flex justify-between">
<span className="text-gray-600">Remaining Credits:</span>
<span className="font-semibold">{creditBalance?.credits_remaining?.toLocaleString() || '0'}</span>
</div>
<div className="text-sm">
<div className="text-gray-600">Paid Invoices:</div>
<div className="text-2xl font-bold text-green-600">
{invoices.filter((i) => i.status === 'paid').length}
</div>
<div className="flex justify-between">
<span className="text-gray-600">Total Invoices:</span>
<span className="font-semibold">{invoices.length}</span>
</div>
<div className="text-sm">
<div className="text-gray-600">Pending Payments:</div>
<div className="text-2xl font-bold text-yellow-600">
{payments.filter((p) => p.status === 'pending_approval').length}
</div>
<div className="flex justify-between">
<span className="text-gray-600">Paid Invoices:</span>
<span className="font-semibold text-green-600">
{invoices.filter(inv => inv.status === 'paid').length}
</span>
</div>
</div>
</div>
</Card>
</div>
</div>
)}

View File

@@ -0,0 +1,282 @@
/**
* Account Settings Page
* Manage account information and billing address
*/
import { useState, useEffect } from 'react';
import { Save, Loader2 } from 'lucide-react';
import { Card } from '../../components/ui/card';
import {
getAccountSettings,
updateAccountSettings,
type AccountSettings,
} from '../../services/billing.api';
export default function AccountSettingsPage() {
const [settings, setSettings] = useState<AccountSettings | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string>('');
const [success, setSuccess] = useState<string>('');
const [formData, setFormData] = useState({
name: '',
billing_address_line1: '',
billing_address_line2: '',
billing_city: '',
billing_state: '',
billing_postal_code: '',
billing_country: '',
tax_id: '',
billing_email: '',
});
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
setLoading(true);
const data = await getAccountSettings();
setSettings(data);
setFormData({
name: data.name || '',
billing_address_line1: data.billing_address_line1 || '',
billing_address_line2: data.billing_address_line2 || '',
billing_city: data.billing_city || '',
billing_state: data.billing_state || '',
billing_postal_code: data.billing_postal_code || '',
billing_country: data.billing_country || '',
tax_id: data.tax_id || '',
billing_email: data.billing_email || '',
});
} catch (err: any) {
setError(err.message || 'Failed to load account settings');
console.error('Account settings load error:', err);
} finally {
setLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setSaving(true);
setError('');
setSuccess('');
await updateAccountSettings(formData);
setSuccess('Account settings updated successfully');
await loadSettings();
} catch (err: any) {
setError(err.message || 'Failed to update account settings');
} finally {
setSaving(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Account Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage your account information and billing details
</p>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
)}
{success && (
<div className="mb-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<p className="text-green-800 dark:text-green-200">{success}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Account Information */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Account Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Name
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full 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"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Slug
</label>
<input
type="text"
value={settings?.slug || ''}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700"
disabled
/>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Billing Email
</label>
<input
type="email"
name="billing_email"
value={formData.billing_email}
onChange={handleChange}
className="w-full 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"
/>
</div>
</Card>
{/* Billing Address */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Billing Address</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 1
</label>
<input
type="text"
name="billing_address_line1"
value={formData.billing_address_line1}
onChange={handleChange}
className="w-full 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 2
</label>
<input
type="text"
name="billing_address_line2"
value={formData.billing_address_line2}
onChange={handleChange}
className="w-full 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"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
City
</label>
<input
type="text"
name="billing_city"
value={formData.billing_city}
onChange={handleChange}
className="w-full 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
State/Province
</label>
<input
type="text"
name="billing_state"
value={formData.billing_state}
onChange={handleChange}
className="w-full 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Postal Code
</label>
<input
type="text"
name="billing_postal_code"
value={formData.billing_postal_code}
onChange={handleChange}
className="w-full 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"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Country
</label>
<input
type="text"
name="billing_country"
value={formData.billing_country}
onChange={handleChange}
className="w-full 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"
placeholder="US, GB, IN, etc."
/>
</div>
</div>
</Card>
{/* Tax Information */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Tax Information</h2>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tax ID / VAT Number
</label>
<input
type="text"
name="tax_id"
value={formData.tax_id}
onChange={handleChange}
className="w-full 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"
placeholder="Optional"
/>
</div>
</Card>
{/* Submit Button */}
<div className="flex justify-end">
<button
type="submit"
disabled={saving}
className="flex items-center gap-2 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{saving ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-4 h-4" />
Save Changes
</>
)}
</button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,264 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { getAccountSettings, updateAccountSettings, AccountSettings } from '../../services/billing.api';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
export default function AccountSettingsPage() {
const toast = useToast();
const [settings, setSettings] = useState<AccountSettings | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState<Partial<AccountSettings>>({});
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
setLoading(true);
const data = await getAccountSettings();
setSettings(data);
setFormData(data);
} catch (error: any) {
toast.error(`Failed to load account settings: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleChange = (field: keyof AccountSettings, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSave = async () => {
try {
setSaving(true);
const result = await updateAccountSettings(formData);
toast.success(result.message || 'Settings updated successfully');
await loadSettings();
} catch (error: any) {
toast.error(`Failed to update settings: ${error.message}`);
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Account Settings" description="Manage your account settings" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Account Settings" description="Manage your account settings" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Account Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage your account information and billing details
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Account Info */}
<Card className="p-6 lg:col-span-2">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Account Information
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Name
</label>
<input
type="text"
value={formData.name || ''}
onChange={(e) => handleChange('name', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Slug
</label>
<input
type="text"
value={settings?.slug || ''}
disabled
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Account slug cannot be changed
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Billing Email
</label>
<input
type="email"
value={formData.billing_email || ''}
onChange={(e) => handleChange('billing_email', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tax ID
</label>
<input
type="text"
value={formData.tax_id || ''}
onChange={(e) => handleChange('tax_id', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="VAT/GST number"
/>
</div>
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-4">
Billing Address
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 1
</label>
<input
type="text"
value={formData.billing_address_line1 || ''}
onChange={(e) => handleChange('billing_address_line1', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 2
</label>
<input
type="text"
value={formData.billing_address_line2 || ''}
onChange={(e) => handleChange('billing_address_line2', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
City
</label>
<input
type="text"
value={formData.billing_city || ''}
onChange={(e) => handleChange('billing_city', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
State/Province
</label>
<input
type="text"
value={formData.billing_state || ''}
onChange={(e) => handleChange('billing_state', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Postal Code
</label>
<input
type="text"
value={formData.billing_postal_code || ''}
onChange={(e) => handleChange('billing_postal_code', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Country
</label>
<input
type="text"
value={formData.billing_country || ''}
onChange={(e) => handleChange('billing_country', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
</div>
</div>
<div className="mt-6 flex justify-end gap-3">
<Button
variant="secondary"
onClick={loadSettings}
disabled={saving}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={saving}
>
{saving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</Card>
{/* Account Summary */}
<Card className="p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Account Summary
</h2>
<div className="space-y-4">
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Credit Balance</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{settings?.credit_balance.toLocaleString() || 0}
</div>
</div>
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-600 dark:text-gray-400">Account Created</div>
<div className="text-sm text-gray-900 dark:text-white">
{settings?.created_at ? new Date(settings.created_at).toLocaleDateString() : '-'}
</div>
</div>
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Last Updated</div>
<div className="text-sm text-gray-900 dark:text-white">
{settings?.updated_at ? new Date(settings.updated_at).toLocaleDateString() : '-'}
</div>
</div>
</div>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,523 @@
/**
* Plans & Billing Page - Consolidated
* Tabs: Current Plan, Upgrade/Downgrade, Credits Overview, Purchase Credits, Billing History, Payment Methods
*/
import { useState, useEffect } from 'react';
import {
CreditCard, Package, TrendingUp, FileText, Wallet, ArrowUpCircle,
Loader2, AlertCircle, CheckCircle, Download
} from 'lucide-react';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import Button from '../../components/ui/button/Button';
import {
getCreditBalance,
getCreditPackages,
getInvoices,
getAvailablePaymentMethods,
purchaseCreditPackage,
type CreditBalance,
type CreditPackage,
type Invoice,
type PaymentMethod,
} from '../../services/billing.api';
type TabType = 'plan' | 'upgrade' | 'credits' | 'purchase' | 'invoices' | 'payment-methods';
export default function PlansAndBillingPage() {
const [activeTab, setActiveTab] = useState<TabType>('plan');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
// Data states
const [creditBalance, setCreditBalance] = useState<CreditBalance | null>(null);
const [packages, setPackages] = useState<CreditPackage[]>([]);
const [invoices, setInvoices] = useState<Invoice[]>([]);
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>([]);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
const [balanceData, packagesData, invoicesData, methodsData] = await Promise.all([
getCreditBalance(),
getCreditPackages(),
getInvoices({}),
getAvailablePaymentMethods(),
]);
setCreditBalance(balanceData);
setPackages(packagesData.results || []);
setInvoices(invoicesData.results || []);
setPaymentMethods(methodsData.results || []);
} catch (err: any) {
setError(err.message || 'Failed to load billing data');
console.error('Billing load error:', err);
} finally {
setLoading(false);
}
};
const handlePurchase = async (packageId: number) => {
try {
await purchaseCreditPackage({
package_id: packageId,
payment_method: 'stripe',
});
await loadData();
} catch (err: any) {
setError(err.message || 'Failed to purchase credits');
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
const tabs = [
{ id: 'plan' as TabType, label: 'Current Plan', icon: <Package className="w-4 h-4" /> },
{ id: 'upgrade' as TabType, label: 'Upgrade/Downgrade', icon: <ArrowUpCircle className="w-4 h-4" /> },
{ id: 'credits' as TabType, label: 'Credits Overview', icon: <TrendingUp className="w-4 h-4" /> },
{ id: 'purchase' as TabType, label: 'Purchase Credits', icon: <CreditCard className="w-4 h-4" /> },
{ id: 'invoices' as TabType, label: 'Billing History', icon: <FileText className="w-4 h-4" /> },
{ id: 'payment-methods' as TabType, label: 'Payment Methods', icon: <Wallet className="w-4 h-4" /> },
];
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Plans & Billing</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage your subscription, credits, and billing information
</p>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-600" />
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
)}
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8 overflow-x-auto">
{tabs.map((tab) => (
<button
type="button"
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm whitespace-nowrap
${activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}
`}
>
{tab.icon}
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab Content */}
<div className="mt-6">
{/* Current Plan Tab */}
{activeTab === 'plan' && (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Your Current Plan</h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">Free Plan</div>
<div className="text-gray-600 dark:text-gray-400">Perfect for getting started</div>
</div>
<Badge variant="light" color="success">Active</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<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">
{creditBalance?.plan_credits_per_month.toLocaleString() || 0}
</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Sites Allowed</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">1</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Team Members</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">1</div>
</div>
</div>
<div className="mt-6 flex gap-3">
<Button variant="primary" tone="brand">
Upgrade Plan
</Button>
<Button variant="outline" tone="neutral">
Compare Plans
</Button>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Plan Features</h2>
<ul className="space-y-3">
{['Basic AI Tools', 'Content Generation', 'Keyword Research', 'Email Support'].map((feature) => (
<li key={feature} className="flex items-center gap-2 text-gray-700 dark:text-gray-300">
<CheckCircle className="w-5 h-5 text-green-600" />
{feature}
</li>
))}
</ul>
</Card>
</div>
)}
{/* Upgrade/Downgrade Tab */}
{activeTab === 'upgrade' && (
<div className="space-y-6">
<div className="mb-4">
<h2 className="text-xl font-semibold mb-2">Available Plans</h2>
<p className="text-gray-600 dark:text-gray-400">Choose the plan that best fits your needs</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Free Plan */}
<Card className="p-6 relative">
<div className="mb-4">
<h3 className="text-lg font-semibold">Free</h3>
<div className="text-3xl font-bold text-gray-900 dark:text-white mt-2">$0</div>
<div className="text-sm text-gray-500">/month</div>
</div>
<div className="space-y-3 mb-6">
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>100 credits/month</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>1 site</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>1 user</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>Basic features</span>
</div>
</div>
<Badge variant="light" color="success" className="absolute top-4 right-4">Current</Badge>
</Card>
{/* Starter Plan */}
<Card className="p-6 border-2 border-blue-500">
<Badge variant="light" color="primary" className="absolute top-4 right-4">Popular</Badge>
<div className="mb-4">
<h3 className="text-lg font-semibold">Starter</h3>
<div className="text-3xl font-bold text-gray-900 dark:text-white mt-2">$29</div>
<div className="text-sm text-gray-500">/month</div>
</div>
<div className="space-y-3 mb-6">
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>1,000 credits/month</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>3 sites</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>2 users</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>Full AI suite</span>
</div>
</div>
<Button variant="primary" tone="brand" fullWidth>
Upgrade to Starter
</Button>
</Card>
{/* Professional Plan */}
<Card className="p-6">
<div className="mb-4">
<h3 className="text-lg font-semibold">Professional</h3>
<div className="text-3xl font-bold text-gray-900 dark:text-white mt-2">$99</div>
<div className="text-sm text-gray-500">/month</div>
</div>
<div className="space-y-3 mb-6">
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>5,000 credits/month</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>10 sites</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>5 users</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>Priority support</span>
</div>
</div>
<Button variant="outline" tone="neutral" fullWidth>
Upgrade to Pro
</Button>
</Card>
{/* Enterprise Plan */}
<Card className="p-6">
<div className="mb-4">
<h3 className="text-lg font-semibold">Enterprise</h3>
<div className="text-3xl font-bold text-gray-900 dark:text-white mt-2">$299</div>
<div className="text-sm text-gray-500">/month</div>
</div>
<div className="space-y-3 mb-6">
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>20,000 credits/month</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>Unlimited sites</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>20 users</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>Dedicated support</span>
</div>
</div>
<Button variant="outline" tone="neutral" fullWidth>
Upgrade to Enterprise
</Button>
</Card>
</div>
<Card className="p-6 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
<h3 className="font-semibold text-blue-900 dark:text-blue-100 mb-2">Plan Change Policy</h3>
<ul className="space-y-2 text-sm text-blue-800 dark:text-blue-200">
<li> Upgrades take effect immediately and you'll be charged a prorated amount</li>
<li>• Downgrades take effect at the end of your current billing period</li>
<li>• Unused credits from your current plan will carry over</li>
<li>• You can cancel your subscription at any time</li>
</ul>
</Card>
</div>
)}
{/* Credits Overview Tab */}
{activeTab === 'credits' && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Current Balance</div>
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
{creditBalance?.credits.toLocaleString() || 0}
</div>
<div className="text-sm text-gray-500 mt-2">credits available</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Used This Month</div>
<div className="text-3xl font-bold text-red-600 dark:text-red-400">
{creditBalance?.credits_used_this_month.toLocaleString() || 0}
</div>
<div className="text-sm text-gray-500 mt-2">credits consumed</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Monthly Included</div>
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
{creditBalance?.plan_credits_per_month.toLocaleString() || 0}
</div>
<div className="text-sm text-gray-500 mt-2">from your plan</div>
</Card>
</div>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Credit Usage Summary</h2>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-gray-700 dark:text-gray-300">Remaining Credits</span>
<span className="font-semibold">{creditBalance?.credits_remaining.toLocaleString() || 0}</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{
width: creditBalance?.credits
? `${Math.min((creditBalance.credits / (creditBalance.plan_credits_per_month || 1)) * 100, 100)}%`
: '0%'
}}
></div>
</div>
</div>
</Card>
</div>
)}
{/* Purchase Credits Tab */}
{activeTab === 'purchase' && (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Credit Packages</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{packages.map((pkg) => (
<div key={pkg.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-6 hover:border-blue-500 transition-colors">
<div className="text-lg font-semibold text-gray-900 dark:text-white">{pkg.name}</div>
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400 mt-2">
{pkg.credits.toLocaleString()} <span className="text-sm text-gray-500">credits</span>
</div>
<div className="text-2xl font-semibold text-gray-900 dark:text-white mt-4">
${pkg.price}
</div>
{pkg.description && (
<div className="text-sm text-gray-600 dark:text-gray-400 mt-2">{pkg.description}</div>
)}
<Button
variant="primary"
tone="brand"
onClick={() => handlePurchase(pkg.id)}
fullWidth
className="mt-6"
>
Purchase
</Button>
</div>
))}
{packages.length === 0 && (
<div className="col-span-3 text-center py-12 text-gray-500">
No credit packages available at this time
</div>
)}
</div>
</Card>
</div>
)}
{/* Billing History Tab */}
{activeTab === 'invoices' && (
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Invoice
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{invoices.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-8 text-center text-gray-500">
<FileText className="w-12 h-12 mx-auto mb-2 text-gray-400" />
No invoices yet
</td>
</tr>
) : (
invoices.map((invoice) => (
<tr key={invoice.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="px-6 py-4 font-medium">{invoice.invoice_number}</td>
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
{new Date(invoice.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-4 font-semibold">${invoice.total_amount}</td>
<td className="px-6 py-4">
<Badge
variant="light"
color={invoice.status === 'paid' ? 'success' : 'warning'}
>
{invoice.status}
</Badge>
</td>
<td className="px-6 py-4 text-right">
<Button
variant="ghost"
tone="brand"
size="sm"
startIcon={<Download className="w-4 h-4" />}
className="ml-auto"
>
Download
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
)}
{/* Payment Methods Tab */}
{activeTab === 'payment-methods' && (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Payment Methods</h2>
<Button variant="primary" tone="brand">
Add Payment Method
</Button>
</div>
<div className="space-y-4">
{paymentMethods.map((method) => (
<div key={method.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<CreditCard className="w-8 h-8 text-gray-400" />
<div>
<div className="font-medium text-gray-900 dark:text-white">{method.display_name}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">{method.type}</div>
</div>
</div>
{method.is_enabled && (
<Badge variant="light" color="success">Active</Badge>
)}
</div>
))}
{paymentMethods.length === 0 && (
<div className="text-center py-12 text-gray-500">
No payment methods configured
</div>
)}
</div>
</Card>
</div>
)}
</div>
</div>
);
}

View File

@@ -5,11 +5,12 @@
import { useState, useEffect } from 'react';
import { AlertCircle, Check, CreditCard, Building2, Wallet, Loader2 } from 'lucide-react';
import Button from '../../components/ui/button/Button';
import {
getCreditPackages,
getAvailablePaymentMethods,
purchaseCreditPackage,
submitManualPayment,
createManualPayment,
type CreditPackage,
type PaymentMethod,
} from '../../services/billing.api';
@@ -41,12 +42,13 @@ export default function PurchaseCreditsPage() {
getAvailablePaymentMethods(),
]);
setPackages(packagesRes.results);
setPaymentMethods(methodsRes.methods);
setPackages(packagesRes?.results || []);
setPaymentMethods(methodsRes?.results || []);
// Auto-select first payment method
if (methodsRes.methods.length > 0) {
setSelectedPaymentMethod(methodsRes.methods[0].type);
const methods = methodsRes?.results || [];
if (methods.length > 0) {
setSelectedPaymentMethod(methods[0].type);
}
} catch (err) {
setError('Failed to load credit packages');
@@ -66,10 +68,10 @@ export default function PurchaseCreditsPage() {
setPurchasing(true);
setError('');
const response = await purchaseCreditPackage(
selectedPackage.id,
selectedPaymentMethod
);
const response = await purchaseCreditPackage({
package_id: selectedPackage.id,
payment_method: selectedPaymentMethod as 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet'
});
if (selectedPaymentMethod === 'stripe') {
// Redirect to Stripe checkout
@@ -101,10 +103,10 @@ export default function PurchaseCreditsPage() {
setPurchasing(true);
setError('');
await submitManualPayment({
invoice_id: invoiceData.invoice_id,
payment_method: selectedPaymentMethod as 'bank_transfer' | 'local_wallet',
transaction_reference: manualPaymentData.transaction_reference,
await createManualPayment({
amount: String(selectedPackage?.price || 0),
payment_method: selectedPaymentMethod as 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet',
reference: manualPaymentData.transaction_reference,
notes: manualPaymentData.notes,
});
@@ -254,31 +256,28 @@ export default function PurchaseCreditsPage() {
</div>
<div className="flex gap-3 pt-4">
<button
type="button"
<Button
variant="outline"
tone="neutral"
onClick={() => {
setShowManualPaymentForm(false);
setInvoiceData(null);
}}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
disabled={purchasing}
className="flex-1"
>
Cancel
</button>
<button
</Button>
<Button
variant="primary"
tone="brand"
type="submit"
disabled={purchasing}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
startIcon={purchasing ? <Loader2 className="w-4 h-4 animate-spin" /> : undefined}
className="flex-1"
>
{purchasing ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Submitting...
</>
) : (
'Submit Payment'
)}
</button>
{purchasing ? 'Submitting...' : 'Submit Payment'}
</Button>
</div>
</div>
</form>
@@ -376,7 +375,7 @@ export default function PurchaseCreditsPage() {
{getPaymentMethodIcon(method.type)}
</div>
<div className="flex-1">
<h3 className="font-semibold mb-1">{method.name}</h3>
<h3 className="font-semibold mb-1">{method.name || method.display_name}</h3>
<p className="text-sm text-gray-600">{method.instructions}</p>
</div>
{selectedPaymentMethod === method.type && (
@@ -408,20 +407,17 @@ export default function PurchaseCreditsPage() {
</div>
</div>
<button
<Button
variant="primary"
tone="brand"
size="lg"
onClick={handlePurchase}
disabled={purchasing}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-semibold text-lg flex items-center justify-center gap-2"
startIcon={purchasing ? <Loader2 className="w-5 h-5 animate-spin" /> : undefined}
fullWidth
>
{purchasing ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Processing...
</>
) : (
'Proceed to Payment'
)}
</button>
{purchasing ? 'Processing...' : 'Proceed to Payment'}
</Button>
</div>
)}
</div>

View File

@@ -0,0 +1,389 @@
/**
* Team Management Page
* Tabs: Users, Invitations, Access Control
*/
import { useState, useEffect } from 'react';
import { Users, UserPlus, Shield } from 'lucide-react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { getTeamMembers, inviteTeamMember, removeTeamMember, TeamMember } from '../../services/billing.api';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Badge from '../../components/ui/badge/Badge';
type TabType = 'users' | 'invitations' | 'access';
export default function TeamManagementPage() {
const toast = useToast();
const [activeTab, setActiveTab] = useState<TabType>('users');
const [members, setMembers] = useState<TeamMember[]>([]);
const [loading, setLoading] = useState(true);
const [showInviteModal, setShowInviteModal] = useState(false);
const [inviting, setInviting] = useState(false);
const [inviteForm, setInviteForm] = useState({
email: '',
first_name: '',
last_name: '',
});
useEffect(() => {
loadTeamMembers();
}, []);
const loadTeamMembers = async () => {
try {
setLoading(true);
const data = await getTeamMembers();
setMembers(data.results || []);
} catch (error: any) {
toast.error(`Failed to load team members: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleInvite = async () => {
if (!inviteForm.email) {
toast.error('Email is required');
return;
}
try {
setInviting(true);
const result = await inviteTeamMember(inviteForm);
toast.success(result.message || 'Team member invited successfully');
setShowInviteModal(false);
setInviteForm({ email: '', first_name: '', last_name: '' });
await loadTeamMembers();
} catch (error: any) {
toast.error(`Failed to invite team member: ${error.message}`);
} finally {
setInviting(false);
}
};
const handleRemove = async (userId: number, email: string) => {
if (!confirm(`Are you sure you want to remove ${email} from the team?`)) {
return;
}
try {
const result = await removeTeamMember(userId);
toast.success(result.message || 'Team member removed successfully');
await loadTeamMembers();
} catch (error: any) {
toast.error(`Failed to remove team member: ${error.message}`);
}
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Team Management" description="Manage your team members" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
</div>
);
}
const tabs = [
{ id: 'users' as TabType, label: 'Users', icon: <Users className="w-4 h-4" /> },
{ id: 'invitations' as TabType, label: 'Invitations', icon: <UserPlus className="w-4 h-4" /> },
{ id: 'access' as TabType, label: 'Access Control', icon: <Shield className="w-4 h-4" /> },
];
return (
<div className="p-6">
<PageMeta title="Team Management" description="Manage your team members" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Team Management</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage team members, invitations, and access control
</p>
</div>
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
type="button"
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm
${activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400'
}
`}
>
{tab.icon}
{tab.label}
</button>
))}
</nav>
</div>
{/* Users Tab */}
{activeTab === 'users' && (
<div className="space-y-6">
<div className="flex justify-end">
<Button
variant="primary"
onClick={() => setShowInviteModal(true)}
>
<UserPlus className="w-4 h-4 mr-2" />
Invite Team Member
</Button>
</div>
{/* Team Members Table */}
<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">Name</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Email</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Status</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Role</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Joined</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Last Login</th>
<th className="text-right py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Actions</th>
</tr>
</thead>
<tbody>
{members.map((member) => (
<tr key={member.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">
{member.first_name || member.last_name
? `${member.first_name} ${member.last_name}`.trim()
: '-'}
</td>
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">
{member.email}
</td>
<td className="py-3 px-4">
<Badge
variant="light"
color={member.is_active ? 'success' : 'error'}
>
{member.is_active ? 'Active' : 'Inactive'}
</Badge>
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
{member.is_staff ? 'Admin' : 'Member'}
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
{member.date_joined ? new Date(member.date_joined).toLocaleDateString() : 'N/A'}
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
{member.last_login ? new Date(member.last_login).toLocaleDateString() : 'Never'}
</td>
<td className="py-3 px-4 text-right">
<Button
variant="secondary"
size="sm"
onClick={() => handleRemove(member.id, member.email)}
>
Remove
</Button>
</td>
</tr>
))}
{members.length === 0 && (
<tr>
<td colSpan={7} className="py-8 text-center text-gray-500 dark:text-gray-400">
No team members yet. Invite your first team member!
</td>
</tr>
)}
</tbody>
</table>
</div>
</Card>
</div>
)}
{/* Invitations Tab */}
{activeTab === 'invitations' && (
<div className="space-y-6">
<div className="flex justify-end">
<Button
variant="primary"
onClick={() => setShowInviteModal(true)}
>
<UserPlus className="w-4 h-4 mr-2" />
Send Invitation
</Button>
</div>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Pending Invitations</h2>
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
No pending invitations
</div>
</Card>
<Card className="p-6 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
<h3 className="font-semibold text-blue-900 dark:text-blue-100 mb-2">How Invitations Work</h3>
<ul className="space-y-2 text-sm text-blue-800 dark:text-blue-200">
<li> Invited users will receive an email with a registration link</li>
<li> Invitations expire after 7 days</li>
<li> You can resend or cancel invitations at any time</li>
<li> New members will have the default "Member" role</li>
</ul>
</Card>
</div>
)}
{/* Access Control Tab */}
{activeTab === 'access' && (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Role Permissions</h2>
<div className="space-y-4">
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-gray-900 dark:text-white">Owner</h3>
<Badge variant="light" color="error">Highest Access</Badge>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
Full access to all features including billing, team management, and account settings
</p>
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<li> Manage billing and subscriptions</li>
<li> Invite and remove team members</li>
<li> Manage all sites and content</li>
<li> Configure account settings</li>
</ul>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-gray-900 dark:text-white">Admin</h3>
<Badge variant="light" color="primary">High Access</Badge>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
Can manage sites and content, invite team members, but cannot access billing
</p>
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<li> Invite team members</li>
<li> Manage all sites and content</li>
<li> View usage analytics</li>
<li> Cannot manage billing</li>
</ul>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-gray-900 dark:text-white">Editor</h3>
<Badge variant="light" color="warning">Medium Access</Badge>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
Can create and edit content, limited settings access
</p>
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<li> Create and edit content</li>
<li> View usage analytics</li>
<li> Cannot invite users</li>
<li> Cannot manage billing</li>
</ul>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-gray-900 dark:text-white">Viewer</h3>
<Badge variant="light" color="default">Read-Only</Badge>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
Read-only access to content and analytics
</p>
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<li> View content and analytics</li>
<li> Cannot create or edit</li>
<li> Cannot invite users</li>
<li> Cannot manage billing</li>
</ul>
</div>
</div>
</Card>
</div>
)}
{/* Invite Modal */}
{showInviteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<Card className="p-6 w-full max-w-md">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Invite Team Member
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email *
</label>
<input
type="email"
value={inviteForm.email}
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="user@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
First Name
</label>
<input
type="text"
value={inviteForm.first_name}
onChange={(e) => setInviteForm(prev => ({ ...prev, first_name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Last Name
</label>
<input
type="text"
value={inviteForm.last_name}
onChange={(e) => setInviteForm(prev => ({ ...prev, last_name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
</div>
<div className="mt-6 flex justify-end gap-3">
<Button
variant="secondary"
onClick={() => {
setShowInviteModal(false);
setInviteForm({ email: '', first_name: '', last_name: '' });
}}
disabled={inviting}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleInvite}
disabled={inviting}
>
{inviting ? 'Inviting...' : 'Send Invitation'}
</Button>
</div>
</Card>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,327 @@
/**
* Usage & Analytics Page
* Tabs: Credit Usage, API Usage, Cost Breakdown
*/
import { useState, useEffect } from 'react';
import { TrendingUp, Activity, DollarSign } from 'lucide-react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { getUsageAnalytics, UsageAnalytics } from '../../services/billing.api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
type TabType = 'credits' | 'api' | 'costs';
export default function UsageAnalyticsPage() {
const toast = useToast();
const [activeTab, setActiveTab] = useState<TabType>('credits');
const [analytics, setAnalytics] = useState<UsageAnalytics | null>(null);
const [loading, setLoading] = useState(true);
const [period, setPeriod] = useState(30);
useEffect(() => {
loadAnalytics();
}, [period]);
const loadAnalytics = async () => {
try {
setLoading(true);
const data = await getUsageAnalytics(period);
setAnalytics(data);
} catch (error: any) {
toast.error(`Failed to load usage analytics: ${error.message}`);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Usage & Analytics" description="Analyze your usage patterns" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
</div>
);
}
const tabs = [
{ id: 'credits' as TabType, label: 'Credit Usage', icon: <TrendingUp className="w-4 h-4" /> },
{ id: 'api' as TabType, label: 'API Usage', icon: <Activity className="w-4 h-4" /> },
{ id: 'costs' as TabType, label: 'Cost Breakdown', icon: <DollarSign className="w-4 h-4" /> },
];
return (
<div className="p-6">
<PageMeta title="Usage & Analytics" description="Analyze your usage patterns" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Usage & Analytics</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Monitor credit usage, API calls, and cost breakdown
</p>
</div>
<div className="mb-6 flex items-center justify-between">
{/* Tabs */}
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm
${activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400'
}
`}
>
{tab.icon}
{tab.label}
</button>
))}
</nav>
</div>
{/* Period Selector */}
<div className="flex gap-2">
<button
onClick={() => setPeriod(7)}
className={`px-4 py-2 rounded-md text-sm font-medium ${
period === 7
? 'bg-primary-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
7 Days
</button>
<button
onClick={() => setPeriod(30)}
className={`px-4 py-2 rounded-md text-sm font-medium ${
period === 30
? 'bg-primary-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
30 Days
</button>
<button
onClick={() => setPeriod(90)}
className={`px-4 py-2 rounded-md text-sm font-medium ${
period === 90
? 'bg-primary-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
90 Days
</button>
</div>
</div>
{/* Tab Content */}
<div className="mt-6">
{/* Credit Usage Tab */}
{activeTab === 'credits' && (
<div className="space-y-6">
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Credits Used</div>
<div className="text-3xl font-bold text-red-600 dark:text-red-400">
{analytics?.total_usage.toLocaleString() || 0}
</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Purchases</div>
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
{analytics?.total_purchases.toLocaleString() || 0}
</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Current Balance</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{analytics?.current_balance.toLocaleString() || 0}
</div>
</Card>
</div>
{/* Usage by Type */}
<Card className="p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Usage by Operation Type
</h2>
<div className="space-y-3">
{analytics?.usage_by_type.map((item, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-1">
<Badge variant="light" color="error">
{item.transaction_type}
</Badge>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{item.count} operations
</div>
</div>
<div className="text-right">
<div className="text-lg font-bold text-red-600 dark:text-red-400">
{item.total.toLocaleString()} credits
</div>
</div>
</div>
))}
{(!analytics?.usage_by_type || analytics.usage_by_type.length === 0) && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
No usage in this period
</div>
)}
</div>
</Card>
</div>
)}
{/* API Usage Tab */}
{activeTab === 'api' && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total API Calls</div>
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
{analytics?.usage_by_type.reduce((sum, item) => sum + item.count, 0).toLocaleString() || 0}
</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Avg Calls/Day</div>
<div className="text-3xl font-bold text-purple-600 dark:text-purple-400">
{Math.round((analytics?.usage_by_type.reduce((sum, item) => sum + item.count, 0) || 0) / period)}
</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Success Rate</div>
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
98.5%
</div>
</Card>
</div>
<Card className="p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
API Calls by Endpoint
</h2>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-1">
<div className="font-medium">/api/v1/content/generate</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">Content generation</div>
</div>
<div className="text-right">
<div className="text-lg font-bold">1,234</div>
<div className="text-xs text-gray-500">calls</div>
</div>
</div>
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-1">
<div className="font-medium">/api/v1/keywords/cluster</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">Keyword clustering</div>
</div>
<div className="text-right">
<div className="text-lg font-bold">567</div>
<div className="text-xs text-gray-500">calls</div>
</div>
</div>
</div>
</Card>
</div>
)}
{/* Cost Breakdown Tab */}
{activeTab === 'costs' && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Cost</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
${((analytics?.total_usage || 0) * 0.01).toFixed(2)}
</div>
<div className="text-xs text-gray-500 mt-1">Estimated USD</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Avg Cost/Day</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
${(((analytics?.total_usage || 0) * 0.01) / period).toFixed(2)}
</div>
<div className="text-xs text-gray-500 mt-1">Estimated USD</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Cost per Credit</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
$0.01
</div>
<div className="text-xs text-gray-500 mt-1">Average rate</div>
</Card>
</div>
<Card className="p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Cost by Operation
</h2>
<div className="space-y-3">
{analytics?.usage_by_type.map((item, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-1">
<div className="font-medium">{item.transaction_type}</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{item.total.toLocaleString()} credits used
</div>
</div>
<div className="text-right">
<div className="text-lg font-bold">${(item.total * 0.01).toFixed(2)}</div>
<div className="text-xs text-gray-500">USD</div>
</div>
</div>
))}
{(!analytics?.usage_by_type || analytics.usage_by_type.length === 0) && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
No cost data available
</div>
)}
</div>
</Card>
</div>
)}
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6 hidden">
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Credits Used</div>
<div className="text-3xl font-bold text-red-600 dark:text-red-400">
{analytics?.total_usage.toLocaleString() || 0}
</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Purchases</div>
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
{analytics?.total_purchases.toLocaleString() || 0}
</div>
</Card>
<Card className="p-6">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Current Balance</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{analytics?.current_balance.toLocaleString() || 0}
</div>
</Card>
</div>
</div>
);
}