import React, { useState, useEffect } from 'react'; import { useAuth } from '../../context/AuthContext'; import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; import { Input } from '../../components/ui/input'; import { Label } from '../../components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../../components/ui/dialog'; import api from '../../utils/api'; import { toast } from 'sonner'; import { DollarSign, CreditCard, TrendingUp, Heart, Search, Loader2, Calendar, Edit, XCircle, Download, FileDown, AlertTriangle, Info, ChevronDown, ChevronUp, ExternalLink, Copy } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../../components/ui/dropdown-menu'; import StatusBadge from '@/components/StatusBadge'; const AdminSubscriptions = () => { const { hasPermission } = useAuth(); const [subscriptions, setSubscriptions] = useState([]); const [filteredSubscriptions, setFilteredSubscriptions] = useState([]); const [plans, setPlans] = useState([]); const [stats, setStats] = useState({}); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [planFilter, setPlanFilter] = useState('all'); const [exporting, setExporting] = useState(false); const [expandedRows, setExpandedRows] = useState(new Set()); // Edit subscription dialog state const [editDialogOpen, setEditDialogOpen] = useState(false); const [selectedSubscription, setSelectedSubscription] = useState(null); const [editFormData, setEditFormData] = useState({ status: '', end_date: '' }); const [isUpdating, setIsUpdating] = useState(false); useEffect(() => { fetchData(); }, []); useEffect(() => { filterSubscriptions(); }, [searchQuery, statusFilter, planFilter, subscriptions]); const fetchData = async () => { setLoading(true); try { const [subsResponse, statsResponse, plansResponse] = await Promise.all([ api.get('/admin/subscriptions'), api.get('/admin/subscriptions/stats'), api.get('/admin/subscriptions/plans') ]); setSubscriptions(subsResponse.data); setStats(statsResponse.data); setPlans(plansResponse.data); } catch (error) { console.error('Failed to fetch subscription data:', error); toast.error('Failed to load subscription data'); } finally { setLoading(false); } }; const filterSubscriptions = () => { let filtered = [...subscriptions]; // Search filter (member name or email) if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); filtered = filtered.filter(sub => sub.user.first_name.toLowerCase().includes(query) || sub.user.last_name.toLowerCase().includes(query) || sub.user.email.toLowerCase().includes(query) ); } // Status filter if (statusFilter !== 'all') { filtered = filtered.filter(sub => sub.status === statusFilter); } // Plan filter if (planFilter !== 'all') { filtered = filtered.filter(sub => sub.plan.id === planFilter); } setFilteredSubscriptions(filtered); }; const handleEdit = (subscription) => { setSelectedSubscription(subscription); setEditFormData({ status: subscription.status, end_date: subscription.end_date ? new Date(subscription.end_date).toISOString().split('T')[0] : '' }); setEditDialogOpen(true); }; const handleSaveSubscription = async () => { if (!selectedSubscription) return; // Check if status is changing const statusChanged = editFormData.status !== selectedSubscription.status; if (statusChanged) { // Get status change consequences let warningMessage = ''; let confirmText = ''; if (editFormData.status === 'cancelled') { warningMessage = `⚠️ CRITICAL: Cancelling this subscription will: • Set the user's status to INACTIVE • Remove their member access immediately • Stop all future billing • This action affects: ${selectedSubscription.user.first_name} ${selectedSubscription.user.last_name} (${selectedSubscription.user.email}) Current Status: ${selectedSubscription.status.toUpperCase()} New Status: CANCELLED Are you absolutely sure you want to proceed?`; confirmText = 'Yes, Cancel Subscription'; } else if (editFormData.status === 'expired') { warningMessage = `⚠️ WARNING: Setting this subscription to EXPIRED will: • Set the user's status to INACTIVE • Remove their member access • Mark the subscription as ended • This action affects: ${selectedSubscription.user.first_name} ${selectedSubscription.user.last_name} (${selectedSubscription.user.email}) Current Status: ${selectedSubscription.status.toUpperCase()} New Status: EXPIRED Are you sure you want to proceed?`; confirmText = 'Yes, Mark as Expired'; } else if (editFormData.status === 'active') { warningMessage = `✓ Activating this subscription will: • Set the user's status to ACTIVE • Grant full member access • Resume billing if applicable • This action affects: ${selectedSubscription.user.first_name} ${selectedSubscription.user.last_name} (${selectedSubscription.user.email}) Current Status: ${selectedSubscription.status.toUpperCase()} New Status: ACTIVE Proceed with activation?`; confirmText = 'Yes, Activate Subscription'; } // Show confirmation dialog const confirmed = window.confirm(warningMessage); if (!confirmed) { return; // User cancelled } } setIsUpdating(true); try { await api.put(`/admin/subscriptions/${selectedSubscription.id}`, { status: editFormData.status, end_date: editFormData.end_date ? new Date(editFormData.end_date).toISOString() : null }); toast.success('Subscription updated successfully'); setEditDialogOpen(false); fetchData(); // Refresh data } catch (error) { console.error('Failed to update subscription:', error); toast.error(error.response?.data?.detail || 'Failed to update subscription'); } finally { setIsUpdating(false); } }; const handleCancelSubscription = async (subscriptionId) => { if (!window.confirm('Are you sure you want to cancel this subscription? This will also set the user status to inactive.')) { return; } try { await api.post(`/admin/subscriptions/${subscriptionId}/cancel`); toast.success('Subscription cancelled successfully'); fetchData(); // Refresh data } catch (error) { console.error('Failed to cancel subscription:', error); toast.error(error.response?.data?.detail || 'Failed to cancel subscription'); } }; const handleExport = async (exportType) => { setExporting(true); try { const params = exportType === 'current' ? { status: statusFilter !== 'all' ? statusFilter : undefined, plan_id: planFilter !== 'all' ? planFilter : undefined, search: searchQuery || undefined } : {}; const response = await api.get('/admin/subscriptions/export', { params, responseType: 'blob' }); const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', `subscriptions_export_${new Date().toISOString().split('T')[0]}.csv`); document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); toast.success('Subscriptions exported successfully'); } catch (error) { console.error('Failed to export subscriptions:', error); toast.error('Failed to export subscriptions'); } finally { setExporting(false); } }; const formatPrice = (cents) => { return `$${(cents / 100).toFixed(2)}`; }; const formatDate = (dateString) => { if (!dateString) return 'N/A'; return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); }; const formatDateTime = (dateString) => { if (!dateString) return 'N/A'; return new Date(dateString).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const toggleRowExpansion = (subscriptionId) => { setExpandedRows((prev) => { const newExpanded = new Set(prev); if (newExpanded.has(subscriptionId)) { newExpanded.delete(subscriptionId); } else { newExpanded.add(subscriptionId); } return newExpanded; }); }; const copyToClipboard = async (text, label) => { try { await navigator.clipboard.writeText(text); toast.success(`${label} copied to clipboard`); } catch (error) { toast.error('Failed to copy to clipboard'); } }; if (loading) { return (
View and manage all member subscriptions
Total Subscriptions
{stats.total || 0}
Active Members
{stats.active || 0}
Total Revenue
{formatPrice(stats.total_revenue || 0)}
Total Donations
{formatPrice(stats.total_donations || 0)}
{sub.user.first_name} {sub.user.last_name}
{sub.user.email}
Plan
{sub.plan.name}
{sub.plan.billing_cycle}
Period
{new Date(sub.current_period_start).toLocaleDateString()} - {new Date(sub.current_period_end).toLocaleDateString()}
Base Fee
${(sub.base_fee_cents / 100).toFixed(2)}
Donation
${(sub.donation_cents / 100).toFixed(2)}
Total
${(sub.total_cents / 100).toFixed(2)}
| Member | Plan | Status | Period | Base Fee | Donation | Total | Details | Actions |
|---|---|---|---|---|---|---|---|---|
|
{sub.user.first_name} {sub.user.last_name}
{sub.user.email}
|
{sub.plan.name}
{sub.plan.billing_cycle}
|
|
{formatDate(sub.start_date)}
to {formatDate(sub.end_date)}
|
{formatPrice(sub.base_subscription_cents || 0)} | {formatPrice(sub.donation_cents || 0)} | {formatPrice(sub.amount_paid_cents || 0)} |
{hasPermission('subscriptions.edit') && (
)}
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
)}
|
|
Transaction Details
{/* Payment Information */}
|
||||||||
| No subscriptions found | ||||||||