billing accoutn with all the mess here
This commit is contained in:
120
frontend/src/pages/admin/AdminAPIMonitorPage.tsx
Normal file
120
frontend/src/pages/admin/AdminAPIMonitorPage.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Admin API Monitor Page
|
||||
* Monitor API usage and performance
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Activity, TrendingUp, Clock, AlertTriangle } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
|
||||
export default function AdminAPIMonitorPage() {
|
||||
const stats = {
|
||||
totalRequests: 125430,
|
||||
requestsPerMinute: 42,
|
||||
avgResponseTime: 234,
|
||||
errorRate: 0.12,
|
||||
};
|
||||
|
||||
const topEndpoints = [
|
||||
{ path: '/v1/billing/credit-balance/', requests: 15234, avgTime: 145 },
|
||||
{ path: '/v1/sites/', requests: 12543, avgTime: 234 },
|
||||
{ path: '/v1/ideas/', requests: 10234, avgTime: 456 },
|
||||
{ path: '/v1/account/settings/', requests: 8234, avgTime: 123 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Activity className="w-6 h-6" />
|
||||
API Monitor
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Monitor API usage and performance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
|
||||
<TrendingUp className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{stats.totalRequests.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Requests</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-green-100 dark:bg-green-900/20 rounded-lg">
|
||||
<Activity className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{stats.requestsPerMinute}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Requests/Min</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-purple-100 dark:bg-purple-900/20 rounded-lg">
|
||||
<Clock className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{stats.avgResponseTime}ms
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Avg Response</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-red-100 dark:bg-red-900/20 rounded-lg">
|
||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{stats.errorRate}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Error Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Top Endpoints</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="border-b border-gray-200 dark:border-gray-700">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">Endpoint</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-gray-500">Requests</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-gray-500">Avg Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{topEndpoints.map((endpoint) => (
|
||||
<tr key={endpoint.path} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-4 py-3 font-mono text-sm">{endpoint.path}</td>
|
||||
<td className="px-4 py-3 text-right">{endpoint.requests.toLocaleString()}</td>
|
||||
<td className="px-4 py-3 text-right">{endpoint.avgTime}ms</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
130
frontend/src/pages/admin/AdminAccountLimitsPage.tsx
Normal file
130
frontend/src/pages/admin/AdminAccountLimitsPage.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Admin Account Limits Page
|
||||
* Configure account limits and quotas
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Save, Shield, Loader2 } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
|
||||
export default function AdminAccountLimitsPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [limits, setLimits] = useState({
|
||||
maxSites: 10,
|
||||
maxTeamMembers: 5,
|
||||
maxStorageGB: 50,
|
||||
maxAPICallsPerMonth: 100000,
|
||||
maxConcurrentJobs: 10,
|
||||
rateLimitPerMinute: 100,
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Shield className="w-6 h-6" />
|
||||
Account Limits
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Configure default account limits and quotas
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Resource Limits</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Max Sites per Account
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limits.maxSites}
|
||||
onChange={(e) => setLimits({ ...limits, maxSites: parseInt(e.target.value) })}
|
||||
className="w-full px-4 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-2">
|
||||
Max Team Members
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limits.maxTeamMembers}
|
||||
onChange={(e) => setLimits({ ...limits, maxTeamMembers: parseInt(e.target.value) })}
|
||||
className="w-full px-4 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-2">
|
||||
Max Storage (GB)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limits.maxStorageGB}
|
||||
onChange={(e) => setLimits({ ...limits, maxStorageGB: parseInt(e.target.value) })}
|
||||
className="w-full px-4 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>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">API & Performance Limits</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Max API Calls per Month
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limits.maxAPICallsPerMonth}
|
||||
onChange={(e) => setLimits({ ...limits, maxAPICallsPerMonth: parseInt(e.target.value) })}
|
||||
className="w-full px-4 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-2">
|
||||
Max Concurrent Background Jobs
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limits.maxConcurrentJobs}
|
||||
onChange={(e) => setLimits({ ...limits, maxConcurrentJobs: parseInt(e.target.value) })}
|
||||
className="w-full px-4 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-2">
|
||||
Rate Limit (requests per minute)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limits.rateLimitPerMinute}
|
||||
onChange={(e) => setLimits({ ...limits, rateLimitPerMinute: parseInt(e.target.value) })}
|
||||
className="w-full px-4 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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
164
frontend/src/pages/admin/AdminActivityLogsPage.tsx
Normal file
164
frontend/src/pages/admin/AdminActivityLogsPage.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Admin Activity Logs Page
|
||||
* View system activity and audit trail
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Search, Filter, Loader2, AlertCircle, Activity } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
|
||||
interface ActivityLog {
|
||||
id: number;
|
||||
user_email: string;
|
||||
account_name: string;
|
||||
action: string;
|
||||
resource_type: string;
|
||||
resource_id: string | null;
|
||||
ip_address: string;
|
||||
timestamp: string;
|
||||
details: string;
|
||||
}
|
||||
|
||||
export default function AdminActivityLogsPage() {
|
||||
const [logs, setLogs] = useState<ActivityLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [actionFilter, setActionFilter] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
// Mock data - replace with API call
|
||||
setLogs([
|
||||
{
|
||||
id: 1,
|
||||
user_email: 'john@example.com',
|
||||
account_name: 'Acme Corp',
|
||||
action: 'create',
|
||||
resource_type: 'Site',
|
||||
resource_id: '123',
|
||||
ip_address: '192.168.1.1',
|
||||
timestamp: new Date().toISOString(),
|
||||
details: 'Created new site "Main Website"',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user_email: 'jane@example.com',
|
||||
account_name: 'TechStart',
|
||||
action: 'update',
|
||||
resource_type: 'Account',
|
||||
resource_id: '456',
|
||||
ip_address: '192.168.1.2',
|
||||
timestamp: new Date(Date.now() - 3600000).toISOString(),
|
||||
details: 'Updated account billing address',
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const filteredLogs = logs.filter((log) => {
|
||||
const matchesSearch = log.user_email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
log.account_name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesAction = actionFilter === 'all' || log.action === actionFilter;
|
||||
return matchesSearch && matchesAction;
|
||||
});
|
||||
|
||||
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">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Activity className="w-6 h-6" />
|
||||
Activity Logs
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
System activity and audit trail
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 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="flex items-center gap-2">
|
||||
<Filter className="w-5 h-5 text-gray-400" />
|
||||
<select
|
||||
value={actionFilter}
|
||||
onChange={(e) => setActionFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
|
||||
>
|
||||
<option value="all">All Actions</option>
|
||||
<option value="create">Create</option>
|
||||
<option value="update">Update</option>
|
||||
<option value="delete">Delete</option>
|
||||
<option value="login">Login</option>
|
||||
<option value="logout">Logout</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Timestamp</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Account</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Action</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Resource</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Details</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{filteredLogs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">No activity logs found</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredLogs.map((log) => (
|
||||
<tr key={log.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm font-medium">{log.user_email}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">{log.account_name}</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge
|
||||
variant="light"
|
||||
color={
|
||||
log.action === 'create' ? 'success' :
|
||||
log.action === 'update' ? 'primary' :
|
||||
log.action === 'delete' ? 'error' : 'default'
|
||||
}
|
||||
>
|
||||
{log.action}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm">{log.resource_type}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">{log.details}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500">{log.ip_address}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
213
frontend/src/pages/admin/AdminAllAccountsPage.tsx
Normal file
213
frontend/src/pages/admin/AdminAllAccountsPage.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Admin All Accounts Page
|
||||
* List and manage all accounts in the system
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Search, Filter, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
|
||||
interface Account {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
owner_email: string;
|
||||
status: string;
|
||||
credit_balance: number;
|
||||
plan_name: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export default function AdminAllAccountsPage() {
|
||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
loadAccounts();
|
||||
}, []);
|
||||
|
||||
const loadAccounts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchAPI('/v1/admin/accounts/');
|
||||
setAccounts(data.results || []);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load accounts');
|
||||
console.error('Accounts load error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredAccounts = accounts.filter((account) => {
|
||||
const matchesSearch = account.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
account.owner_email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'all' || account.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
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">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">All Accounts</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Manage all accounts in the system
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<div className="mb-6 flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search accounts..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 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="flex items-center gap-2">
|
||||
<Filter className="w-5 h-5 text-gray-400" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="trial">Trial</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Accounts Table */}
|
||||
<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">
|
||||
Account
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Owner
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Plan
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Credits
|
||||
</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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
</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">
|
||||
{filteredAccounts.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">
|
||||
No accounts found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredAccounts.map((account) => (
|
||||
<tr key={account.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-medium text-gray-900 dark:text-white">{account.name}</div>
|
||||
<div className="text-sm text-gray-500">{account.slug}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{account.owner_email}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-900 dark:text-white">
|
||||
{account.plan_name || 'Free'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{account.credit_balance?.toLocaleString() || 0}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge
|
||||
variant="light"
|
||||
color={
|
||||
account.status === 'active' ? 'success' :
|
||||
account.status === 'trial' ? 'primary' :
|
||||
account.status === 'suspended' ? 'error' : 'warning'
|
||||
}
|
||||
>
|
||||
{account.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{new Date(account.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-blue-600 hover:text-blue-700 text-sm font-medium">
|
||||
Manage
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Accounts</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">{accounts.length}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Active</div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{accounts.filter(a => a.status === 'active').length}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Trial</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{accounts.filter(a => a.status === 'trial').length}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Suspended</div>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{accounts.filter(a => a.status === 'suspended').length}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
161
frontend/src/pages/admin/AdminAllInvoicesPage.tsx
Normal file
161
frontend/src/pages/admin/AdminAllInvoicesPage.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Admin All Invoices Page
|
||||
* View and manage all system invoices
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Search, Filter, Loader2, AlertCircle, Download } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { getInvoices, type Invoice } from '../../services/billing.api';
|
||||
|
||||
export default function AdminAllInvoicesPage() {
|
||||
const [invoices, setInvoices] = useState<Invoice[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
loadInvoices();
|
||||
}, []);
|
||||
|
||||
const loadInvoices = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getInvoices({});
|
||||
setInvoices(data.results || []);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load invoices');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredInvoices = invoices.filter((invoice) => {
|
||||
const matchesSearch = invoice.invoice_number.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'all' || invoice.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
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">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">All Invoices</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
View and manage all system invoices
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<div className="mb-6 flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search invoices..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 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="flex items-center gap-2">
|
||||
<Filter className="w-5 h-5 text-gray-400" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="void">Void</option>
|
||||
<option value="uncollectible">Uncollectible</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoices Table */}
|
||||
<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">
|
||||
{filteredInvoices.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-8 text-center text-gray-500">
|
||||
No invoices found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredInvoices.map((invoice) => (
|
||||
<tr key={invoice.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-6 py-4 font-medium text-gray-900 dark:text-white">
|
||||
{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 text-gray-900 dark:text-white">
|
||||
${invoice.total_amount}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge
|
||||
variant="light"
|
||||
color={
|
||||
invoice.status === 'paid' ? 'success' :
|
||||
invoice.status === 'pending' ? 'warning' : 'error'
|
||||
}
|
||||
>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-blue-600 hover:text-blue-700 flex items-center gap-1 ml-auto">
|
||||
<Download className="w-4 h-4" />
|
||||
Download
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
137
frontend/src/pages/admin/AdminAllPaymentsPage.tsx
Normal file
137
frontend/src/pages/admin/AdminAllPaymentsPage.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Admin All Payments Page
|
||||
* View and manage all payment transactions
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Search, Filter, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
|
||||
interface Payment {
|
||||
id: number;
|
||||
account_name: string;
|
||||
amount: string;
|
||||
currency: string;
|
||||
status: string;
|
||||
payment_method: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export default function AdminAllPaymentsPage() {
|
||||
const [payments, setPayments] = useState<Payment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
loadPayments();
|
||||
}, []);
|
||||
|
||||
const loadPayments = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchAPI('/v1/admin/payments/');
|
||||
setPayments(data.results || []);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load payments');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredPayments = payments.filter((payment) => {
|
||||
return statusFilter === 'all' || payment.status === statusFilter;
|
||||
});
|
||||
|
||||
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">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">All Payments</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
View and manage all payment transactions
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<Filter className="w-5 h-5 text-gray-400" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="succeeded">Succeeded</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="refunded">Refunded</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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">Account</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{filteredPayments.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">No payments found</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredPayments.map((payment) => (
|
||||
<tr key={payment.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-6 py-4 font-medium">{payment.account_name}</td>
|
||||
<td className="px-6 py-4 font-semibold">{payment.currency} {payment.amount}</td>
|
||||
<td className="px-6 py-4 text-sm">{payment.payment_method}</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge
|
||||
variant="light"
|
||||
color={
|
||||
payment.status === 'succeeded' ? 'success' :
|
||||
payment.status === 'pending' ? 'warning' : 'error'
|
||||
}
|
||||
>
|
||||
{payment.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">
|
||||
{new Date(payment.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-blue-600 hover:text-blue-700 text-sm">View</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
217
frontend/src/pages/admin/AdminAllUsersPage.tsx
Normal file
217
frontend/src/pages/admin/AdminAllUsersPage.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Admin All Users Page
|
||||
* View and manage all users across all accounts
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Search, Filter, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
account_name: string;
|
||||
role: string;
|
||||
is_active: boolean;
|
||||
last_login: string | null;
|
||||
date_joined: string;
|
||||
}
|
||||
|
||||
export default function AdminAllUsersPage() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [roleFilter, setRoleFilter] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
}, []);
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchAPI('/v1/admin/users/');
|
||||
setUsers(data.results || []);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load users');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredUsers = users.filter((user) => {
|
||||
const matchesSearch = user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
`${user.first_name} ${user.last_name}`.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesRole = roleFilter === 'all' || user.role === roleFilter;
|
||||
return matchesSearch && matchesRole;
|
||||
});
|
||||
|
||||
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">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">All Users</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
View and manage all users across all accounts
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<div className="mb-6 flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search users..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 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="flex items-center gap-2">
|
||||
<Filter className="w-5 h-5 text-gray-400" />
|
||||
<select
|
||||
value={roleFilter}
|
||||
onChange={(e) => setRoleFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
|
||||
>
|
||||
<option value="all">All Roles</option>
|
||||
<option value="owner">Owner</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="editor">Editor</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Users Table */}
|
||||
<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">
|
||||
User
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Account
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Role
|
||||
</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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Last Login
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Joined
|
||||
</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">
|
||||
{filteredUsers.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">
|
||||
No users found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredUsers.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
{user.first_name || user.last_name
|
||||
? `${user.first_name} ${user.last_name}`.trim()
|
||||
: user.email}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{user.email}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{user.account_name}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge variant="light" color="primary">
|
||||
{user.role}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge
|
||||
variant="light"
|
||||
color={user.is_active ? 'success' : 'error'}
|
||||
>
|
||||
{user.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{user.last_login
|
||||
? new Date(user.last_login).toLocaleDateString()
|
||||
: 'Never'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{new Date(user.date_joined).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-blue-600 hover:text-blue-700 text-sm font-medium">
|
||||
Manage
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Users</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">{users.length}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Active</div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{users.filter(u => u.is_active).length}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Owners</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{users.filter(u => u.role === 'owner').length}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Admins</div>
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{users.filter(u => u.role === 'admin').length}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
frontend/src/pages/admin/AdminCreditPackagesPage.tsx
Normal file
114
frontend/src/pages/admin/AdminCreditPackagesPage.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Admin Credit Packages Page
|
||||
* Manage credit packages available for purchase
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Loader2, AlertCircle, Edit, Trash } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { getCreditPackages, type CreditPackage } from '../../services/billing.api';
|
||||
|
||||
export default function AdminCreditPackagesPage() {
|
||||
const [packages, setPackages] = useState<CreditPackage[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
loadPackages();
|
||||
}, []);
|
||||
|
||||
const loadPackages = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getCreditPackages();
|
||||
setPackages(data.results || []);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load credit packages');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Credit Packages</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Manage credit packages available for purchase
|
||||
</p>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Package
|
||||
</button>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{packages.map((pkg) => (
|
||||
<Card key={pkg.id} className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{pkg.name}</h3>
|
||||
{pkg.is_featured && (
|
||||
<Badge variant="light" color="primary" className="mt-1">Featured</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="light" color={pkg.is_active ? 'success' : 'error'}>
|
||||
{pkg.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="text-3xl font-bold text-blue-600">{pkg.credits.toLocaleString()}</div>
|
||||
<div className="text-sm text-gray-500">credits</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<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>
|
||||
|
||||
{pkg.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{pkg.description}</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<button className="flex-1 flex items-center justify-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<Edit className="w-4 h-4" />
|
||||
Edit
|
||||
</button>
|
||||
<button className="px-3 py-2 border border-red-300 dark:border-red-600 text-red-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20">
|
||||
<Trash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{packages.length === 0 && (
|
||||
<div className="col-span-3 text-center py-12 text-gray-500">
|
||||
No credit packages configured. Click "Add Package" to create one.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
147
frontend/src/pages/admin/AdminRolesPermissionsPage.tsx
Normal file
147
frontend/src/pages/admin/AdminRolesPermissionsPage.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Admin Roles & Permissions Page
|
||||
* Manage user roles and permissions
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Shield, Users, Lock, Loader2 } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
|
||||
const roles = [
|
||||
{
|
||||
id: 'developer',
|
||||
name: 'Developer',
|
||||
description: 'Super admin with full system access',
|
||||
color: 'error' as const,
|
||||
userCount: 1,
|
||||
permissions: ['all'],
|
||||
},
|
||||
{
|
||||
id: 'owner',
|
||||
name: 'Owner',
|
||||
description: 'Account owner with full account access',
|
||||
color: 'primary' as const,
|
||||
userCount: 5,
|
||||
permissions: ['manage_account', 'manage_billing', 'manage_team', 'manage_sites', 'view_analytics'],
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
name: 'Admin',
|
||||
description: 'Account admin with most permissions',
|
||||
color: 'success' as const,
|
||||
userCount: 12,
|
||||
permissions: ['manage_team', 'manage_sites', 'view_analytics', 'manage_content'],
|
||||
},
|
||||
{
|
||||
id: 'editor',
|
||||
name: 'Editor',
|
||||
description: 'Can edit content and limited settings',
|
||||
color: 'warning' as const,
|
||||
userCount: 25,
|
||||
permissions: ['manage_content', 'view_analytics'],
|
||||
},
|
||||
{
|
||||
id: 'viewer',
|
||||
name: 'Viewer',
|
||||
description: 'Read-only access',
|
||||
color: 'default' as const,
|
||||
userCount: 10,
|
||||
permissions: ['view_analytics', 'view_content'],
|
||||
},
|
||||
];
|
||||
|
||||
export default function AdminRolesPermissionsPage() {
|
||||
const [selectedRole, setSelectedRole] = useState(roles[0]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Roles & Permissions</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Manage user roles and their permissions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Roles List */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="p-4">
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Shield className="w-5 h-5" />
|
||||
System Roles
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{roles.map((role) => (
|
||||
<button
|
||||
key={role.id}
|
||||
onClick={() => setSelectedRole(role)}
|
||||
className={`w-full text-left p-3 rounded-lg transition-colors ${
|
||||
selectedRole.id === role.id
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-medium text-gray-900 dark:text-white">{role.name}</span>
|
||||
<Badge variant="light" color={role.color}>
|
||||
{role.userCount}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">{role.description}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Role Details */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{selectedRole.name}</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">{selectedRole.description}</p>
|
||||
</div>
|
||||
<Badge variant="light" color={selectedRole.color}>
|
||||
{selectedRole.userCount} users
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
|
||||
<Lock className="w-5 h-5" />
|
||||
Permissions
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{selectedRole.permissions.map((permission) => (
|
||||
<div key={permission} className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked
|
||||
readOnly
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{permission.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
Users with this Role
|
||||
</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{selectedRole.userCount} users currently have the {selectedRole.name} role
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
129
frontend/src/pages/admin/AdminSubscriptionsPage.tsx
Normal file
129
frontend/src/pages/admin/AdminSubscriptionsPage.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Admin All Subscriptions Page
|
||||
* Manage all subscriptions across all accounts
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Search, Filter, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
|
||||
interface Subscription {
|
||||
id: number;
|
||||
account_name: string;
|
||||
status: string;
|
||||
current_period_start: string;
|
||||
current_period_end: string;
|
||||
cancel_at_period_end: boolean;
|
||||
plan_name: string;
|
||||
}
|
||||
|
||||
export default function AdminSubscriptionsPage() {
|
||||
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscriptions();
|
||||
}, []);
|
||||
|
||||
const loadSubscriptions = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchAPI('/v1/admin/subscriptions/');
|
||||
setSubscriptions(data.results || []);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load subscriptions');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredSubscriptions = subscriptions.filter((sub) => {
|
||||
return statusFilter === 'all' || sub.status === statusFilter;
|
||||
});
|
||||
|
||||
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">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">All Subscriptions</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Manage all active and past subscriptions
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<Filter className="w-5 h-5 text-gray-400" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="trialing">Trialing</option>
|
||||
<option value="past_due">Past Due</option>
|
||||
<option value="canceled">Canceled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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">Account</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Plan</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Period End</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{filteredSubscriptions.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-8 text-center text-gray-500">No subscriptions found</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredSubscriptions.map((sub) => (
|
||||
<tr key={sub.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-6 py-4 font-medium">{sub.account_name}</td>
|
||||
<td className="px-6 py-4">{sub.plan_name}</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge variant="light" color={sub.status === 'active' ? 'success' : 'warning'}>
|
||||
{sub.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">
|
||||
{new Date(sub.current_period_end).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-blue-600 hover:text-blue-700 text-sm">Manage</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
216
frontend/src/pages/admin/AdminSystemDashboard.tsx
Normal file
216
frontend/src/pages/admin/AdminSystemDashboard.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Admin System Dashboard
|
||||
* Overview page with stats, alerts, revenue, active accounts, pending approvals
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Users, DollarSign, TrendingUp, AlertCircle,
|
||||
CheckCircle, Clock, Activity, Loader2
|
||||
} from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { getAdminBillingStats } from '../../services/billing.api';
|
||||
|
||||
export default function AdminSystemDashboard() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
}, []);
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getAdminBillingStats();
|
||||
setStats(data);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load system stats');
|
||||
console.error('Admin stats load error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">System Dashboard</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Overview of system health, accounts, and revenue
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Accounts</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{stats?.total_accounts?.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-sm text-green-600 mt-1">
|
||||
+{stats?.new_accounts_this_month || 0} this month
|
||||
</div>
|
||||
</div>
|
||||
<Users className="w-12 h-12 text-blue-600 opacity-50" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Active Subscriptions</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{stats?.active_subscriptions?.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">paying customers</div>
|
||||
</div>
|
||||
<CheckCircle className="w-12 h-12 text-green-600 opacity-50" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Revenue This Month</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
${stats?.revenue_this_month || '0.00'}
|
||||
</div>
|
||||
<div className="text-sm text-green-600 mt-1">
|
||||
<TrendingUp className="w-4 h-4 inline" /> +12% vs last month
|
||||
</div>
|
||||
</div>
|
||||
<DollarSign className="w-12 h-12 text-green-600 opacity-50" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Pending Approvals</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{stats?.pending_approvals || 0}
|
||||
</div>
|
||||
<div className="text-sm text-yellow-600 mt-1">requires attention</div>
|
||||
</div>
|
||||
<Clock className="w-12 h-12 text-yellow-600 opacity-50" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* System Health */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Activity className="w-5 h-5" />
|
||||
System Health
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700 dark:text-gray-300">API Status</span>
|
||||
<Badge variant="light" color="success">Operational</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700 dark:text-gray-300">Database</span>
|
||||
<Badge variant="light" color="success">Healthy</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700 dark:text-gray-300">Background Jobs</span>
|
||||
<Badge variant="light" color="success">Running</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700 dark:text-gray-300">Last Check</span>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{stats?.system_health?.last_check || 'Just now'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Credit Usage</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Issued (30 days)</span>
|
||||
<span className="font-semibold">{stats?.credits_issued_30d?.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: '75%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Used (30 days)</span>
|
||||
<span className="font-semibold">{stats?.credits_used_30d?.toLocaleString() || 0}</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div className="bg-green-600 h-2 rounded-full" style={{ width: '60%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Recent Activity</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="border-b border-gray-200 dark:border-gray-700">
|
||||
<tr>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Type</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Account</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Description</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Amount</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{stats?.recent_activity?.map((activity: any, idx: number) => (
|
||||
<tr key={idx} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="py-3 px-4">
|
||||
<Badge variant="light" color={activity.type === 'purchase' ? 'success' : 'primary'}>
|
||||
{activity.type}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{activity.account_name}</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{activity.description}</td>
|
||||
<td className="py-3 px-4 text-sm text-right font-semibold">
|
||||
{activity.currency} {activity.amount}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-right text-gray-500">
|
||||
{new Date(activity.timestamp).toLocaleTimeString()}
|
||||
</td>
|
||||
</tr>
|
||||
)) || (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-gray-500">No recent activity</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
162
frontend/src/pages/admin/AdminSystemHealthPage.tsx
Normal file
162
frontend/src/pages/admin/AdminSystemHealthPage.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Admin System Health Page
|
||||
* Monitor system health and status
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Activity, Database, Server, Zap, CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
|
||||
interface HealthStatus {
|
||||
component: string;
|
||||
status: 'healthy' | 'degraded' | 'down';
|
||||
message: string;
|
||||
responseTime?: number;
|
||||
lastChecked: string;
|
||||
}
|
||||
|
||||
export default function AdminSystemHealthPage() {
|
||||
const [healthData, setHealthData] = useState<HealthStatus[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadHealthData();
|
||||
const interval = setInterval(loadHealthData, 30000); // Refresh every 30s
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const loadHealthData = async () => {
|
||||
// Mock data - replace with API call
|
||||
setHealthData([
|
||||
{
|
||||
component: 'API Server',
|
||||
status: 'healthy',
|
||||
message: 'All systems operational',
|
||||
responseTime: 45,
|
||||
lastChecked: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
component: 'Database',
|
||||
status: 'healthy',
|
||||
message: 'Connection pool healthy',
|
||||
responseTime: 12,
|
||||
lastChecked: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
component: 'Background Jobs',
|
||||
status: 'healthy',
|
||||
message: '5 workers active',
|
||||
lastChecked: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
component: 'Redis Cache',
|
||||
status: 'healthy',
|
||||
message: 'Cache hit rate: 94%',
|
||||
responseTime: 2,
|
||||
lastChecked: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return <CheckCircle className="w-5 h-5 text-green-600" />;
|
||||
case 'degraded':
|
||||
return <Activity className="w-5 h-5 text-yellow-600" />;
|
||||
case 'down':
|
||||
return <XCircle className="w-5 h-5 text-red-600" />;
|
||||
default:
|
||||
return <Activity className="w-5 h-5 text-gray-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return 'success' as const;
|
||||
case 'degraded':
|
||||
return 'warning' as const;
|
||||
case 'down':
|
||||
return 'error' as const;
|
||||
default:
|
||||
return 'default' as const;
|
||||
}
|
||||
};
|
||||
|
||||
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 allHealthy = healthData.every(item => item.status === 'healthy');
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Activity className="w-6 h-6" />
|
||||
System Health
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Monitor system health and status
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
{allHealthy ? (
|
||||
<CheckCircle className="w-12 h-12 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="w-12 h-12 text-red-600" />
|
||||
)}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{allHealthy ? 'All Systems Operational' : 'System Issues Detected'}
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Last updated: {new Date().toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{healthData.map((item) => (
|
||||
<Card key={item.component} className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon(item.status)}
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{item.component}
|
||||
</h3>
|
||||
</div>
|
||||
<Badge variant="light" color={getStatusColor(item.status)}>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{item.message}</p>
|
||||
|
||||
{item.responseTime && (
|
||||
<div className="text-sm text-gray-500">
|
||||
Response time: {item.responseTime}ms
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-400 mt-2">
|
||||
Last checked: {new Date(item.lastChecked).toLocaleString()}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
173
frontend/src/pages/admin/AdminSystemSettingsPage.tsx
Normal file
173
frontend/src/pages/admin/AdminSystemSettingsPage.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Admin System Settings Page
|
||||
* Configure general system settings
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Save, Settings, Loader2 } from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
|
||||
export default function AdminSystemSettingsPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState({
|
||||
siteName: 'IGNY8 Platform',
|
||||
siteDescription: 'AI-powered content management platform',
|
||||
maintenanceMode: false,
|
||||
allowRegistration: true,
|
||||
requireEmailVerification: true,
|
||||
sessionTimeout: 3600,
|
||||
maxUploadSize: 10,
|
||||
defaultTimezone: 'UTC',
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Settings className="w-6 h-6" />
|
||||
System Settings
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Configure general system settings
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">General Settings</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Site Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.siteName}
|
||||
onChange={(e) => setSettings({ ...settings, siteName: e.target.value })}
|
||||
className="w-full px-4 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-2">
|
||||
Site Description
|
||||
</label>
|
||||
<textarea
|
||||
value={settings.siteDescription}
|
||||
onChange={(e) => setSettings({ ...settings, siteDescription: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-4 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-2">
|
||||
Default Timezone
|
||||
</label>
|
||||
<select
|
||||
value={settings.defaultTimezone}
|
||||
onChange={(e) => setSettings({ ...settings, defaultTimezone: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
|
||||
>
|
||||
<option value="UTC">UTC</option>
|
||||
<option value="America/New_York">Eastern Time</option>
|
||||
<option value="America/Los_Angeles">Pacific Time</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Security & Access</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium">Maintenance Mode</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Disable access to non-admin users
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.maintenanceMode}
|
||||
onChange={(e) => setSettings({ ...settings, maintenanceMode: e.target.checked })}
|
||||
className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium">Allow Registration</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Allow new users to register
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.allowRegistration}
|
||||
onChange={(e) => setSettings({ ...settings, allowRegistration: e.target.checked })}
|
||||
className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium">Require Email Verification</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Users must verify email before accessing
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.requireEmailVerification}
|
||||
onChange={(e) => setSettings({ ...settings, requireEmailVerification: e.target.checked })}
|
||||
className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Limits & Restrictions</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Session Timeout (seconds)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.sessionTimeout}
|
||||
onChange={(e) => setSettings({ ...settings, sessionTimeout: parseInt(e.target.value) })}
|
||||
className="w-full px-4 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-2">
|
||||
Max Upload Size (MB)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.maxUploadSize}
|
||||
onChange={(e) => setSettings({ ...settings, maxUploadSize: parseInt(e.target.value) })}
|
||||
className="w-full px-4 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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export default function AdminPaymentApprovalPage() {
|
||||
setProcessing(paymentId);
|
||||
setError('');
|
||||
|
||||
await approvePayment(paymentId, approvalNotes || undefined);
|
||||
await approvePayment(paymentId, { notes: approvalNotes || undefined });
|
||||
|
||||
// Remove from list
|
||||
setPayments(payments.filter((p) => p.id !== paymentId));
|
||||
@@ -70,7 +70,7 @@ export default function AdminPaymentApprovalPage() {
|
||||
setProcessing(selectedPayment.id);
|
||||
setError('');
|
||||
|
||||
await rejectPayment(selectedPayment.id, rejectReason);
|
||||
await rejectPayment(selectedPayment.id, { reason: rejectReason });
|
||||
|
||||
// Remove from list
|
||||
setPayments(payments.filter((p) => p.id !== selectedPayment.id));
|
||||
|
||||
Reference in New Issue
Block a user