From 3822ba8ffb8f950fa5f2d3e29c10ae14b989de98 Mon Sep 17 00:00:00 2001 From: kayela Date: Tue, 20 Jan 2026 12:33:17 -0600 Subject: [PATCH 1/6] feat: add member since date handling across admin and member views --- src/pages/Dashboard.js | 3 +- src/pages/admin/AdminMembers.js | 21 +++++---- src/pages/admin/AdminStaff.js | 21 +++++---- src/pages/admin/AdminUserView.js | 65 ++++++++++++++++++++++++++- src/pages/members/MembersDirectory.js | 13 +++--- 5 files changed, 98 insertions(+), 25 deletions(-) diff --git a/src/pages/Dashboard.js b/src/pages/Dashboard.js index 1b4f052..eb9429a 100644 --- a/src/pages/Dashboard.js +++ b/src/pages/Dashboard.js @@ -17,6 +17,7 @@ const Dashboard = () => { const [resendLoading, setResendLoading] = useState(false); const [eventActivity, setEventActivity] = useState(null); const [activityLoading, setActivityLoading] = useState(true); + const joinedDate = user?.member_since || user?.created_at; useEffect(() => { fetchUpcomingEvents(); @@ -197,7 +198,7 @@ const Dashboard = () => {

Member Since

- {user?.created_at ? new Date(user.created_at).toLocaleDateString() : 'N/A'} + {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}

{user?.subscription_start_date && user?.subscription_end_date && ( diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js index 5a2d181..1091f80 100644 --- a/src/pages/admin/AdminMembers.js +++ b/src/pages/admin/AdminMembers.js @@ -385,13 +385,15 @@ const AdminMembers = () => { ) : filteredUsers.length > 0 ? (
- {filteredUsers.map((user) => ( - -
+ {filteredUsers.map((user) => { + const joinedDate = user.member_since || user.created_at; + return ( + +
{/* Avatar */}
@@ -409,7 +411,7 @@ const AdminMembers = () => {

Email: {user.email}

Phone: {user.phone}

-

Joined: {new Date(user.created_at).toLocaleDateString()}

+

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

{user.referred_by_member_name && (

Referred by: {user.referred_by_member_name}

)} @@ -523,7 +525,8 @@ const AdminMembers = () => {
- ))} + ); + })}
) : (
diff --git a/src/pages/admin/AdminStaff.js b/src/pages/admin/AdminStaff.js index e484e69..046a681 100644 --- a/src/pages/admin/AdminStaff.js +++ b/src/pages/admin/AdminStaff.js @@ -242,12 +242,14 @@ const AdminStaff = () => {
) : filteredUsers.length > 0 ? (
- {filteredUsers.map((user) => ( - + {filteredUsers.map((user) => { + const joinedDate = user.member_since || user.created_at; + return ( +
{/* Avatar */} @@ -267,7 +269,7 @@ const AdminStaff = () => {

Email: {user.email}

Phone: {user.phone}

-

Joined: {new Date(user.created_at).toLocaleDateString()}

+

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

{user.last_login && (

Last Login: {new Date(user.last_login).toLocaleDateString()}

)} @@ -321,8 +323,9 @@ const AdminStaff = () => { )}
- - ))} + + ); + })}
) : (
diff --git a/src/pages/admin/AdminUserView.js b/src/pages/admin/AdminUserView.js index d94bd76..d20c48d 100644 --- a/src/pages/admin/AdminUserView.js +++ b/src/pages/admin/AdminUserView.js @@ -5,6 +5,7 @@ import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; import { Badge } from '../../components/ui/badge'; import { Avatar, AvatarImage, AvatarFallback } from '../../components/ui/avatar'; +import { Input } from '../../components/ui/input'; import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, Upload, Trash2, Shield } from 'lucide-react'; import { toast } from 'sonner'; import ConfirmationDialog from '../../components/ConfirmationDialog'; @@ -24,15 +25,31 @@ const AdminUserView = () => { const [uploadingPhoto, setUploadingPhoto] = useState(false); const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); + const [memberSince, setMemberSince] = useState(''); + const [memberSinceSaving, setMemberSinceSaving] = useState(false); const fileInputRef = useRef(null); const [changeRoleDialogOpen, setChangeRoleDialogOpen] = useState(false); + const formatDateInputValue = (value) => { + if (!value) return ''; + if (typeof value === 'string') { + return value.slice(0, 10); + } + return new Date(value).toISOString().slice(0, 10); + }; + useEffect(() => { fetchConfig(); fetchUserProfile(); fetchSubscriptions(); }, [userId]); + useEffect(() => { + if (user) { + setMemberSince(formatDateInputValue(user.member_since)); + } + }, [user]); + const fetchUserProfile = async () => { try { const response = await api.get(`/admin/users/${userId}`); @@ -177,6 +194,27 @@ const AdminUserView = () => { } }; + const handleMemberSinceSave = async () => { + if (!user) return; + setMemberSinceSaving(true); + try { + const payload = { + member_since: memberSince ? memberSince : null + }; + const response = await api.put(`/admin/users/${userId}`, payload); + setUser(prev => ({ + ...prev, + ...(response?.data || {}), + member_since: payload.member_since + })); + toast.success('Member since updated successfully'); + } catch (error) { + toast.error(error.response?.data?.detail || 'Failed to update member since'); + } finally { + setMemberSinceSaving(false); + } + }; + const getActionMessage = () => { if (!pendingAction || !user) return {}; @@ -212,6 +250,10 @@ const AdminUserView = () => { if (loading) return
Loading...
; if (!user) return null; + const joinedDate = user.member_since || user.created_at; + const memberSinceBaseline = formatDateInputValue(user.member_since); + const memberSinceHasChanges = memberSince !== memberSinceBaseline; + return ( <> {/* Back Button */} @@ -262,7 +304,7 @@ const AdminUserView = () => {
- Joined {new Date(user.created_at).toLocaleDateString()} + Joined {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}
@@ -363,6 +405,27 @@ const AdminUserView = () => {

+
+ +
+ setMemberSince(e.target.value)} + className="max-w-[200px] border-[var(--neutral-800)]" + /> + +
+
+ {user.partner_first_name && (
diff --git a/src/pages/members/MembersDirectory.js b/src/pages/members/MembersDirectory.js index 5d17b86..31bd3d3 100644 --- a/src/pages/members/MembersDirectory.js +++ b/src/pages/members/MembersDirectory.js @@ -118,8 +118,10 @@ const MembersDirectory = () => { :
) } - const MemberCard = ({ member }) => ( - + const MemberCard = ({ member }) => { + const joinedDate = member.member_since || member.created_at; + return ( + {/* Profile Photo */}
{member.profile_photo_url ? ( @@ -160,11 +162,11 @@ const MembersDirectory = () => { )} {/* Member Since */} - {member.created_at && ( + {joinedDate && (
- Member since {new Date(member.created_at).toLocaleDateString('en-US', { + Member since {new Date(joinedDate).toLocaleDateString('en-US', { month: 'long', year: 'numeric' })} @@ -276,7 +278,8 @@ const MembersDirectory = () => {
- ); + ); + }; return (
-- 2.39.5 From c73ebfb6c0b4bf594db827bab04d01de640f966d Mon Sep 17 00:00:00 2001 From: kayela Date: Tue, 20 Jan 2026 14:43:17 -0600 Subject: [PATCH 2/6] feat: implement StatCard component and integrate it into AdminDashboard and AdminMembers for improved stats display --- src/components/StatCard.jsx | 36 ++++ src/components/ui/card.jsx | 57 ++++-- src/pages/admin/AdminDashboard.js | 88 ++++----- src/pages/admin/AdminMembers.js | 315 ++++++++++++++++-------------- 4 files changed, 278 insertions(+), 218 deletions(-) create mode 100644 src/components/StatCard.jsx diff --git a/src/components/StatCard.jsx b/src/components/StatCard.jsx new file mode 100644 index 0000000..e6a0f8b --- /dev/null +++ b/src/components/StatCard.jsx @@ -0,0 +1,36 @@ +import React from "react"; +import { Card } from "./ui/card"; + +export const StatCard = ({ + title, + value, + icon: Icon, + iconBgClass, + dataTestId, +}) => ( + +
+
+ +
+ +
+

+ {value} +

+
+
+

+ {title} +

+
+); diff --git a/src/components/ui/card.jsx b/src/components/ui/card.jsx index 2985cca..853f881 100644 --- a/src/components/ui/card.jsx +++ b/src/components/ui/card.jsx @@ -1,50 +1,65 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Card = React.forwardRef(({ className, ...props }, ref) => (
-)) -Card.displayName = "Card" + className={cn( + "rounded-xl border bg-card text-card-foreground shadow", + className, + )} + {...props} + /> +)); +Card.displayName = "Card"; const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
-)) -CardHeader.displayName = "CardHeader" + {...props} + /> +)); +CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
-)) -CardTitle.displayName = "CardTitle" + {...props} + /> +)); +CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
-)) -CardDescription.displayName = "CardDescription" + {...props} + /> +)); +CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef(({ className, ...props }, ref) => (
-)) -CardContent.displayName = "CardContent" +)); +CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
-)) -CardFooter.displayName = "CardFooter" + {...props} + /> +)); +CardFooter.displayName = "CardFooter"; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/src/pages/admin/AdminDashboard.js b/src/pages/admin/AdminDashboard.js index 6c2d934..fef95b2 100644 --- a/src/pages/admin/AdminDashboard.js +++ b/src/pages/admin/AdminDashboard.js @@ -4,13 +4,16 @@ import api from '../../utils/api'; import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; import { Badge } from '../../components/ui/badge'; -import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle, Globe } from 'lucide-react'; +import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle, Globe, CircleMinus } from 'lucide-react'; +import { StatCard } from '../../components/StatCard'; + const AdminDashboard = () => { const [stats, setStats] = useState({ totalMembers: 0, pendingValidations: 0, - activeMembers: 0 + activeMembers: 0, + inactiveMembers: 0 }); const [usersNeedingAttention, setUsersNeedingAttention] = useState([]); const [loading, setLoading] = useState(true); @@ -29,7 +32,8 @@ const AdminDashboard = () => { pendingValidations: users.filter(u => ['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(u.status) ).length, - activeMembers: users.filter(u => u.status === 'active' && u.role === 'member').length + activeMembers: users.filter(u => u.status === 'active' && u.role === 'member').length, + inactiveMembers: users.filter(u => u.status === 'inactive' && u.role === 'member').length }); // Find users who have received 3+ reminders (may need personal outreach) @@ -76,52 +80,42 @@ const AdminDashboard = () => {
{/* Stats Grid */} -
- -
-

- {loading ? '-' : stats.totalMembers} -

-
-
- -
-
-
-

Total Members

-
+
+
+ Quick Overview +
+
+ + + + - -
-

- {loading ? '-' : stats.pendingValidations} -

-
-
- -
-
- -
- -

Pending Validations

-
- - -
-

- {loading ? '-' : stats.activeMembers} -

-
-
- -
-
- -
-

Active Members

-
+
{/* Quick Actions */} diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js index 1091f80..39614ba 100644 --- a/src/pages/admin/AdminMembers.js +++ b/src/pages/admin/AdminMembers.js @@ -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 = () => {
{/* Stats */} -
- -

Total Members

-

- {users.length} -

-
- -

Active

-

- {users.filter(u => u.status === 'active').length} -

-
- -

Payment Pending

-

- {users.filter(u => u.status === 'payment_pending').length} -

-
- -

Inactive

-

- {users.filter(u => u.status === 'inactive').length} -

-
+
+
+ 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 */} @@ -394,137 +409,137 @@ const AdminMembers = () => { data-testid={`member-card-${user.id}`} >
-
- {/* Avatar */} -
- {user.first_name?.[0]}{user.last_name?.[0]} +
+ {/* Avatar */} +
+ {user.first_name?.[0]}{user.last_name?.[0]} +
+ + {/* Info */} +
+
+

+ {user.first_name} {user.last_name} +

+ {getStatusBadge(user.status)} +
+
+

Email: {user.email}

+

Phone: {user.phone}

+

Joined: {joinedDate ? 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; + })()} +
- {/* Info */} -
-
-

- {user.first_name} {user.last_name} -

- {getStatusBadge(user.status)} -
-
-

Email: {user.email}

-

Phone: {user.phone}

-

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

- {user.referred_by_member_name && ( -

Referred by: {user.referred_by_member_name}

+ {/* Actions */} +
+
+ + + + + {/* Show Activate Payment button for payment_pending users */} + {user.status === 'payment_pending' && ( + )}
- {/* 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; - })()} + {/* Status Management */} +
+ + Change Status: + + +
- - {/* Actions */} -
-
- - - - - {/* Show Activate Payment button for payment_pending users */} - {user.status === 'payment_pending' && ( - - )} -
- - {/* Status Management */} -
- - Change Status: - - -
-
-
- + ); })}
-- 2.39.5 From 819062d69742c7e33186b039428c303f2a507796 Mon Sep 17 00:00:00 2001 From: kayela Date: Tue, 20 Jan 2026 14:45:05 -0600 Subject: [PATCH 3/6] fixed spacing in AdminMembers.js --- src/pages/admin/AdminMembers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js index 39614ba..c1e6a95 100644 --- a/src/pages/admin/AdminMembers.js +++ b/src/pages/admin/AdminMembers.js @@ -324,7 +324,7 @@ const AdminMembers = () => {
Quick Overview
-
+
Date: Tue, 20 Jan 2026 16:02:54 -0600 Subject: [PATCH 4/6] fix: update color styles in AdminSidebar, Register, and CSS files for improved UI consistency --- src/components/AdminSidebar.js | 4 ++-- src/pages/Register.js | 4 ++-- src/styles/components.css | 16 +++++++--------- src/styles/theme.css | 3 +-- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/components/AdminSidebar.js b/src/components/AdminSidebar.js index 26c8702..12401d5 100644 --- a/src/components/AdminSidebar.js +++ b/src/components/AdminSidebar.js @@ -224,7 +224,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { ${item.disabled ? 'opacity-50 cursor-not-allowed text-brand-purple ' : active - ? 'bg-[var(--orange-light)]/10 text-[var(--orange-light)]' + ? 'bg-[var(--orange-light)]/10 text-[var(--purple-ink)]' : 'text-[var(--purple-ink)] hover:bg-[var(--neutral-800)]/20' } `} @@ -254,7 +254,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { {/* Badge when collapsed */} {!isOpen && item.badge > 0 && !item.disabled && ( -
+
{item.badge}
)} diff --git a/src/pages/Register.js b/src/pages/Register.js index a1b31ab..7bd7fc5 100644 --- a/src/pages/Register.js +++ b/src/pages/Register.js @@ -258,7 +258,7 @@ const Register = () => {
+ {/* Member Since */} +
+ + handleChange('member_since', e.target.value)} + className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple " + /> +
+ {/* Role */}
- Joined {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'} + Joined {formatDateDisplayValue(joinedDate)}
@@ -401,7 +428,7 @@ const AdminUserView = () => {

- {new Date(user.date_of_birth).toLocaleDateString()} + {formatDateDisplayValue(user.date_of_birth)}

@@ -492,14 +519,14 @@ const AdminUserView = () => {

- {new Date(sub.start_date).toLocaleDateString()} + {formatDateDisplayValue(sub.start_date)}

{sub.end_date && (

- {new Date(sub.end_date).toLocaleDateString()} + {formatDateDisplayValue(sub.end_date)}

)} -- 2.39.5