feat: implement StatCard component and integrate it into AdminDashboard and AdminMembers for improved stats display
This commit is contained in:
@@ -14,12 +14,13 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} 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 } from 'lucide-react';
|
||||
import { Users, Search, User, CreditCard, Eye, CheckCircle, Calendar, AlertCircle, Clock, Mail, UserPlus, Upload, Download, FileDown, ChevronDown, CircleMinus } from 'lucide-react';
|
||||
import PaymentActivationDialog from '../../components/PaymentActivationDialog';
|
||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||
import CreateMemberDialog from '../../components/CreateMemberDialog';
|
||||
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
||||
import WordPressImportWizard from '../../components/WordPressImportWizard';
|
||||
import { StatCard } from '@/components/StatCard';
|
||||
|
||||
const AdminMembers = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -319,31 +320,45 @@ const AdminMembers = () => {
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple dark:text-brand-lavender mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Members</p>
|
||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple dark:text-brand-lavender mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active</p>
|
||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'active').length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple dark:text-brand-lavender mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Payment Pending</p>
|
||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'payment_pending').length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple dark:text-brand-lavender mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Inactive</p>
|
||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'inactive').length}
|
||||
</p>
|
||||
</Card>
|
||||
<div className='rounded-3xl bg-brand-lavender/10 p-8 mb-8'>
|
||||
<div className=' text-2xl text-[var(--purple-ink)] pb-8 font-semibold'>
|
||||
Quick Overview
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
|
||||
|
||||
<StatCard
|
||||
title="Total Members"
|
||||
value={users.length}
|
||||
icon={Users}
|
||||
iconBgClass="bg-[var(--blue-light)] text-[var(--blue-dark)]"
|
||||
dataTestId="stat-total-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Active"
|
||||
value={users.filter(u => u.status === 'active').length}
|
||||
icon={CheckCircle}
|
||||
iconBgClass="text-[var(--green-light)]"
|
||||
dataTestId="stat-active-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Payment Pending"
|
||||
value={users.filter(u => u.status === 'payment_pending').length}
|
||||
icon={CreditCard}
|
||||
iconBgClass="text-brand-light-orange"
|
||||
dataTestId="stat-payment-pending-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Inactive"
|
||||
value={users.filter(u => u.status === 'inactive').length}
|
||||
icon={CircleMinus}
|
||||
iconBgClass=" text-brand-pink"
|
||||
dataTestId="stat-inactive-members"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
@@ -394,137 +409,137 @@ const AdminMembers = () => {
|
||||
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-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold text-lg flex-shrink-0">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
<div className="h-14 w-14 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] 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 text-[var(--purple-ink)] " style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h3>
|
||||
{getStatusBadge(user.status)}
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple dark:text-brand-lavender " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone}</p>
|
||||
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||
{user.referred_by_member_name && (
|
||||
<p>Referred by: {user.referred_by_member_name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reminder Info */}
|
||||
{(() => {
|
||||
const reminderInfo = getReminderInfo(user);
|
||||
if (reminderInfo.totalReminders > 0) {
|
||||
return (
|
||||
<div className="mt-4 p-3 bg-[var(--lavender-500)] rounded-lg border border-[var(--neutral-800)]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle className="h-4 w-4 text-[var(--orange-light)]" />
|
||||
<span className="text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{reminderInfo.totalReminders} reminder{reminderInfo.totalReminders !== 1 ? 's' : ''} sent
|
||||
{reminderInfo.totalReminders >= 3 && (
|
||||
<Badge className="ml-2 bg-[var(--orange-light)] text-white px-2 py-0.5 rounded-full text-xs">
|
||||
Needs attention
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{reminderInfo.emailReminders > 0 && (
|
||||
<p>
|
||||
<Mail className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.emailReminders} email verification
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.eventReminders > 0 && (
|
||||
<p>
|
||||
<Calendar className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.eventReminders} event attendance
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.paymentReminders > 0 && (
|
||||
<p>
|
||||
<Clock className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.paymentReminders} payment
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.renewalReminders > 0 && (
|
||||
<p>
|
||||
<CheckCircle className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.renewalReminders} renewal
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{reminderInfo.lastReminderAt && (
|
||||
<p className="mt-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Last reminder: {new Date(reminderInfo.lastReminderAt).toLocaleDateString()} at {new Date(reminderInfo.lastReminderAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
</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 text-[var(--purple-ink)] " style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h3>
|
||||
{getStatusBadge(user.status)}
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple dark:text-brand-lavender " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone}</p>
|
||||
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||
{user.referred_by_member_name && (
|
||||
<p>Referred by: {user.referred_by_member_name}</p>
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Link to={`/admin/users/${user.id}`}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className=""
|
||||
>
|
||||
<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-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-1" />
|
||||
Activate Payment
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reminder Info */}
|
||||
{(() => {
|
||||
const reminderInfo = getReminderInfo(user);
|
||||
if (reminderInfo.totalReminders > 0) {
|
||||
return (
|
||||
<div className="mt-4 p-3 bg-[var(--lavender-500)] rounded-lg border border-[var(--neutral-800)]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle className="h-4 w-4 text-[var(--orange-light)]" />
|
||||
<span className="text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{reminderInfo.totalReminders} reminder{reminderInfo.totalReminders !== 1 ? 's' : ''} sent
|
||||
{reminderInfo.totalReminders >= 3 && (
|
||||
<Badge className="ml-2 bg-[var(--orange-light)] text-white px-2 py-0.5 rounded-full text-xs">
|
||||
Needs attention
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{reminderInfo.emailReminders > 0 && (
|
||||
<p>
|
||||
<Mail className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.emailReminders} email verification
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.eventReminders > 0 && (
|
||||
<p>
|
||||
<Calendar className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.eventReminders} event attendance
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.paymentReminders > 0 && (
|
||||
<p>
|
||||
<Clock className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.paymentReminders} payment
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.renewalReminders > 0 && (
|
||||
<p>
|
||||
<CheckCircle className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.renewalReminders} renewal
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{reminderInfo.lastReminderAt && (
|
||||
<p className="mt-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Last reminder: {new Date(reminderInfo.lastReminderAt).toLocaleDateString()} at {new Date(reminderInfo.lastReminderAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{/* Status Management */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-brand-purple dark:text-brand-lavender whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Change Status:
|
||||
</span>
|
||||
<Select
|
||||
value={user.status}
|
||||
onValueChange={(newStatus) => handleStatusChangeRequest(user.id, user.status, newStatus, user)}
|
||||
disabled={statusChanging === user.id}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-9 border-[var(--neutral-800)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="payment_pending">Payment Pending</SelectItem>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="inactive">Inactive</SelectItem>
|
||||
<SelectItem value="canceled">Canceled</SelectItem>
|
||||
<SelectItem value="expired">Expired</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Link to={`/admin/users/${user.id}`}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className=""
|
||||
>
|
||||
<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-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-1" />
|
||||
Activate Payment
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Management */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-brand-purple dark:text-brand-lavender whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Change Status:
|
||||
</span>
|
||||
<Select
|
||||
value={user.status}
|
||||
onValueChange={(newStatus) => handleStatusChangeRequest(user.id, user.status, newStatus, user)}
|
||||
disabled={statusChanging === user.id}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-9 border-[var(--neutral-800)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="payment_pending">Payment Pending</SelectItem>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="inactive">Inactive</SelectItem>
|
||||
<SelectItem value="canceled">Canceled</SelectItem>
|
||||
<SelectItem value="expired">Expired</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user