188 lines
7.3 KiB
TypeScript
188 lines
7.3 KiB
TypeScript
/**
|
|
* Admin All Subscriptions Page
|
|
* Manage all subscriptions across all accounts
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useLocation } from 'react-router-dom';
|
|
import { Search, Filter, Loader2, AlertCircle, Check, X, RefreshCw } from 'lucide-react';
|
|
import { Card } from '../../components/ui/card';
|
|
import Badge from '../../components/ui/badge/Badge';
|
|
import { fetchAPI } from '../../services/api';
|
|
import Button from '../../components/ui/button/Button';
|
|
|
|
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;
|
|
account: number;
|
|
plan: number | 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');
|
|
const [actionLoadingId, setActionLoadingId] = useState<number | null>(null);
|
|
const location = useLocation();
|
|
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(location.search);
|
|
const accountId = params.get('account_id');
|
|
loadSubscriptions(accountId ? Number(accountId) : undefined);
|
|
}, [location.search]);
|
|
|
|
const loadSubscriptions = async (accountId?: number) => {
|
|
try {
|
|
setLoading(true);
|
|
const query = accountId ? `?account_id=${accountId}` : '';
|
|
const data = await fetchAPI(`/v1/admin/subscriptions/${query}`);
|
|
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;
|
|
});
|
|
|
|
const changeStatus = async (id: number, action: 'activate' | 'cancel') => {
|
|
try {
|
|
setActionLoadingId(id);
|
|
const endpoint = action === 'activate'
|
|
? `/v1/admin/subscriptions/${id}/activate/`
|
|
: `/v1/admin/subscriptions/${id}/cancel/`;
|
|
await fetchAPI(endpoint, { method: 'POST' });
|
|
await loadSubscriptions();
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to update subscription');
|
|
} finally {
|
|
setActionLoadingId(null);
|
|
}
|
|
};
|
|
|
|
const refreshPlans = async () => {
|
|
await loadSubscriptions();
|
|
};
|
|
|
|
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 space-x-2">
|
|
{sub.status !== 'active' && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => changeStatus(sub.id, 'activate')}
|
|
disabled={actionLoadingId === sub.id}
|
|
startIcon={actionLoadingId === sub.id ? <Loader2 className="w-4 h-4 animate-spin" /> : undefined}
|
|
>
|
|
Activate
|
|
</Button>
|
|
)}
|
|
{sub.status === 'active' && (
|
|
<Button
|
|
variant="outline"
|
|
tone="neutral"
|
|
size="sm"
|
|
onClick={() => changeStatus(sub.id, 'cancel')}
|
|
disabled={actionLoadingId === sub.id}
|
|
startIcon={actionLoadingId === sub.id ? <Loader2 className="w-4 h-4 animate-spin" /> : undefined}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={refreshPlans}
|
|
startIcon={<RefreshCw className="w-4 h-4" />}
|
|
>
|
|
Refresh
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|