import React, { useEffect, useState } from 'react'; 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 { Table, TableHeader, TableBody, TableHead, TableRow, TableCell, } from '../../components/ui/table'; import { Pagination, PaginationContent, PaginationLink, PaginationItem, PaginationPrevious, PaginationNext, PaginationEllipsis, } from '../../components/ui/pagination'; import { toast } from 'sonner'; import { CheckCircle, Clock, Search, ArrowUp, ArrowDown, X, XCircle } from 'lucide-react'; import PaymentActivationDialog from '../../components/PaymentActivationDialog'; import ConfirmationDialog from '../../components/ConfirmationDialog'; import RejectionDialog from '../../components/RejectionDialog'; import StatusBadge from '@/components/StatusBadge'; import { StatCard } from '@/components/StatCard'; const AdminValidations = () => { const { hasPermission } = useAuth(); const [pendingUsers, setPendingUsers] = useState([]); const [filteredUsers, setFilteredUsers] = useState([]); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(null); const [paymentDialogOpen, setPaymentDialogOpen] = useState(false); const [selectedUserForPayment, setSelectedUserForPayment] = useState(null); const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); const [pendingAction, setPendingAction] = useState(null); const [rejectionDialogOpen, setRejectionDialogOpen] = useState(false); const [userToReject, setUserToReject] = useState(null); // Filtering state const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); // Sorting state const [sortBy, setSortBy] = useState('created_at'); const [sortOrder, setSortOrder] = useState('desc'); useEffect(() => { fetchPendingUsers(); }, []); useEffect(() => { filterAndSortUsers(); }, [pendingUsers, searchQuery, statusFilter, sortBy, sortOrder]); useEffect(() => { setCurrentPage(1); }, [searchQuery, statusFilter]); const fetchPendingUsers = async () => { try { const response = await api.get('/admin/users'); const pending = response.data.filter(user => ['pending_email', 'pending_validation', 'pre_validated', 'payment_pending', 'rejected'].includes(user.status) ); setPendingUsers(pending); } catch (error) { toast.error('Failed to fetch pending users'); } finally { setLoading(false); } }; const filterAndSortUsers = () => { let filtered = [...pendingUsers]; // Apply status filter if (statusFilter !== 'all') { filtered = filtered.filter(user => user.status === statusFilter); } // Apply search query if (searchQuery) { const query = searchQuery.toLowerCase(); filtered = filtered.filter(user => user.first_name.toLowerCase().includes(query) || user.last_name.toLowerCase().includes(query) || user.email.toLowerCase().includes(query) || user.phone?.toLowerCase().includes(query) ); } // Apply sorting filtered.sort((a, b) => { let aVal = a[sortBy]; let bVal = b[sortBy]; if (sortBy === 'created_at') { aVal = new Date(aVal); bVal = new Date(bVal); } else if (sortBy === 'first_name') { aVal = `${a.first_name} ${a.last_name}`; bVal = `${b.first_name} ${b.last_name}`; } if (sortOrder === 'asc') { return aVal > bVal ? 1 : -1; } else { return aVal < bVal ? 1 : -1; } }); setFilteredUsers(filtered); }; const handleValidateRequest = (user) => { setPendingAction({ type: 'validate', user }); setConfirmDialogOpen(true); }; const handleBypassAndValidateRequest = (user) => { setPendingAction({ type: 'bypass_and_validate', user }); setConfirmDialogOpen(true); }; const confirmAction = async () => { if (!pendingAction) return; const { type, user } = pendingAction; setActionLoading(user.id); setConfirmDialogOpen(false); try { if (type === 'validate') { await api.put(`/admin/users/${user.id}/validate`); toast.success('User validated! Payment email sent.'); } else if (type === 'bypass_and_validate') { await api.put(`/admin/users/${user.id}/validate?bypass_email_verification=true`); toast.success('User email verified and validated! Payment email sent.'); } fetchPendingUsers(); } catch (error) { toast.error(error.response?.data?.detail || 'Failed to validate user'); } finally { setActionLoading(null); setPendingAction(null); } }; const getActionMessage = () => { if (!pendingAction) return {}; const { type, user } = pendingAction; const userName = `${user.first_name} ${user.last_name}`; if (type === 'validate') { return { title: 'Validate User?', description: `This will validate ${userName} and send them a payment link email. They will be able to complete payment and become an active member.`, variant: 'success', confirmText: 'Yes, Validate User', }; } if (type === 'bypass_and_validate') { return { title: 'Bypass Email & Validate User?', description: `This will bypass email verification for ${userName} and validate them immediately. A payment link email will be sent. Use this only if you've confirmed their email through other means.`, variant: 'warning', confirmText: 'Yes, Bypass & Validate', }; } return {}; }; const handleActivatePayment = (user) => { setSelectedUserForPayment(user); setPaymentDialogOpen(true); }; const handlePaymentSuccess = () => { fetchPendingUsers(); // Refresh list }; const handleRejectUser = (user) => { setUserToReject(user); setRejectionDialogOpen(true); }; const confirmRejection = async (reason) => { if (!userToReject) return; setActionLoading(userToReject.id); try { await api.post(`/admin/users/${userToReject.id}/reject`, { reason }); toast.success(`${userToReject.first_name} ${userToReject.last_name} has been rejected`); fetchPendingUsers(); setRejectionDialogOpen(false); setUserToReject(null); } catch (error) { toast.error(error.response?.data?.detail || 'Failed to reject user'); } finally { setActionLoading(null); } }; const handleReactivateUser = async (user) => { setActionLoading(user.id); try { await api.put(`/admin/users/${user.id}/status`, { status: 'pending_validation' }); toast.success(`${user.first_name} ${user.last_name} has been reactivated and moved to pending validation`); fetchPendingUsers(); } catch (error) { toast.error(error.response?.data?.detail || 'Failed to reactivate user'); } finally { setActionLoading(null); } }; const handleSort = (column) => { if (sortBy === column) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortBy(column); setSortOrder('asc'); } }; // Pagination calculations const totalPages = Math.ceil(filteredUsers.length / itemsPerPage); const paginatedUsers = filteredUsers.slice( (currentPage - 1) * itemsPerPage, currentPage * itemsPerPage ); const renderSortIcon = (column) => { if (sortBy !== column) return null; return sortOrder === 'asc' ? : ; }; return ( <> {/* Header */}

Validation Queue

Review and validate pending membership applications.

{/* Stats Card */}
Quick Overview
u.status === 'pending_email').length} icon={CheckCircle} iconBgClass="text-brand-purple" dataTestId="stat-total-users" /> u.status === 'pending_validation').length} icon={CheckCircle} iconBgClass="text-brand-purple" dataTestId="stat-pending-validation" /> u.status === 'pre_validated').length} icon={CheckCircle} iconBgClass="text-brand-purple" dataTestId="stat-pre-validated" /> u.status === 'payment_pending').length} icon={CheckCircle} iconBgClass="text-brand-purple" dataTestId="stat-payment-pending" /> u.status === 'rejected').length} icon={XCircle} iconBgClass="text-red-600" dataTestId="stat-rejected" />
{/* Filter Card */}
setSearchQuery(e.target.value)} className="pl-12 h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple " />
{/* Table */} {loading ? (

Loading pending applications...

) : filteredUsers.length > 0 ? ( <> handleSort('first_name')} > Name {renderSortIcon('first_name')} Email Phone handleSort('status')} > Status {renderSortIcon('status')} handleSort('created_at')} > Registered {renderSortIcon('created_at')} Referred By Actions {paginatedUsers.map((user) => ( {user.first_name} {user.last_name} {user.email} {user.phone} {new Date(user.created_at).toLocaleDateString()} {user.referred_by_member_name || '-'}
{user.status === 'rejected' ? ( ) : user.status === 'pending_email' ? ( <> {hasPermission('users.approve') && ( )} {hasPermission('users.approve') && ( )} ) : user.status === 'payment_pending' ? ( <> {hasPermission('subscriptions.activate') && ( )} {hasPermission('users.approve') && ( )} ) : ( <> {hasPermission('users.approve') && ( )} {hasPermission('users.approve') && ( )} )}
))}
{/* Pagination Controls */}
{/* Page size selector */}

Show

entries (showing {(currentPage - 1) * itemsPerPage + 1}- {Math.min(currentPage * itemsPerPage, filteredUsers.length)} of {filteredUsers.length})

{/* Pagination */} {totalPages > 1 && ( setCurrentPage(p => Math.max(1, p - 1))} className={currentPage === 1 ? 'pointer-events-none opacity-50' : 'cursor-pointer'} /> {[...Array(totalPages)].map((_, i) => { const showPage = i < 2 || i >= totalPages - 2 || Math.abs(i - currentPage + 1) <= 1; if (!showPage && i === 2) { return ( ); } if (!showPage) return null; return ( setCurrentPage(i + 1)} isActive={currentPage === i + 1} className="cursor-pointer" > {i + 1} ); })} setCurrentPage(p => Math.min(totalPages, p + 1))} className={currentPage === totalPages ? 'pointer-events-none opacity-50' : 'cursor-pointer'} /> )}
) : (

No Pending Validations

{searchQuery || statusFilter !== 'all' ? 'Try adjusting your filters' : 'All applications have been reviewed!'}

)} {/* Payment Activation Dialog */} {/* Validation Confirmation Dialog */} {/* Rejection Dialog */} ); }; export default AdminValidations;