// ─────────────────────────────────────────────────────────── // Huio Control Panel — Users / Members View (cp-users-view.jsx) // ─────────────────────────────────────────────────────────── if (!window.CP_STYLES) throw new Error('[cp-users-view] window.CP_STYLES not loaded'); function renderUsers({ lang = 'vi', t: rawT, appStyles: rawAppStyles, users: rawUsers = [], userSearchQuery = '', setUserSearchQuery = () => {}, userRoleFilter = 'all', setUserRoleFilter = () => {}, userPlanFilter = 'all', setUserPlanFilter = () => {}, userStatusFilter = 'all', setUserStatusFilter = () => {}, userSortBy = 'last_active_desc', setUserSortBy = () => {}, userPage = 1, setUserPage = () => {}, userPerPage = 10, setUserPerPage = () => {}, handlePlanChange = () => {}, handleRoleChange = () => {}, handleToggleSuspend = () => {}, handleToggleFeature = () => {}, openSupportChatForMember = () => {}, registrySupportOpening = false, }) { const appStyles = rawAppStyles || window.CP_STYLES || {}; const t = typeof rawT === 'function' ? rawT : (k) => k; const users = Array.isArray(rawUsers) ? rawUsers : []; const formatPhone = window.formatPhone || ((p) => p || '—'); const formatDateTime = window.formatDateTime || ((d) => d || '—'); const formatRelativeTime = window.formatRelativeTime || ((d, l) => d || '—'); const getInitials = window.getInitials || ((n) => (n ? n.slice(0, 2).toUpperCase() : '??')); const getAvatarColor = window.getAvatarColor || (() => '#4A7A96'); // Calculate filtered users const filteredUsers = users.filter(u => { if (userSearchQuery.trim()) { const q = userSearchQuery.toLowerCase().trim(); const matchName = u.name && u.name.toLowerCase().includes(q); const matchPhone = u.phone && u.phone.toLowerCase().includes(q); const matchDisplayId = u.displayId && u.displayId.toLowerCase().includes(q); const matchId = u.id && u.id.toLowerCase().includes(q); if (!matchName && !matchPhone && !matchDisplayId && !matchId) return false; } if (userRoleFilter !== 'all' && u.role !== userRoleFilter) return false; if (userPlanFilter !== 'all' && u.plan !== userPlanFilter) return false; if (userStatusFilter !== 'all' && u.status !== userStatusFilter) return false; return true; }); // Calculate sorted users const sortedUsers = [...filteredUsers].sort((a, b) => { if (userSortBy === 'last_active_desc') { return new Date(b.lastActiveAt) - new Date(a.lastActiveAt); } if (userSortBy === 'last_active_asc') { return new Date(a.lastActiveAt) - new Date(b.lastActiveAt); } if (userSortBy === 'name_asc') { return (a.name || '').localeCompare(b.name || ''); } if (userSortBy === 'name_desc') { return (b.name || '').localeCompare(a.name || ''); } if (userSortBy === 'join_desc') { return new Date(b.createdIso || b.date) - new Date(a.createdIso || a.date); } if (userSortBy === 'join_asc') { return new Date(a.createdIso || a.date) - new Date(b.createdIso || b.date); } if (userSortBy === 'plan_desc') { const planRank = { organizer_business: 4, organizer_pro: 3, premium_auto: 3, basic_assistant: 2, free_trial: 1, free: 0, }; return (planRank[b.plan] || 0) - (planRank[a.plan] || 0); } return 0; }); // Calculate pagination const userTotalPages = Math.max(1, Math.ceil(sortedUsers.length / userPerPage)); const safeUserPage = Math.min(userPage, userTotalPages); const paginatedUsers = sortedUsers.slice((safeUserPage - 1) * userPerPage, safeUserPage * userPerPage); const active30dCount = users.filter(u => { const diffMs = Date.now() - new Date(u.lastActiveAt).getTime(); return diffMs < 30 * 86400 * 1000; }).length; const verifiedCount = users.filter(u => u.status === 'verified').length; const suspendedCount = users.filter(u => u.suspendedAt).length; const handleHeaderSort = (colKey) => { if (colKey === 'name') { setUserSortBy(prev => prev === 'name_asc' ? 'name_desc' : 'name_asc'); } else if (colKey === 'plan') { setUserSortBy(prev => prev === 'plan_desc' ? 'name_asc' : 'plan_desc'); } else if (colKey === 'last_active') { setUserSortBy(prev => prev === 'last_active_desc' ? 'last_active_asc' : 'last_active_desc'); } else if (colKey === 'join') { setUserSortBy(prev => prev === 'join_desc' ? 'join_asc' : 'join_desc'); } setUserPage(1); }; const renderSortArrow = (colKey) => { let isActive = false; let isAsc = false; if (colKey === 'name' && (userSortBy === 'name_asc' || userSortBy === 'name_desc')) { isActive = true; isAsc = userSortBy === 'name_asc'; } if (colKey === 'plan' && userSortBy === 'plan_desc') { isActive = true; isAsc = false; // we only have plan_desc } if (colKey === 'last_active' && (userSortBy === 'last_active_desc' || userSortBy === 'last_active_asc')) { isActive = true; isAsc = userSortBy === 'last_active_asc'; } if (colKey === 'join' && (userSortBy === 'join_desc' || userSortBy === 'join_asc')) { isActive = true; isAsc = userSortBy === 'join_asc'; } if (!isActive) return null; return ( ); }; const handleResetFilters = () => { setUserSearchQuery(''); setUserRoleFilter('all'); setUserPlanFilter('all'); setUserStatusFilter('all'); setUserSortBy('last_active_desc'); setUserPage(1); }; const handleExportCSV = () => { const headers = ['ID', 'Name', 'Phone', 'Display ID', 'Role', 'Plan', 'Verification Status', 'Last Activity', 'Join Date']; const rows = sortedUsers.map(u => [ u.id, `"${(u.name || '').replace(/"/g, '""')}"`, `"${u.phone}"`, u.displayId || '', u.role, u.plan, u.status, `"${formatDateTime(u.lastActiveAt)}"`, u.date ]); const csvContent = 'data:text/csv;charset=utf-8,' + [headers.join(','), ...rows.map(r => r.join(','))].join('\n'); const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); link.setAttribute('href', encodedUri); link.setAttribute('download', `huio_members_${new Date().toISOString().slice(0, 10)}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; return (

{t('users.title')}

{t('users.sub')}

{/* Metrics Overview */}
{/* Controls Bar: Search, Filters, Sorting, Export */}
{/* Search Bar */}
{ setUserSearchQuery(e.target.value); setUserPage(1); }} className="hu-input" style={{ width: '100%', paddingLeft: '32px', fontSize: '13px', height: '36px' }} /> 🔍 {userSearchQuery && ( )}
{/* Filters & Sorting Controls */}
{/* Role filter */} {/* Plan filter */} {/* Verification filter */} {/* Sorting dropdown */} {/* Items per page */} {/* Export CSV Button */}
{/* Users Registry Table */}
{paginatedUsers.length === 0 ? ( ) : ( paginatedUsers.map(u => { const diffMs = Date.now() - new Date(u.lastActiveAt).getTime(); const isRecent24h = diffMs < 24 * 3600 * 1000; const isRecent7d = diffMs < 7 * 86400 * 1000; return ( {/* Full Name & Avatar & ID */} {/* Phone Number */} {/* Role Select */} {/* Plan Select */} {/* Status Badge */} {/* Last Activity Column (Relative Time + Date/Time Tooltip + Status Dot) */} {/* Join Date */} {/* Actions */} ); }) )}
handleHeaderSort('name')} style={{ ...appStyles.tableTh, cursor: 'pointer', userSelect: 'none' }} title="Click to sort by Name" > {t('users.col.name')}{renderSortArrow('name')} {t('users.col.phone')} {t('users.col.role')} handleHeaderSort('plan')} style={{ ...appStyles.tableTh, cursor: 'pointer', userSelect: 'none' }} title="Click to sort by Plan Tier" > {t('users.col.plan')}{renderSortArrow('plan')} {t('users.col.status')} handleHeaderSort('last_active')} style={{ ...appStyles.tableTh, cursor: 'pointer', userSelect: 'none' }} title="Click to sort by Last Activity" > {lang === 'vi' ? 'Hoạt động gần nhất' : lang === 'ru' ? 'Последняя активность' : 'Last Activity'}{renderSortArrow('last_active')} handleHeaderSort('join')} style={{ ...appStyles.tableTh, cursor: 'pointer', userSelect: 'none' }} title="Click to sort by Join Date" > {t('users.col.date')}{renderSortArrow('join')} {t('users.col.actions')}
{lang === 'vi' ? 'Không tìm thấy thành viên nào' : lang === 'ru' ? 'Участники не найдены' : 'No matching members found'}
{lang === 'vi' ? 'Thử thay đổi từ khóa tìm kiếm hoặc điều chỉnh bộ lọc để xem kết quả.' : lang === 'ru' ? 'Попробуйте изменить поисковый запрос или сбросить активные фильтры.' : 'Try changing your search terms or clearing active filters.'}
{u.avatarUrl && u.avatarUrl.startsWith('http') ? ( {u.name} ) : (
{getInitials(u.name)}
)}
{u.name} {u.suspendedAt && ( SUSPENDED )} {u.status === 'verified' && !u.suspendedAt && ( ✓ )}
{u.displayId && ( {u.displayId} )}
{formatPhone(u.phone)} {t(`status.${u.status}`).toUpperCase()}
{formatRelativeTime(u.lastActiveAt, lang)} {formatDateTime(u.lastActiveAt)}
{u.date}
{/* Pagination Controls */} {userTotalPages > 1 && (
{lang === 'vi' ? `Hiển thị ${(safeUserPage - 1) * userPerPage + 1} đến ${Math.min(safeUserPage * userPerPage, sortedUsers.length)} trong số ${sortedUsers.length} thành viên` : lang === 'ru' ? `Показаны с ${(safeUserPage - 1) * userPerPage + 1} по ${Math.min(safeUserPage * userPerPage, sortedUsers.length)} из ${sortedUsers.length} участников` : `Showing ${(safeUserPage - 1) * userPerPage + 1} to ${Math.min(safeUserPage * userPerPage, sortedUsers.length)} of ${sortedUsers.length} members` }
{safeUserPage} / {userTotalPages}
)}
); } window.CpUsersView = { renderUsers, };