From 6c844c0e198cc230cf0150d4621b1bf290a35772 Mon Sep 17 00:00:00 2001 From: kayela Date: Thu, 22 Jan 2026 12:07:56 -0600 Subject: [PATCH 01/11] feat: add @tailwindcss/line-clamp dependency and integrate responsive layout adjustments in Admin components for improved UI --- package.json | 1 + src/components/StatCard.jsx | 97 +++++++++++++++++++++++-------- src/pages/admin/AdminDashboard.js | 6 +- src/pages/admin/AdminMembers.js | 4 +- src/pages/admin/AdminStaff.js | 14 ++--- src/styles/App.css | 1 + tailwind.config.js | 3 +- yarn.lock | 5 ++ 8 files changed, 93 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 5663868..13ec4bf 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@radix-ui/react-tooltip": "^1.2.4", "@stripe/react-stripe-js": "^2.0.0", "@stripe/stripe-js": "^2.0.0", + "@tailwindcss/line-clamp": "^0.4.4", "axios": "^1.8.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/components/StatCard.jsx b/src/components/StatCard.jsx index e6a0f8b..5afc9f2 100644 --- a/src/components/StatCard.jsx +++ b/src/components/StatCard.jsx @@ -7,30 +7,77 @@ export const StatCard = ({ icon: Icon, iconBgClass, dataTestId, -}) => ( - -
-
- -
+}) => { + const valueString = value == null ? "" : String(value); -
-

- {value} -

-
-
-

{ + if (digitCount <= 3) { + // 3.75rem for 3 or fewer digits + return "3.75rem"; + } else if (digitCount <= 6) { + // Scale down for more digits + return "clamp(2rem, 5cqi, 3rem)"; + } else if (digitCount <= 9) { + return "clamp(1.5rem, 4cqi, 2.5rem)"; + } else { + return "clamp(1.25rem, 3cqi, 2rem)"; + } + }; + + * */ + + const getValueFontSize = () => { + switch (true) { + case digitCount <= 3: + // 3.75rem for 3 or fewer digits + return "3.75rem"; + case digitCount <= 6: + // Scale down for more digits + return "clamp(2rem, 5cqi, 3rem)"; + case digitCount <= 9: + return "clamp(1.5rem, 4cqi, 2.5rem)"; + default: + return "clamp(1.25rem, 3cqi, 2rem)"; + } + }; + const valueFontSize = getValueFontSize(); + + return ( + - {title} -

-
-); +
+
+

+ {value} +

+
+ +
+ +
+
+

+ {title} +

+ + ); +}; diff --git a/src/pages/admin/AdminDashboard.js b/src/pages/admin/AdminDashboard.js index fef95b2..8c0a4c2 100644 --- a/src/pages/admin/AdminDashboard.js +++ b/src/pages/admin/AdminDashboard.js @@ -60,7 +60,7 @@ const AdminDashboard = () => { return ( <> -
+

Admin Dashboard @@ -69,9 +69,9 @@ const AdminDashboard = () => { Manage users, events, and membership applications.

- + +
+ + ); +}; + +export default MemberCard \ No newline at end of file diff --git a/src/components/StatusBadge.js b/src/components/StatusBadge.js new file mode 100644 index 0000000..89ce768 --- /dev/null +++ b/src/components/StatusBadge.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { Badge } from './ui/badge'; + +const STATUS_BADGE_CONFIG = { + + //status-based badges + pending_email: { label: 'Pending Email', variant: 'orange2' }, + pending_validation: { label: 'Pending Validation', variant: 'gray' }, + pre_validated: { label: 'Pre-Validated', variant: 'green' }, + payment_pending: { label: 'Payment Pending', variant: 'orange' }, + active: { label: 'Active', variant: 'green' }, + inactive: { label: 'Inactive', variant: 'gray2' }, + canceled: { label: 'Canceled', variant: 'red' }, + expired: { label: 'Expired', variant: 'red2' }, + abandoned: { label: 'Abandoned', variant: 'gray3' }, + rejected: { label: 'Rejected', className: 'bg-red-100 text-red-700' }, + + //role-based badges + finance: { label: 'Finance Manager', variant: 'purple' }, + guest: { label: 'Guest', variant: 'gray' }, + member: { label: 'Member', variant: 'purple' }, + superadmin: { label: 'Superadmin', variant: 'purple' }, + admin: { label: 'Admin', variant: 'purple' }, + moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' }, + staff: { label: 'Staff', variant: 'gray' }, + media: { label: 'Media', variant: 'gray2' } +}; + +//todo: make shield icon dynamic based on status +const StatusBadge = ({ status }) => { + const statusConfig = STATUS_BADGE_CONFIG[status] || STATUS_BADGE_CONFIG.inactive; + + return ( + + {/* */} + {statusConfig.label} + + ); +}; + + +export default StatusBadge; diff --git a/src/hooks/use-members.js b/src/hooks/use-members.js new file mode 100644 index 0000000..e69de29 diff --git a/src/layouts/AdminLayout.js b/src/layouts/AdminLayout.js index a57e9d6..7a120eb 100644 --- a/src/layouts/AdminLayout.js +++ b/src/layouts/AdminLayout.js @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useTheme } from 'next-themes'; +import { Menu } from 'lucide-react'; import AdminSidebar from '../components/AdminSidebar'; const AdminLayout = ({ children }) => { @@ -64,6 +65,23 @@ const AdminLayout = ({ children }) => { {/* Main Content Area */}
+ {isMobile && ( +
+ + + Menu + +
+ )}
{children}
diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js index 6c7d902..3397927 100644 --- a/src/pages/admin/AdminMembers.js +++ b/src/pages/admin/AdminMembers.js @@ -4,7 +4,6 @@ import { useAuth } from '../../context/AuthContext'; 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 { @@ -20,6 +19,7 @@ import ConfirmationDialog from '../../components/ConfirmationDialog'; import CreateMemberDialog from '../../components/CreateMemberDialog'; import InviteStaffDialog from '../../components/InviteStaffDialog'; import WordPressImportWizard from '../../components/WordPressImportWizard'; +import StatusBadge from '../../components/StatusBadge'; import { StatCard } from '@/components/StatCard'; const AdminMembers = () => { @@ -200,27 +200,6 @@ const AdminMembers = () => { }; }; - const getStatusBadge = (status) => { - const config = { - pending_email: { label: 'Pending Email', variant: 'orange2' }, - pending_validation: { label: 'Pending Validation', variant: 'gray' }, - pre_validated: { label: 'Pre-Validated', variant: 'green' }, - payment_pending: { label: 'Payment Pending', variant: 'orange' }, - active: { label: 'Active', variant: 'green' }, - inactive: { label: 'Inactive', variant: 'gray2' }, - canceled: { label: 'Canceled', variant: 'red' }, - expired: { label: 'Expired', variant: 'red2' }, - abandoned: { label: 'Abandoned', variant: 'gray3' } - }; - - const statusConfig = config[status] || config.inactive; - return ( - - {statusConfig.label} - - ); - }; - const getReminderInfo = (user) => { const emailReminders = user.email_verification_reminders_sent || 0; const eventReminders = user.event_attendance_reminders_sent || 0; @@ -325,8 +304,6 @@ const AdminMembers = () => { Quick Overview
- - { iconBgClass=" text-brand-pink" dataTestId="stat-inactive-members" /> - - -
@@ -401,7 +375,8 @@ const AdminMembers = () => { ) : filteredUsers.length > 0 ? (
{filteredUsers.map((user) => { - const joinedDate = user.member_since || user.created_at; + const joinedDate = user.created_at; + const memberDate = user.member_since; return ( {

{user.first_name} {user.last_name}

- {getStatusBadge(user.status)} +

Email: {user.email}

Phone: {user.phone}

-

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

+

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

+

Member Since: {memberDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}

{user.referred_by_member_name && (

Referred by: {user.referred_by_member_name}

)} diff --git a/src/pages/admin/AdminPlans.js b/src/pages/admin/AdminPlans.js index 279e6e3..4d90b44 100644 --- a/src/pages/admin/AdminPlans.js +++ b/src/pages/admin/AdminPlans.js @@ -23,6 +23,7 @@ import { Search, DollarSign } from 'lucide-react'; +import StatusBadge from '@/components/StatusBadge'; const AdminPlans = () => { const { hasPermission } = useAuth(); @@ -236,7 +237,7 @@ const AdminPlans = () => { {plan.active ? 'Active' : 'Inactive'} {plan.subscriber_count > 0 && ( - + {plan.subscriber_count} diff --git a/src/pages/admin/AdminStaff.js b/src/pages/admin/AdminStaff.js index dbbb3ab..f4465bb 100644 --- a/src/pages/admin/AdminStaff.js +++ b/src/pages/admin/AdminStaff.js @@ -12,7 +12,10 @@ import CreateStaffDialog from '../../components/CreateStaffDialog'; import InviteStaffDialog from '../../components/InviteStaffDialog'; import PendingInvitationsTable from '../../components/PendingInvitationsTable'; import { toast } from 'sonner'; -import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye, Trash2, UserCheck, UserX } from 'lucide-react'; +import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye, Trash2, UserCheck, UserX, ShieldIcon } from 'lucide-react'; +import StatusBadge from '../../components/StatusBadge'; +import { StatCard } from '@/components/StatCard'; +import { CircleMinus, CreditCard, Users } from 'lucide-react'; const AdminStaff = () => { const navigate = useNavigate(); @@ -97,37 +100,8 @@ const AdminStaff = () => { } }; - const getRoleBadge = (role) => { - const config = { - superadmin: { label: 'Superadmin', variant: 'purple' }, - admin: { label: 'Admin', variant: 'green' }, - moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' }, - staff: { label: 'Staff', variant: 'gray' }, - media: { label: 'Media', variant: 'gray2' } - }; - const roleConfig = config[role] || { label: role, className: 'bg-gray-500 text-white' }; - return ( - - - {roleConfig.label} - - ); - }; - const getStatusBadge = (status) => { - const config = { - active: { label: 'Active', variant: 'green' }, - inactive: { label: 'Inactive', className: 'bg-gray-400 text-white ' } - }; - - const statusConfig = config[status] || config.inactive; - return ( - - {statusConfig.label} - - ); - }; return ( <> @@ -141,6 +115,7 @@ const AdminStaff = () => { Manage internal team members and their roles.

+
{hasPermission('users.create') && (
{/* Stats */} -
- -

Total Staff

-

- {users.length} -

-
- -

Admins

-

- {users.filter(u => ['admin', 'superadmin'].includes(u.role)).length} -

-
- -

Moderators

-

- {users.filter(u => u.role === 'moderator').length} -

-
- -

Active

-

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

-
+
+
+ Quick Overview +
+
+ ['admin', 'superadmin', 'finance', 'staff', 'media', 'moderator'].includes(u.role)).length} + icon={Users} + iconBgClass="bg-[var(--blue-light)] text-[var(--blue-dark)]" + dataTestId="stat-total-members" + /> + ['admin', 'superadmin'].includes(u.role)).length} + icon={Shield} + iconBgClass="text-[var(--green-light)]" + dataTestId="stat-active-members" + /> + u.role === 'moderator').length} + icon={CreditCard} + iconBgClass="text-brand-light-orange" + dataTestId="stat-payment-pending-members" + /> + ['admin', 'superadmin'].includes(u.role)).length && users.filter(u => u.status !== 'inactive').length} + icon={CircleMinus} + iconBgClass=" text-brand-pink" + dataTestId="stat-inactive-members" + /> +
{/* Tabs */} @@ -250,79 +235,79 @@ const AdminStaff = () => { className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow" data-testid={`staff-card-${user.id}`} > -
-
- {/* Avatar */} -
- {user.first_name?.[0]}{user.last_name?.[0]} -
- - {/* Info */} -
-
-

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

- {getRoleBadge(user.role)} - {getStatusBadge(user.status)} +
+
+ {/* Avatar */} +
+ {user.first_name?.[0]}{user.last_name?.[0]}
-
-

Email: {user.email}

-

Phone: {user.phone}

-

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

- {user.last_login && ( -

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

- )} + + {/* Info */} +
+
+

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

+ + +
+
+

Email: {user.email}

+

Phone: {user.phone}

+

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

+ {user.last_login && ( +

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

+ )} +
-
- {/* Actions */} -
- - - {hasPermission('users.status') && ( + {/* Actions */} +
- )} - {hasPermission('users.delete') && ( - - )} + {hasPermission('users.status') && ( + + )} + + {hasPermission('users.delete') && ( + + )} +
-
); })} diff --git a/src/pages/admin/AdminSubscriptions.js b/src/pages/admin/AdminSubscriptions.js index 815580d..dc6c14c 100644 --- a/src/pages/admin/AdminSubscriptions.js +++ b/src/pages/admin/AdminSubscriptions.js @@ -19,7 +19,6 @@ import { DialogHeader, DialogTitle, } from '../../components/ui/dialog'; -import { Badge } from '../../components/ui/badge'; import api from '../../utils/api'; import { toast } from 'sonner'; import { @@ -47,6 +46,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '../../components/ui/dropdown-menu'; +import StatusBadge from '@/components/StatusBadge'; const AdminSubscriptions = () => { const { hasPermission } = useAuth(); @@ -302,14 +302,7 @@ Proceed with activation?`; } }; - const getStatusBadgeVariant = (status) => { - const variants = { - active: 'default', - cancelled: 'destructive', - expired: 'secondary' - }; - return variants[status] || 'outline'; - }; + if (loading) { return ( @@ -501,7 +494,7 @@ Proceed with activation?`; {sub.user.email}

- {sub.status} +
{/* Plan & Period */} @@ -635,9 +628,8 @@ Proceed with activation?`;
- - {sub.status} - + +
diff --git a/src/pages/admin/AdminUserView.js b/src/pages/admin/AdminUserView.js index 350880a..f47b64c 100644 --- a/src/pages/admin/AdminUserView.js +++ b/src/pages/admin/AdminUserView.js @@ -10,6 +10,7 @@ import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, import { toast } from 'sonner'; import ConfirmationDialog from '../../components/ConfirmationDialog'; import ChangeRoleDialog from '../../components/ChangeRoleDialog'; +import StatusBadge from '../../components/StatusBadge'; const AdminUserView = () => { const { userId } = useParams(); @@ -277,7 +278,7 @@ const AdminUserView = () => { if (loading) return
Loading...
; if (!user) return null; - const joinedDate = user.member_since || user.created_at; + const joinedDate = user.created_at; const memberSinceBaseline = formatDateInputValue(user.member_since); const memberSinceHasChanges = memberSince !== memberSinceBaseline; @@ -311,8 +312,9 @@ const AdminUserView = () => { {user.first_name} {user.last_name} {/* Status & Role Badges */} - {user.status} - {user.role} + + +
{/* Contact Info */} @@ -331,7 +333,7 @@ const AdminUserView = () => {
- Joined {formatDateDisplayValue(joinedDate)} + Registered: {formatDateDisplayValue(joinedDate)}
@@ -506,13 +508,7 @@ const AdminUserView = () => { {sub.plan.billing_cycle}

- - {sub.status} - +
diff --git a/src/pages/admin/AdminValidations.js b/src/pages/admin/AdminValidations.js index d2ab17c..c26a5fd 100644 --- a/src/pages/admin/AdminValidations.js +++ b/src/pages/admin/AdminValidations.js @@ -3,7 +3,6 @@ import { useAuth } from '../../context/AuthContext'; 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, @@ -34,6 +33,7 @@ import { CheckCircle, Clock, Search, ArrowUp, ArrowDown, X } from 'lucide-react' import PaymentActivationDialog from '../../components/PaymentActivationDialog'; import ConfirmationDialog from '../../components/ConfirmationDialog'; import RejectionDialog from '../../components/RejectionDialog'; +import StatusBadge from '@/components/StatusBadge'; const AdminValidations = () => { const { hasPermission } = useAuth(); @@ -235,22 +235,7 @@ const AdminValidations = () => { } }; - const getStatusBadge = (status) => { - const config = { - pending_email: { label: 'Awaiting Email', className: 'bg-orange-100 text-orange-700' }, - pending_validation: { label: 'Pending Validation', className: 'bg-gray-200 text-gray-700' }, - pre_validated: { label: 'Pre-Validated', className: 'bg-[var(--green-light)] text-white' }, - payment_pending: { label: 'Payment Pending', className: 'bg-orange-500 text-white' }, - rejected: { label: 'Rejected', className: 'bg-red-100 text-red-700' } - }; - - const statusConfig = config[status]; - return ( - - {statusConfig.label} - - ); - }; + const handleSort = (column) => { if (sortBy === column) { @@ -401,7 +386,7 @@ const AdminValidations = () => { {user.email} {user.phone} - {getStatusBadge(user.status)} + {new Date(user.created_at).toLocaleDateString()} diff --git a/src/pages/members/MembersDirectory.js b/src/pages/members/MembersDirectory.js index 31bd3d3..161e56a 100644 --- a/src/pages/members/MembersDirectory.js +++ b/src/pages/members/MembersDirectory.js @@ -15,6 +15,8 @@ import { } from '../../components/ui/dialog'; import { User, Search, Mail, MapPin, Phone, Heart, Facebook, Instagram, Twitter, Linkedin, UserCircle, Calendar } from 'lucide-react'; import { useToast } from '../../hooks/use-toast'; +import StatusBadge from '@/components/StatusBadge'; +import MemberCard from '../../components/MemberCard'; const MembersDirectory = () => { const [members, setMembers] = useState([]); @@ -80,9 +82,7 @@ const MembersDirectory = () => { const totalMembers = members.length; - const getInitials = (firstName, lastName) => { - return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); - }; + const getSocialMediaLink = (url) => { if (!url) return null; @@ -118,168 +118,6 @@ const MembersDirectory = () => { :
) } - const MemberCard = ({ member }) => { - const joinedDate = member.member_since || member.created_at; - return ( - - {/* Profile Photo */} -
- {member.profile_photo_url ? ( - {`${member.first_name} - ) : ( -
- - {getInitials(member.first_name, member.last_name)} - -
- )} -
- - {/* Name */} -

- {member.first_name} {member.last_name} -

- - {/* Partner Name */} - {member.directory_partner_name && ( -
- - - Partner: {member.directory_partner_name} - -
- )} - - {/* Bio */} - {member.directory_bio && ( -

- {member.directory_bio} -

- )} - - {/* Member Since */} - {joinedDate && ( -
- - - Member since {new Date(joinedDate).toLocaleDateString('en-US', { - month: 'long', - year: 'numeric' - })} - -
- )} - - {/* Contact Information */} -
- {member.directory_email && ( - - )} - - {member.directory_phone && ( - - )} - - {member.directory_address && ( -
- - - {member.directory_address} - -
- )} -
- - {/* Social Media Links */} - {(member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && ( -
-
- {member.social_media_facebook && ( - - - - )} - - {member.social_media_instagram && ( - - - - )} - - {member.social_media_twitter && ( - - - - )} - - {member.social_media_linkedin && ( - - - - )} -
-
- )} - - {/* View Profile Button */} -
- -
-
- ); - }; return (
@@ -354,7 +192,7 @@ const MembersDirectory = () => { {/* Border Decoration */} - + {/* todo: use badge to display if member */} {/* Info Card */} {!loading && members.length > 0 && ( @@ -377,15 +215,16 @@ const MembersDirectory = () => { )}
- {/* Profile Detail Dialog */} {selectedMember && ( <> - + {selectedMember.first_name} {selectedMember.last_name} + {/* todo: figure out the correct selection to get the status of the user and pass into badge */} + {selectedMember.directory_partner_name && ( @@ -563,8 +402,6 @@ const MembersDirectory = () => { - - {/* Pagination */} {!loading && filteredMembers.length > 0 && (
From 554b5995998c3fd6aa11a6a25fff77c94ebcd7b2 Mon Sep 17 00:00:00 2001 From: kayela Date: Thu, 22 Jan 2026 14:47:34 -0600 Subject: [PATCH 03/11] feat: refactor AdminMembers and AdminStaff to utilize useMembers hook for improved member management --- src/hooks/use-members.js | 68 ++++++++++++++++++++++++++++ src/pages/admin/AdminMembers.js | 58 +++++------------------- src/pages/admin/AdminStaff.js | 78 ++++++++++----------------------- 3 files changed, 103 insertions(+), 101 deletions(-) diff --git a/src/hooks/use-members.js b/src/hooks/use-members.js index e69de29..91c98d3 100644 --- a/src/hooks/use-members.js +++ b/src/hooks/use-members.js @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useState } from 'react'; +import { toast } from 'sonner'; +import api from '../utils/api'; + +const useMembers = ({ + initialFilter = 'active', + initialSearch = '', + filterKey = 'status', + allowedRoles = ['member'], + fetchErrorMessage = 'Failed to fetch members', +} = {}) => { + const [users, setUsers] = useState([]); + const [filteredUsers, setFilteredUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(initialSearch); + const [filterValue, setFilterValue] = useState(initialFilter); + + const fetchMembers = useCallback(async () => { + try { + const response = await api.get('/admin/users'); + let filtered = response.data; + if (allowedRoles && allowedRoles.length) { + filtered = filtered.filter(user => allowedRoles.includes(user.role)); + } + setUsers(filtered); + } catch (error) { + toast.error(fetchErrorMessage); + } finally { + setLoading(false); + } + }, [allowedRoles, fetchErrorMessage]); + + useEffect(() => { + fetchMembers(); + }, [fetchMembers]); + + useEffect(() => { + let filtered = users; + + if (filterValue && filterValue !== 'all') { + filtered = filtered.filter(user => user[filterKey] === filterValue); + } + + 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); + }, [users, searchQuery, filterKey, filterValue]); + + return { + users, + filteredUsers, + loading, + searchQuery, + setSearchQuery, + filterValue, + setFilterValue, + fetchMembers, + }; +}; + +export default useMembers; diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js index 3397927..25d2e3d 100644 --- a/src/pages/admin/AdminMembers.js +++ b/src/pages/admin/AdminMembers.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useNavigate, useLocation, Link } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; import api from '../../utils/api'; @@ -21,16 +21,22 @@ import InviteStaffDialog from '../../components/InviteStaffDialog'; import WordPressImportWizard from '../../components/WordPressImportWizard'; import StatusBadge from '../../components/StatusBadge'; import { StatCard } from '@/components/StatCard'; +import useMembers from '../../hooks/use-members'; const AdminMembers = () => { const navigate = useNavigate(); const location = useLocation(); const { hasPermission } = useAuth(); - const [users, setUsers] = useState([]); - const [filteredUsers, setFilteredUsers] = useState([]); - const [loading, setLoading] = useState(true); - const [searchQuery, setSearchQuery] = useState(''); - const [statusFilter, setStatusFilter] = useState('active'); + const { + users, + filteredUsers, + loading, + searchQuery, + setSearchQuery, + filterValue: statusFilter, + setFilterValue: setStatusFilter, + fetchMembers, + } = useMembers(); const [paymentDialogOpen, setPaymentDialogOpen] = useState(false); const [selectedUserForPayment, setSelectedUserForPayment] = useState(null); const [statusChanging, setStatusChanging] = useState(null); @@ -41,46 +47,6 @@ const AdminMembers = () => { const [importDialogOpen, setImportDialogOpen] = useState(false); const [exporting, setExporting] = useState(false); - 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); diff --git a/src/pages/admin/AdminStaff.js b/src/pages/admin/AdminStaff.js index f4465bb..46a461f 100644 --- a/src/pages/admin/AdminStaff.js +++ b/src/pages/admin/AdminStaff.js @@ -1,10 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs'; @@ -16,71 +15,40 @@ import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye, Trash2, UserCheck, import StatusBadge from '../../components/StatusBadge'; import { StatCard } from '@/components/StatCard'; import { CircleMinus, CreditCard, Users } from 'lucide-react'; +import useMembers from '../../hooks/use-members'; + +// Staff roles (non-guest, non-member) - includes all admin-type roles +const STAFF_ROLES = ['admin', 'superadmin', 'finance']; const AdminStaff = () => { const navigate = useNavigate(); const { hasPermission, user } = useAuth(); - const [users, setUsers] = useState([]); - const [filteredUsers, setFilteredUsers] = useState([]); - const [loading, setLoading] = useState(true); - const [searchQuery, setSearchQuery] = useState(''); - const [roleFilter, setRoleFilter] = useState('all'); + const { + users, + filteredUsers, + loading, + searchQuery, + setSearchQuery, + filterValue: roleFilter, + setFilterValue: setRoleFilter, + fetchMembers, + } = useMembers({ + initialFilter: 'all', + filterKey: 'role', + allowedRoles: STAFF_ROLES, + fetchErrorMessage: 'Failed to fetch staff', + }); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [inviteDialogOpen, setInviteDialogOpen] = useState(false); const [activeTab, setActiveTab] = useState('staff-list'); - // Staff roles (non-guest, non-member) - includes all admin-type roles - const STAFF_ROLES = ['admin', 'superadmin', 'finance']; - - useEffect(() => { - fetchStaff(); - }, []); - - useEffect(() => { - filterUsers(); - }, [users, searchQuery, roleFilter]); - - const fetchStaff = async () => { - try { - const response = await api.get('/admin/users'); - // Filter to only staff roles - const staffUsers = response.data.filter(user => - STAFF_ROLES.includes(user.role) - ); - setUsers(staffUsers); - } catch (error) { - toast.error('Failed to fetch staff'); - } finally { - setLoading(false); - } - }; - - const filterUsers = () => { - let filtered = users; - - if (roleFilter && roleFilter !== 'all') { - filtered = filtered.filter(user => user.role === roleFilter); - } - - 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 handleToggleStatus = async (userId, currentStatus) => { const newStatus = currentStatus === 'active' ? 'inactive' : 'active'; try { await api.put(`/admin/users/${userId}/status`, { status: newStatus }); toast.success(`User ${newStatus === 'active' ? 'activated' : 'deactivated'} successfully`); - fetchStaff(); // Refresh list + fetchMembers(); // Refresh list } catch (error) { toast.error(error.response?.data?.detail || 'Failed to update user status'); } @@ -94,7 +62,7 @@ const AdminStaff = () => { try { await api.delete(`/admin/users/${userId}`); toast.success('User deleted successfully'); - fetchStaff(); // Refresh list + fetchMembers(); // Refresh list } catch (error) { toast.error(error.response?.data?.detail || 'Failed to delete user'); } @@ -336,7 +304,7 @@ const AdminStaff = () => { Date: Thu, 22 Jan 2026 15:23:50 -0600 Subject: [PATCH 04/11] feat: enhance AdminRoles to manage permissions with loading state and role slug updates --- src/pages/admin/AdminRoles.js | 169 ++++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 49 deletions(-) diff --git a/src/pages/admin/AdminRoles.js b/src/pages/admin/AdminRoles.js index 4f5f328..921a060 100644 --- a/src/pages/admin/AdminRoles.js +++ b/src/pages/admin/AdminRoles.js @@ -39,6 +39,7 @@ const AdminRoles = () => { const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showPermissionsModal, setShowPermissionsModal] = useState(false); const [expandedModules, setExpandedModules] = useState({}); + const [savingPermissions, setSavingPermissions] = useState(false); const [formData, setFormData] = useState({ code: '', name: '', @@ -46,6 +47,15 @@ const AdminRoles = () => { permissions: [] }); + const formatRoleSlug = (value) => ( + value + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/_+/g, '-') + .replace(/^_+|_+$/g, '') + ); + useEffect(() => { fetchRoles(); fetchPermissions(); @@ -133,6 +143,7 @@ const AdminRoles = () => { }; const handleSavePermissions = async () => { + setSavingPermissions(true); try { await api.put(`/admin/roles/${selectedRole.id}/permissions`, { permission_codes: selectedPermissions @@ -142,6 +153,8 @@ const AdminRoles = () => { fetchRoles(); } catch (error) { toast.error('Failed to update permissions'); + } finally { + setSavingPermissions(false); } }; @@ -155,6 +168,14 @@ const AdminRoles = () => { }); }; + const addPermissions = (permissionCodes) => { + setSelectedPermissions(prev => [...new Set([...prev, ...permissionCodes])]); + }; + + const removePermissions = (permissionCodes) => { + setSelectedPermissions(prev => prev.filter(code => !permissionCodes.includes(code))); + }; + const toggleModule = (module) => { setExpandedModules(prev => ({ ...prev, @@ -282,8 +303,28 @@ const AdminRoles = () => {
+
- + + { + const nextName = e.target.value; + setFormData(prev => { + const prevAuto = formatRoleSlug(prev.name); + const isAuto = !prev.code || prev.code === prevAuto; + return { + ...prev, + name: nextName, + code: isAuto ? formatRoleSlug(nextName) : prev.code + }; + }); + }} + /> +
+
+ {

-
- - setFormData({ ...formData, name: e.target.value })} - /> -
-