Initial Commit

This commit is contained in:
Koncept Kit
2025-12-05 16:40:33 +07:00
parent 0834f12410
commit 94c7d5aec0
91 changed files with 20446 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useLocation, Link } from 'react-router-dom';
import api from '../../utils/api';
import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
import { Badge } from '../../components/ui/badge';
import { Input } from '../../components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
import { toast } from 'sonner';
import { Users, Search, User, CreditCard, Eye, CheckCircle } from 'lucide-react';
import PaymentActivationDialog from '../../components/PaymentActivationDialog';
const AdminMembers = () => {
const navigate = useNavigate();
const location = useLocation();
const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('active');
const [paymentDialogOpen, setPaymentDialogOpen] = useState(false);
const [selectedUserForPayment, setSelectedUserForPayment] = useState(null);
const tabs = [
{ name: 'All Users', path: '/admin/users' },
{ name: 'Staff', path: '/admin/staff' },
{ name: 'Members', path: '/admin/members' }
];
useEffect(() => {
fetchMembers();
}, []);
useEffect(() => {
filterUsers();
}, [users, searchQuery, statusFilter]);
const fetchMembers = async () => {
try {
const response = await api.get('/admin/users');
// Filter to only members
const members = response.data.filter(user => user.role === 'member');
setUsers(members);
} catch (error) {
toast.error('Failed to fetch members');
} finally {
setLoading(false);
}
};
const filterUsers = () => {
let filtered = users;
if (statusFilter && statusFilter !== 'all') {
filtered = filtered.filter(user => user.status === statusFilter);
}
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)
);
}
setFilteredUsers(filtered);
};
const handleActivatePayment = (user) => {
setSelectedUserForPayment(user);
setPaymentDialogOpen(true);
};
const handlePaymentSuccess = () => {
fetchMembers(); // Refresh list
};
const getStatusBadge = (status) => {
const config = {
pending_email: { label: 'Pending Email', className: 'bg-[#F2CC8F] text-[#3D405B]' },
pending_approval: { label: 'Pending Approval', className: 'bg-[#A3B1C6] text-white' },
pre_approved: { label: 'Pre-Approved', className: 'bg-[#81B29A] text-white' },
payment_pending: { label: 'Payment Pending', className: 'bg-[#E07A5F] text-white' },
active: { label: 'Active', className: 'bg-[#81B29A] text-white' },
inactive: { label: 'Inactive', className: 'bg-[#6B708D] text-white' }
};
const statusConfig = config[status] || config.inactive;
return (
<Badge className={`${statusConfig.className} px-3 py-1 rounded-full text-sm`}>
{statusConfig.label}
</Badge>
);
};
return (
<>
<div className="mb-8">
<h1 className="text-4xl md:text-5xl font-semibold fraunces text-[#3D405B] mb-4">
Members Management
</h1>
<p className="text-lg text-[#6B708D]">
Manage paying members and their subscriptions.
</p>
</div>
{/* Tab Navigation */}
<div className="border-b border-[#EAE0D5] mb-8">
<nav className="flex gap-8">
{tabs.map((tab) => (
<button
key={tab.path}
onClick={() => navigate(tab.path)}
className={`
pb-4 px-2 font-medium transition-colors relative
${location.pathname === tab.path
? 'text-[#E07A5F]'
: 'text-[#6B708D] hover:text-[#3D405B]'
}
`}
>
{tab.name}
{location.pathname === tab.path && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#E07A5F]" />
)}
</button>
))}
</nav>
</div>
{/* Stats */}
<div className="grid md:grid-cols-4 gap-4 mb-8">
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5]">
<p className="text-sm text-[#6B708D] mb-2">Total Members</p>
<p className="text-3xl font-semibold fraunces text-[#3D405B]">
{users.length}
</p>
</Card>
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5]">
<p className="text-sm text-[#6B708D] mb-2">Active</p>
<p className="text-3xl font-semibold fraunces text-[#3D405B]">
{users.filter(u => u.status === 'active').length}
</p>
</Card>
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5]">
<p className="text-sm text-[#6B708D] mb-2">Payment Pending</p>
<p className="text-3xl font-semibold fraunces text-[#3D405B]">
{users.filter(u => u.status === 'payment_pending').length}
</p>
</Card>
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5]">
<p className="text-sm text-[#6B708D] mb-2">Inactive</p>
<p className="text-3xl font-semibold fraunces text-[#3D405B]">
{users.filter(u => u.status === 'inactive').length}
</p>
</Card>
</div>
{/* Filters */}
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5] mb-8">
<div className="grid md:grid-cols-2 gap-4">
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-[#6B708D]" />
<Input
placeholder="Search by name or email..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-12 h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="search-members-input"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="h-14 rounded-xl border-2 border-[#EAE0D5]" data-testid="status-filter-select">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="payment_pending">Payment Pending</SelectItem>
<SelectItem value="pending_approval">Pending Approval</SelectItem>
<SelectItem value="pre_approved">Pre-Approved</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
</Card>
{/* Members List */}
{loading ? (
<div className="text-center py-20">
<p className="text-[#6B708D]">Loading members...</p>
</div>
) : filteredUsers.length > 0 ? (
<div className="space-y-4">
{filteredUsers.map((user) => (
<Card
key={user.id}
className="p-6 bg-white rounded-2xl border border-[#EAE0D5] hover:shadow-md transition-shadow"
data-testid={`member-card-${user.id}`}
>
<div className="flex justify-between items-start flex-wrap gap-4">
<div className="flex items-start gap-4 flex-1">
{/* Avatar */}
<div className="h-14 w-14 rounded-full bg-[#F2CC8F] flex items-center justify-center text-[#3D405B] font-semibold text-lg flex-shrink-0">
{user.first_name?.[0]}{user.last_name?.[0]}
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2 flex-wrap">
<h3 className="text-xl font-semibold fraunces text-[#3D405B]">
{user.first_name} {user.last_name}
</h3>
{getStatusBadge(user.status)}
</div>
<div className="grid md:grid-cols-2 gap-2 text-sm text-[#6B708D]">
<p>Email: {user.email}</p>
<p>Phone: {user.phone}</p>
<p>Joined: {new Date(user.created_at).toLocaleDateString()}</p>
{user.referred_by_member_name && (
<p>Referred by: {user.referred_by_member_name}</p>
)}
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
<Link to={`/admin/users/${user.id}`}>
<Button
variant="outline"
size="sm"
className="border-[#A3B1C6] text-[#A3B1C6] hover:bg-[#A3B1C6] hover:text-white"
>
<Eye className="h-4 w-4 mr-1" />
View Profile
</Button>
</Link>
{/* Show Activate Payment button for payment_pending users */}
{user.status === 'payment_pending' && (
<Button
onClick={() => handleActivatePayment(user)}
size="sm"
className="bg-[#E07A5F] text-white hover:bg-[#D0694E]"
>
<CheckCircle className="h-4 w-4 mr-1" />
Activate Payment
</Button>
)}
{/* Show Subscription button for active users */}
{user.status === 'active' && (
<Button
variant="outline"
size="sm"
className="border-[#81B29A] text-[#81B29A] hover:bg-[#81B29A] hover:text-white"
>
<CreditCard className="h-4 w-4 mr-1" />
Subscription
</Button>
)}
</div>
</div>
</Card>
))}
</div>
) : (
<div className="text-center py-20">
<Users className="h-20 w-20 text-[#EAE0D5] mx-auto mb-6" />
<h3 className="text-2xl font-semibold fraunces text-[#3D405B] mb-4">
No Members Found
</h3>
<p className="text-[#6B708D]">
{searchQuery || statusFilter !== 'all'
? 'Try adjusting your filters'
: 'No members yet'}
</p>
</div>
)}
{/* Payment Activation Dialog */}
<PaymentActivationDialog
open={paymentDialogOpen}
onOpenChange={setPaymentDialogOpen}
user={selectedUserForPayment}
onSuccess={handlePaymentSuccess}
/>
</>
);
};
export default AdminMembers;