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/components/CreateMemberDialog.js b/src/components/CreateMemberDialog.js
index c287564..02b077e 100644
--- a/src/components/CreateMemberDialog.js
+++ b/src/components/CreateMemberDialog.js
@@ -31,6 +31,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
});
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({});
+ const getTodayDate = () => new Date().toISOString().slice(0, 10);
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
@@ -84,8 +85,8 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
if (payload.date_of_birth === '') {
delete payload.date_of_birth;
}
- if (payload.member_since === '') {
- delete payload.member_since;
+ if (!payload.member_since) {
+ payload.member_since = getTodayDate();
}
await api.post('/admin/users/create', payload);
diff --git a/src/components/CreateStaffDialog.js b/src/components/CreateStaffDialog.js
index 5bbb606..166806f 100644
--- a/src/components/CreateStaffDialog.js
+++ b/src/components/CreateStaffDialog.js
@@ -22,10 +22,12 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
first_name: '',
last_name: '',
phone: '',
+ member_since: '',
role: 'admin'
});
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({});
+ const getTodayDate = () => new Date().toISOString().slice(0, 10);
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
@@ -74,7 +76,11 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
setLoading(true);
try {
- await api.post('/admin/users/create', formData);
+ const payload = { ...formData };
+ if (!payload.member_since) {
+ payload.member_since = getTodayDate();
+ }
+ await api.post('/admin/users/create', payload);
toast.success('Staff member created successfully');
// Reset form
@@ -84,6 +90,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
first_name: '',
last_name: '',
phone: '',
+ member_since: '',
role: 'admin'
});
@@ -200,6 +207,20 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
)}
+ {/* Member Since */}
+
+
+ handleChange('member_since', e.target.value)}
+ className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
+ />
+
+
{/* Role */}
{/* 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 5a2d181..c1e6a95 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 */}
@@ -385,145 +400,148 @@ const AdminMembers = () => {
) : filteredUsers.length > 0 ? (
- {filteredUsers.map((user) => (
-
-
-
- {/* Avatar */}
-
- {user.first_name?.[0]}{user.last_name?.[0]}
+ {filteredUsers.map((user) => {
+ const joinedDate = user.member_since || user.created_at;
+ return (
+
+
+
+ {/* 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: {new Date(user.created_at).toLocaleDateString()}
- {user.referred_by_member_name && (
-
Referred by: {user.referred_by_member_name}
+ {/* Actions */}
+
+
+
+
+
+ View Profile
+
+
+
+ {/* Show Activate Payment button for payment_pending users */}
+ {user.status === 'payment_pending' && (
+ handleActivatePayment(user)}
+ size="sm"
+ className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
+ >
+
+ Activate Payment
+
)}
- {/* 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 */}
-
-
-
-
-
- View Profile
-
-
-
- {/* Show Activate Payment button for payment_pending users */}
- {user.status === 'payment_pending' && (
- handleActivatePayment(user)}
- size="sm"
- className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
- >
-
- Activate Payment
-
- )}
-
-
- {/* Status Management */}
-
-
- Change Status:
-
-
-
-
-
-
- ))}
+
+ );
+ })}
) : (
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..350880a 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,58 @@ 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 formatLocalDateInputValue = (date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+ };
+
+ const formatDateInputValue = (value) => {
+ if (!value) return '';
+ if (typeof value === 'string') {
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
+ return value;
+ }
+ const parsed = new Date(value);
+ if (Number.isNaN(parsed.getTime())) {
+ return value.slice(0, 10);
+ }
+ return formatLocalDateInputValue(parsed);
+ }
+ const parsed = new Date(value);
+ if (Number.isNaN(parsed.getTime())) return '';
+ return formatLocalDateInputValue(parsed);
+ };
+
+ const formatDateDisplayValue = (value) => {
+ if (!value) return 'N/A';
+ if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
+ const [year, month, day] = value.split('-').map(Number);
+ return new Date(year, month - 1, day).toLocaleDateString();
+ }
+ const parsed = new Date(value);
+ if (Number.isNaN(parsed.getTime())) return 'N/A';
+ return parsed.toLocaleDateString();
+ };
+
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 +221,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 +277,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 +331,7 @@ const AdminUserView = () => {
- Joined {new Date(user.created_at).toLocaleDateString()}
+ Joined {formatDateDisplayValue(joinedDate)}
@@ -359,10 +428,31 @@ const AdminUserView = () => {
- {new Date(user.date_of_birth).toLocaleDateString()}
+ {formatDateDisplayValue(user.date_of_birth)}
+
+
+
+ setMemberSince(e.target.value)}
+ className="max-w-[200px] border-[var(--neutral-800)]"
+ />
+
+ {memberSinceSaving ? 'Saving...' : 'Save'}
+
+
+
+
{user.partner_first_name && (
@@ -429,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)}
)}
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 (
diff --git a/src/styles/components.css b/src/styles/components.css
index 494188d..3654b21 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -17,7 +17,7 @@
}
.btn-ghost {
- @apply hover:bg-brand-purple bg-brand-purple/10 rounded-full disabled:opacity-50 px-6 transition-transform text-brand-purple hover:text-[var(--purple-muted)];
+ @apply hover:bg-brand-purple bg-brand-purple/10 rounded-full disabled:opacity-50 px-6 transition-transform text-brand-purple hover:text-background;
}
.btn-outline {
@@ -56,10 +56,10 @@
@apply h-9 w-9 rounded-full disabled:opacity-50 px-6;
}
.btn-green {
- @apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-full px-6 ;
+ @apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-full px-6;
}
.btn-util-green {
- @apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-xl h-12 px-6 ;
+ @apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-xl h-12 px-6;
}
.btn-util-purple {
@@ -76,21 +76,19 @@
@apply bg-[var(--purple-lavender)] text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center gap-2 dark:hover:bg-foreground dark:hover:text-background;
}
.btn-light-lavender {
- @apply bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-lavender rounded-full px-6 transition-transform dark:hover:bg-brand-lavender dark:hover:text-brand-dark-lavender ;
+ @apply bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-lavender rounded-full px-6 transition-transform dark:hover:bg-brand-lavender dark:hover:text-brand-dark-lavender;
}
/* Badges */
-
+
.badge {
@apply px-3 py-1 rounded-full text-sm font-medium transition-transform;
}
.badge-green {
@apply bg-[var(--green-light)] text-white transition-transform;
}
- /* Backgrounds */
- .bg-light-lavender {
+ /* Backgrounds */
+ .bg-light-lavender {
@apply bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] text-white bg-[var(--neutral-800)] transition-transform dark:bg-brand-lavender dark:text-brand-dark-lavender;
}
-
-
}
diff --git a/src/styles/theme.css b/src/styles/theme.css
index fd138a9..7096ed1 100644
--- a/src/styles/theme.css
+++ b/src/styles/theme.css
@@ -78,7 +78,7 @@
--green-muted: #66927e;
--green-soft: #6a9680;
--green-eucalyptus: #6a9a83;
- --green-forest: #5e8EsOkvcwL7472374;
+ --green-forest: #5a7d68;
--green-fern: #6da085;
--green-mint: #6fa087;
--green-pastel: #6fa188;
@@ -243,4 +243,3 @@
--neutral-900: #f4f4ff; /* highest contrast text */
}
}
-