'use client'; import { useState, useEffect, useRef } from 'react'; import { useLanguage } from '@/context/LanguageContext'; import { usersApi, eventsApi, User, Event } from '@/lib/api'; import { parseDate, formatRucDisplay } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { AdminPageSkeleton } from '@/components/ui/Skeleton'; import Input from '@/components/ui/Input'; import { MoreMenu, DropdownItem, BottomSheet, AdminMobileStyles } from '@/components/admin/MobileComponents'; import { TrashIcon, PencilSquareIcon, FunnelIcon, XMarkIcon, MagnifyingGlassIcon, ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; import toast from 'react-hot-toast'; import clsx from 'clsx'; type RegisteredRange = '' | '7d' | '30d' | '90d'; const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; function getPageNumbers(current: number, totalPages: number): (number | '...')[] { if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1); const pages: (number | '...')[] = [1]; const start = Math.max(2, current - 1); const end = Math.min(totalPages - 1, current + 1); if (start > 2) pages.push('...'); for (let i = start; i <= end; i++) pages.push(i); if (end < totalPages - 1) pages.push('...'); pages.push(totalPages); return pages; } function registeredAfterFromRange(range: RegisteredRange): string | undefined { if (!range) return undefined; const days = range === '7d' ? 7 : range === '30d' ? 30 : 90; const date = new Date(Date.now() - days * 24 * 60 * 60 * 1000); return date.toISOString(); } export default function AdminUsersPage() { const { t, locale } = useLanguage(); const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [roleFilter, setRoleFilter] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [hasBookingsFilter, setHasBookingsFilter] = useState<'' | 'yes' | 'no'>(''); const [registeredRange, setRegisteredRange] = useState(''); const [eventFilter, setEventFilter] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); const [editingUser, setEditingUser] = useState(null); const [editForm, setEditForm] = useState({ name: '', email: '', phone: '', role: '' as User['role'], languagePreference: '' as string, accountStatus: '' as string, }); const [saving, setSaving] = useState(false); const [mobileFilterOpen, setMobileFilterOpen] = useState(false); // Debounce the search box like the Emails page does (300ms). useEffect(() => { const handle = setTimeout(() => setDebouncedSearch(searchQuery), 300); return () => clearTimeout(handle); }, [searchQuery]); useEffect(() => { eventsApi.getAll().then((res) => setEvents(res.events)).catch(() => {}); }, []); // When a filter changes, jump back to page 1 before fetching (skipping the // fetch for the stale page); otherwise fetch for the current page/pageSize. const filterKey = JSON.stringify([roleFilter, statusFilter, hasBookingsFilter, registeredRange, eventFilter, debouncedSearch]); const prevFilterKey = useRef(filterKey); useEffect(() => { if (prevFilterKey.current !== filterKey) { prevFilterKey.current = filterKey; if (page !== 1) { setPage(1); return; } } loadUsers(); }, [filterKey, page, pageSize]); const hasActiveFilters = roleFilter || statusFilter || hasBookingsFilter || registeredRange || eventFilter || searchQuery; const clearFilters = () => { setRoleFilter(''); setStatusFilter(''); setHasBookingsFilter(''); setRegisteredRange(''); setEventFilter(''); setSearchQuery(''); }; const loadUsers = async () => { try { const { users, total } = await usersApi.getAll({ role: roleFilter || undefined, accountStatus: statusFilter || undefined, hasBookings: hasBookingsFilter || undefined, registeredAfter: registeredAfterFromRange(registeredRange), eventId: eventFilter || undefined, search: debouncedSearch.trim() || undefined, page, pageSize, }); setUsers(users); setTotal(total); // If the current page emptied out (e.g. after deleting the last user on // it), fall back to the new last page. if (users.length === 0 && total > 0 && page > 1) { setPage(Math.max(1, Math.ceil(total / pageSize))); } } catch (error) { toast.error('Failed to load users'); } finally { setLoading(false); } }; const handleRoleChange = async (userId: string, newRole: string) => { try { await usersApi.update(userId, { role: newRole as any }); toast.success('Role updated'); loadUsers(); } catch (error) { toast.error('Failed to update role'); } }; const handleDelete = async (userId: string) => { if (!confirm('Are you sure you want to delete this user?')) return; try { await usersApi.delete(userId); toast.success('User deleted'); loadUsers(); } catch (error: any) { toast.error(error.message || 'Failed to delete user'); } }; const openEditModal = (user: User) => { setEditingUser(user); setEditForm({ name: user.name, email: user.email, phone: user.phone || '', role: user.role, languagePreference: user.languagePreference || '', accountStatus: user.accountStatus || 'active', }); }; const handleEditSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!editingUser) return; if (!editForm.name.trim() || editForm.name.trim().length < 2) { toast.error('Name must be at least 2 characters'); return; } if (!editForm.email.trim()) { toast.error('Email is required'); return; } setSaving(true); try { await usersApi.update(editingUser.id, { name: editForm.name.trim(), email: editForm.email.trim(), phone: editForm.phone.trim() || undefined, role: editForm.role, languagePreference: editForm.languagePreference || undefined, accountStatus: editForm.accountStatus || undefined, } as Partial); toast.success('User updated successfully'); setEditingUser(null); loadUsers(); } catch (error: any) { toast.error(error.message || 'Failed to update user'); } finally { setSaving(false); } }; const formatDate = (dateStr: string) => { return parseDate(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'America/Asuncion', }); }; const getRoleBadge = (role: string) => { const styles: Record = { admin: 'badge-danger', organizer: 'badge-info', staff: 'badge-warning', marketing: 'badge-success', user: 'badge-gray', }; return {t(`admin.users.roles.${role}`)}; }; if (loading) { return ; } return (

{t('admin.users.title')} ({total})

{/* Desktop Filters */}
setSearchQuery(e.target.value)} className="w-full pl-9 pr-3 py-2 rounded-btn border border-secondary-light-gray text-sm focus:outline-none focus:ring-2 focus:ring-primary-yellow" />
{hasActiveFilters && (
Showing {users.length} of {total}
)}
{/* Mobile Toolbar */}
setSearchQuery(e.target.value)} className="w-full pl-9 pr-3 py-2.5 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow" />
{hasActiveFilters && ( )} {total} users
{/* Desktop: Table */}
{users.length === 0 ? ( ) : ( users.map((user) => ( )) )}
User Contact RUC Role Joined Actions
No users found
{user.name.charAt(0).toUpperCase()}

{user.name}

{user.email}

{user.phone || '-'} {formatRucDisplay(user.rucNumber) || '-'} {formatDate(user.createdAt)}
{/* Mobile: Card List */}
{users.length === 0 ? (
No users found
) : ( users.map((user) => (
{user.name.charAt(0).toUpperCase()}

{user.name}

{user.email}

{user.phone &&

{user.phone}

} {user.rucNumber &&

RUC: {formatRucDisplay(user.rucNumber)}

}
{getRoleBadge(user.role)}

Joined {formatDate(user.createdAt)}

openEditModal(user)}> Edit User handleDelete(user.id)} className="text-red-600"> Delete
)) )}
{/* Pagination */} {total > 0 && (
{(page - 1) * pageSize + 1}–{Math.min(page * pageSize, total)} of {total}
{getPageNumbers(page, Math.max(1, Math.ceil(total / pageSize))).map((p, i) => p === '...' ? ( ) : ( ) )}
)} {/* Mobile Filter BottomSheet */} setMobileFilterOpen(false)} title="Filters">
{/* Edit User Modal */} {editingUser && (

Edit User

setEditForm({ ...editForm, name: e.target.value })} required minLength={2} /> setEditForm({ ...editForm, email: e.target.value })} required /> setEditForm({ ...editForm, phone: e.target.value })} placeholder="Optional" />
)}
); }