Add search, filter, and sort to Admin Panel user management
Same UX improvements as the modal — search bar, group filter chips, sortable column headers, and filtered count. Applied to the in-page UserManagementPanel in AdminPage.js so the Admin Panel tab has parity with the modal.
This commit is contained in:
@@ -117,6 +117,10 @@ function UserManagementPanel() {
|
||||
const [formError, setFormError] = useState('');
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
const [pendingConfirm, setPendingConfirm] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [groupFilter, setGroupFilter] = useState('');
|
||||
const [sortField, setSortField] = useState('username');
|
||||
const [sortDir, setSortDir] = useState('asc');
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -148,6 +152,43 @@ function UserManagementPanel() {
|
||||
}
|
||||
}, [successMessage]);
|
||||
|
||||
// Filtered and sorted user list
|
||||
const filteredUsers = users
|
||||
.filter(u => {
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
const matchesName = u.username?.toLowerCase().includes(term);
|
||||
const matchesEmail = u.email?.toLowerCase().includes(term);
|
||||
const matchesTeam = (u.teams || []).some(t => t.toLowerCase().includes(term));
|
||||
if (!matchesName && !matchesEmail && !matchesTeam) return false;
|
||||
}
|
||||
if (groupFilter === 'no-teams') return !u.teams || u.teams.length === 0;
|
||||
if (groupFilter && u.group !== groupFilter) return false;
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
let aVal, bVal;
|
||||
switch (sortField) {
|
||||
case 'username': aVal = a.username || ''; bVal = b.username || ''; break;
|
||||
case 'email': aVal = a.email || ''; bVal = b.email || ''; break;
|
||||
case 'group': aVal = a.group || ''; bVal = b.group || ''; break;
|
||||
case 'active': aVal = a.is_active ? 'a' : 'z'; bVal = b.is_active ? 'a' : 'z'; break;
|
||||
case 'last_login': aVal = a.last_login || ''; bVal = b.last_login || ''; break;
|
||||
default: aVal = a.username || ''; bVal = b.username || '';
|
||||
}
|
||||
const cmp = String(aVal).localeCompare(String(bVal));
|
||||
return sortDir === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
|
||||
const handleSort = (field) => {
|
||||
if (sortField === field) {
|
||||
setSortDir(d => d === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDir('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddClick = () => {
|
||||
setEditingUser(null);
|
||||
setFormData({ username: '', email: '', password: '', group: 'Read_Only' });
|
||||
@@ -433,6 +474,48 @@ function UserManagementPanel() {
|
||||
|
||||
{/* User table */}
|
||||
{!loading && !error && (
|
||||
<div>
|
||||
{/* Search and filter controls */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.625rem', marginBottom: '0.75rem' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Search style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', width: '14px', height: '14px', color: '#475569' }} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name, email, or team..."
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
style={{
|
||||
width: '100%', padding: '0.5rem 0.75rem 0.5rem 2.25rem',
|
||||
background: 'rgba(15, 23, 42, 0.6)',
|
||||
border: '1px solid rgba(14, 165, 233, 0.2)',
|
||||
borderRadius: '0.375rem', color: '#E2E8F0',
|
||||
fontSize: '0.8rem', fontFamily: 'monospace', outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.3rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.65rem', color: '#64748B', marginRight: '0.25rem' }}>Filter:</span>
|
||||
{['', 'Admin', 'Standard_User', 'Leadership', 'Read_Only', 'no-teams'].map(g => (
|
||||
<button
|
||||
key={g}
|
||||
onClick={() => setGroupFilter(g)}
|
||||
style={{
|
||||
padding: '0.15rem 0.5rem', borderRadius: '0.25rem',
|
||||
fontSize: '0.65rem', fontFamily: 'monospace', fontWeight: 600, cursor: 'pointer',
|
||||
border: groupFilter === g ? '1px solid #0EA5E9' : '1px solid rgba(14,165,233,0.2)',
|
||||
background: groupFilter === g ? 'rgba(14,165,233,0.15)' : 'transparent',
|
||||
color: groupFilter === g ? '#0EA5E9' : '#94A3B8',
|
||||
}}
|
||||
>
|
||||
{g === '' ? 'All' : g === 'no-teams' ? 'No Teams' : g.replace('_', ' ')}
|
||||
</button>
|
||||
))}
|
||||
<span style={{ marginLeft: 'auto', fontSize: '0.65rem', color: '#64748B' }}>
|
||||
{filteredUsers.length} of {users.length} users
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: '1px solid rgba(14,165,233,0.15)',
|
||||
@@ -448,21 +531,21 @@ function UserManagementPanel() {
|
||||
fontSize: '0.62rem', color: '#334155',
|
||||
fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
}}>
|
||||
<span>Username</span>
|
||||
<span>Email</span>
|
||||
<span>Group</span>
|
||||
<span>Active</span>
|
||||
<span>Last Login</span>
|
||||
<span style={{ cursor: 'pointer' }} onClick={() => handleSort('username')}>Username {sortField === 'username' ? (sortDir === 'asc' ? '▲' : '▼') : ''}</span>
|
||||
<span style={{ cursor: 'pointer' }} onClick={() => handleSort('email')}>Email {sortField === 'email' ? (sortDir === 'asc' ? '▲' : '▼') : ''}</span>
|
||||
<span style={{ cursor: 'pointer' }} onClick={() => handleSort('group')}>Group {sortField === 'group' ? (sortDir === 'asc' ? '▲' : '▼') : ''}</span>
|
||||
<span style={{ cursor: 'pointer' }} onClick={() => handleSort('active')}>Active {sortField === 'active' ? (sortDir === 'asc' ? '▲' : '▼') : ''}</span>
|
||||
<span style={{ cursor: 'pointer' }} onClick={() => handleSort('last_login')}>Last Login {sortField === 'last_login' ? (sortDir === 'asc' ? '▲' : '▼') : ''}</span>
|
||||
<span style={{ textAlign: 'right' }}>Actions</span>
|
||||
</div>
|
||||
|
||||
{/* Rows */}
|
||||
{users.length === 0 ? (
|
||||
{filteredUsers.length === 0 ? (
|
||||
<div style={{ padding: '2rem', textAlign: 'center', color: '#334155', fontFamily: 'monospace', fontSize: '0.8rem' }}>
|
||||
No users found
|
||||
</div>
|
||||
) : (
|
||||
users.map(u => {
|
||||
filteredUsers.map(u => {
|
||||
const badge = getGroupBadgeStyle(u.group);
|
||||
const self = isSelfUser(currentUser?.id, u.id);
|
||||
return (
|
||||
@@ -575,6 +658,7 @@ function UserManagementPanel() {
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
|
||||
Reference in New Issue
Block a user