import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { useNavigate, useLocation, Link } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; import api from '../../utils/api'; import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; import { Input } from '../../components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select'; import { Checkbox } from '../../components/ui/checkbox'; import { Badge } from '../../components/ui/badge'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, } from '../../components/ui/dropdown-menu'; import { toast } from 'sonner'; import { Users, Search, User, CreditCard, Eye, CheckCircle, Calendar, AlertCircle, Clock, Mail, UserPlus, Upload, Download, FileDown, ChevronDown, CircleMinus, KeyRound, Loader2, X } from 'lucide-react'; import PaymentActivationDialog from '../../components/PaymentActivationDialog'; import ConfirmationDialog from '../../components/ConfirmationDialog'; import CreateMemberDialog from '../../components/CreateMemberDialog'; import InviteMemberDialog from '../../components/InviteMemberDialog'; import WordPressImportWizard from '../../components/WordPressImportWizard'; import ComprehensiveImportWizard from '../../components/ComprehensiveImportWizard'; import TemplateImportWizard from '../../components/TemplateImportWizard'; import StatusBadge from '../../components/StatusBadge'; import { StatCard } from '@/components/StatCard'; import { useMembers } from '../../hooks/use-users'; const AdminMembers = () => { const navigate = useNavigate(); const location = useLocation(); const { hasPermission } = useAuth(); const { users, filteredUsers, loading, searchQuery, setSearchQuery, filterValue: statusFilter, setFilterValue: setStatusFilter, refetch, } = useMembers(); const [paymentDialogOpen, setPaymentDialogOpen] = useState(false); const [selectedUserForPayment, setSelectedUserForPayment] = useState(null); const [statusChanging, setStatusChanging] = useState(null); const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); const [pendingStatusChange, setPendingStatusChange] = useState(null); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [inviteDialogOpen, setInviteDialogOpen] = useState(false); const [importDialogOpen, setImportDialogOpen] = useState(false); const [comprehensiveImportOpen, setComprehensiveImportOpen] = useState(false); const [templateImportOpen, setTemplateImportOpen] = useState(false); const [exporting, setExporting] = useState(false); // Bulk selection state const [selectedUsers, setSelectedUsers] = useState(new Set()); const [bulkActionLoading, setBulkActionLoading] = useState(false); const [bulkPasswordResetOpen, setBulkPasswordResetOpen] = useState(false); // Import job filter state const [importJobs, setImportJobs] = useState([]); const [selectedImportJob, setSelectedImportJob] = useState(null); const [importJobLoading, setImportJobLoading] = useState(false); // Fetch import jobs on mount useEffect(() => { const fetchImportJobs = async () => { try { const response = await api.get('/admin/users/import-jobs'); // Filter to only show completed/partial jobs that have users const jobsWithUsers = response.data.filter( (job) => job.successful_rows > 0 && ['completed', 'partial'].includes(job.status) ); setImportJobs(jobsWithUsers); } catch (error) { console.error('Failed to fetch import jobs:', error); } }; if (hasPermission('users.import')) { fetchImportJobs(); } }, [hasPermission]); // Select all users from an import job const selectUsersFromImportJob = useCallback(async (jobId) => { if (!jobId) { setSelectedImportJob(null); return; } setImportJobLoading(true); setSelectedImportJob(jobId); try { // Get import job details to get the user IDs const response = await api.get(`/admin/users/import-jobs/${jobId}`); const importedUserIds = response.data.imported_user_ids || []; if (importedUserIds.length === 0) { toast.info('No users found in this import job'); setSelectedImportJob(null); return; } // Filter to only select users that are currently visible in filteredUsers const visibleImportedUsers = filteredUsers.filter((user) => importedUserIds.includes(user.id) ); if (visibleImportedUsers.length === 0) { // If no visible users match, select from all users const allImportedUsers = users.filter((user) => importedUserIds.includes(user.id) ); if (allImportedUsers.length > 0) { setSelectedUsers(new Set(allImportedUsers.map((u) => u.id))); toast.success( `Selected ${allImportedUsers.length} users from import job (some may be hidden by current filters)` ); } else { toast.info('No users from this import job found'); } } else { setSelectedUsers(new Set(visibleImportedUsers.map((u) => u.id))); toast.success(`Selected ${visibleImportedUsers.length} users from import job`); } } catch (error) { const message = error.response?.data?.detail || 'Failed to load import job'; toast.error(message); setSelectedImportJob(null); } finally { setImportJobLoading(false); } }, [filteredUsers, users]); // Check if all visible users are selected const allSelected = useMemo(() => { if (!filteredUsers || filteredUsers.length === 0) return false; return filteredUsers.every((user) => selectedUsers.has(user.id)); }, [filteredUsers, selectedUsers]); // Toggle single user selection const toggleUserSelection = (userId) => { setSelectedUsers((prev) => { const newSet = new Set(prev); if (newSet.has(userId)) { newSet.delete(userId); } else { newSet.add(userId); } return newSet; }); }; // Toggle all visible users const toggleAllUsers = () => { if (allSelected) { setSelectedUsers(new Set()); } else { setSelectedUsers(new Set(filteredUsers.map((user) => user.id))); } }; // Clear selection const clearSelection = () => { setSelectedUsers(new Set()); }; // Handle bulk password reset const handleBulkPasswordReset = async () => { if (selectedUsers.size === 0) { toast.error('No users selected'); return; } setBulkActionLoading(true); try { const response = await api.post('/admin/users/bulk-password-reset', { user_ids: Array.from(selectedUsers), send_email: true, }); toast.success( `Password reset emails sent to ${response.data.successful} users` ); if (response.data.failed > 0) { toast.warning(`${response.data.failed} emails failed to send`); } setBulkPasswordResetOpen(false); clearSelection(); } catch (error) { const message = error.response?.data?.detail || 'Failed to send password reset emails'; toast.error(message); } finally { setBulkActionLoading(false); } }; const handleActivatePayment = (user) => { setSelectedUserForPayment(user); setPaymentDialogOpen(true); }; const handlePaymentSuccess = () => { refetch(); // Refresh list }; const handleStatusChangeRequest = (userId, currentStatus, newStatus, user) => { // Skip confirmation if status didn't actually change if (currentStatus === newStatus) return; setPendingStatusChange({ userId, newStatus, user }); setConfirmDialogOpen(true); }; const confirmStatusChange = async () => { if (!pendingStatusChange) return; const { userId, newStatus } = pendingStatusChange; setStatusChanging(userId); setConfirmDialogOpen(false); try { await api.put(`/admin/users/${userId}/status`, { status: newStatus }); toast.success('Member status updated successfully'); refetch(); // Refresh list } catch (error) { toast.error(error.response?.data?.detail || 'Failed to update status'); } finally { setStatusChanging(null); setPendingStatusChange(null); } }; const handleExport = async (filterType) => { setExporting(true); try { let params = {}; if (filterType === 'current') { if (statusFilter && statusFilter !== 'all') { params.status = statusFilter; } if (searchQuery) { params.search = searchQuery; } } // filterType === 'all' will export all members without filters const response = await api.get('/admin/users/export', { params, responseType: 'blob' }); // Create download link const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', `members_export_${new Date().toISOString().split('T')[0]}.csv`); document.body.appendChild(link); link.click(); link.remove(); toast.success('Members exported successfully'); } catch (error) { toast.error('Failed to export members'); } finally { setExporting(false); } }; const getStatusChangeMessage = () => { if (!pendingStatusChange) return {}; const { newStatus, user } = pendingStatusChange; const userName = `${user.first_name} ${user.last_name}`; const messages = { payment_pending: { title: 'Revert to Payment Pending?', description: `This will change ${userName}'s status back to Payment Pending. They will need to complete payment again to become active.`, variant: 'warning', confirmText: 'Yes, Revert Status', }, active: { title: 'Activate Member?', description: `This will activate ${userName}'s membership. They will gain full access to member features and resources.`, variant: 'success', confirmText: 'Yes, Activate', }, inactive: { title: 'Deactivate Member?', description: `This will deactivate ${userName}'s membership. They will lose access to member-only features but their data will be preserved.`, variant: 'warning', confirmText: 'Yes, Deactivate', }, canceled: { title: 'Cancel Membership?', description: `This will mark ${userName}'s membership as canceled. This indicates they voluntarily ended their membership. Their subscription will not auto-renew.`, variant: 'danger', confirmText: 'Yes, Cancel Membership', }, expired: { title: 'Mark Membership as Expired?', description: `This will mark ${userName}'s membership as expired. This indicates their subscription period has ended without renewal.`, variant: 'warning', confirmText: 'Yes, Mark as Expired', }, }; return messages[newStatus] || { title: 'Confirm Status Change', description: `Are you sure you want to change ${userName}'s status to ${newStatus}?`, variant: 'warning', confirmText: 'Confirm', }; }; const getReminderInfo = (user) => { const emailReminders = user.email_verification_reminders_sent || 0; const eventReminders = user.event_attendance_reminders_sent || 0; const paymentReminders = user.payment_reminders_sent || 0; const renewalReminders = user.renewal_reminders_sent || 0; const totalReminders = emailReminders + eventReminders + paymentReminders + renewalReminders; return { emailReminders, eventReminders, paymentReminders, renewalReminders, totalReminders, lastReminderAt: user.last_email_verification_reminder_at || user.last_event_attendance_reminder_at || user.last_payment_reminder_at || user.last_renewal_reminder_at }; }; return ( <>

Members Management

Manage paying members and their subscriptions.

{hasPermission('users.export') && ( handleExport('all')} className="cursor-pointer"> Export All Members handleExport('current')} className="cursor-pointer"> Export Current View )} {hasPermission('users.import') && ( setTemplateImportOpen(true)} className="cursor-pointer" >
Template Import (Recommended)

Download templates, fill your data, upload

setComprehensiveImportOpen(true)} className="cursor-pointer" >
WordPress Import

For WordPress/PMS exports

setImportDialogOpen(true)} className="cursor-pointer" >
Basic User Import

Simple WordPress users CSV

)} {hasPermission('users.invite') && ( )} {hasPermission('users.create') && ( )}
{/* Stats */}
Quick Overview
u.status === 'active').length} icon={CheckCircle} iconBgClass="text-[var(--green-light)]" dataTestId="stat-active-members" /> u.status === 'payment_pending').length} icon={CreditCard} iconBgClass="text-brand-light-orange" dataTestId="stat-payment-pending-members" /> u.status === 'inactive').length} icon={CircleMinus} iconBgClass=" text-brand-pink" dataTestId="stat-inactive-members" />
{/* Filters */}
setSearchQuery(e.target.value)} className="pl-12 h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple " data-testid="search-members-input" />
{/* Import Job Quick Select */} {hasPermission('users.import') && importJobs.length > 0 && (
Quick Select from Import
{importJobLoading && ( )}
)} {/* Bulk Action Bar */} {selectedUsers.size > 0 && (
{selectedUsers.size} user{selectedUsers.size !== 1 ? 's' : ''} selected {selectedImportJob && ( (from import job) )}
{hasPermission('users.reset_password') && ( )}
)} {/* Members List */} {loading ? (

Loading members...

) : filteredUsers.length > 0 ? (
{/* Select All Row */} {filteredUsers.length > 0 && hasPermission('users.reset_password') && (
)} {filteredUsers.map((user) => { const joinedDate = user.created_at; const memberDate = user.member_since; return (
{/* Selection Checkbox */} {hasPermission('users.reset_password') && (
toggleUserSelection(user.id)} aria-label={`Select ${user.first_name} ${user.last_name}`} />
)} {/* Avatar */}
{user.first_name?.[0]}{user.last_name?.[0]}
{/* Info */}

{user.first_name} {user.last_name}

Email: {user.email}

Phone: {user.phone}

Registered: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}

Member Since: {memberDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}

{user.referred_by_member_name && (

Referred by: {user.referred_by_member_name}

)}
{/* Reminder Info */} {(() => { const reminderInfo = getReminderInfo(user); if (reminderInfo.totalReminders > 0) { return (
{reminderInfo.totalReminders} reminder{reminderInfo.totalReminders !== 1 ? 's' : ''} sent {reminderInfo.totalReminders >= 3 && ( Needs attention )}
{reminderInfo.emailReminders > 0 && (

{reminderInfo.emailReminders} email verification

)} {reminderInfo.eventReminders > 0 && (

{reminderInfo.eventReminders} event attendance

)} {reminderInfo.paymentReminders > 0 && (

{reminderInfo.paymentReminders} payment

)} {reminderInfo.renewalReminders > 0 && (

{reminderInfo.renewalReminders} renewal

)}
{reminderInfo.lastReminderAt && (

Last reminder: {new Date(reminderInfo.lastReminderAt).toLocaleDateString()} at {new Date(reminderInfo.lastReminderAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}

)}
); } return null; })()}
{/* Actions */}
{/* Show Activate Payment button for payment_pending users */} {user.status === 'payment_pending' && ( )}
{/* Status Management */}
Change Status:
); })}
) : (

No Members Found

{searchQuery || statusFilter !== 'all' ? 'Try adjusting your filters' : 'No members yet'}

)} {/* Payment Activation Dialog */} {/* Status Change Confirmation Dialog */} {/* Create/Invite/Import Dialogs */} {/* Bulk Password Reset Confirmation Dialog */}

You are about to send password reset emails to{' '} {selectedUsers.size} user{selectedUsers.size !== 1 ? 's' : ''}.

What happens:

  • Each user will receive an email with a password reset link
  • The force_password_change flag will be set
  • Users must set a new password on their next login

This action cannot be undone. Continue?

} confirmText={bulkActionLoading ? 'Sending...' : 'Send Emails'} variant="info" loading={bulkActionLoading} /> ); }; export default AdminMembers;