better ux with search functionality
This commit is contained in:
@ -11,9 +11,35 @@ const CREATEABLE_ROLES = {
|
||||
let usersList = [];
|
||||
let supervisorsList = [];
|
||||
let callerRole = '';
|
||||
let filterSearch = '';
|
||||
/** @type {Record<string, string>} */
|
||||
let filterSearchByRole = { admin: '', supervisor: '', coach: '' };
|
||||
let filterTestData = '';
|
||||
|
||||
function roleGroupsForCaller() {
|
||||
return callerRole === 'supervisor'
|
||||
? [{ label: 'Coaches', key: 'coach' }]
|
||||
: [
|
||||
{ label: 'Admins', key: 'admin' },
|
||||
{ label: 'Supervisors', key: 'supervisor' },
|
||||
{ label: 'Coaches', key: 'coach' },
|
||||
];
|
||||
}
|
||||
|
||||
function searchPlaceholderForRole(roleKey) {
|
||||
return roleKey === 'coach'
|
||||
? 'Search username or supervisor…'
|
||||
: 'Search username or location…';
|
||||
}
|
||||
|
||||
function usersByRoleMap() {
|
||||
const byRole = { admin: [], supervisor: [], coach: [] };
|
||||
usersList.forEach(u => {
|
||||
if (byRole[u.role]) byRole[u.role].push(u);
|
||||
else (byRole.other = byRole.other || []).push(u);
|
||||
});
|
||||
return byRole;
|
||||
}
|
||||
|
||||
export async function usersPage() {
|
||||
callerRole = getRole();
|
||||
const app = document.getElementById('app');
|
||||
@ -76,36 +102,34 @@ function renderUsers() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by role for a cleaner display
|
||||
const byRole = { admin: [], supervisor: [], coach: [] };
|
||||
usersList.forEach(u => (byRole[u.role] || (byRole.other = byRole.other || [])).push(u));
|
||||
const byRole = usersByRoleMap();
|
||||
const groups = roleGroupsForCaller();
|
||||
|
||||
// Which role groups to show
|
||||
const groups = callerRole === 'supervisor'
|
||||
? [{ label: 'Coaches', key: 'coach' }]
|
||||
: [
|
||||
{ label: 'Admins', key: 'admin' },
|
||||
{ label: 'Supervisors', key: 'supervisor' },
|
||||
{ label: 'Coaches', key: 'coach' },
|
||||
];
|
||||
|
||||
container.innerHTML = `
|
||||
const testFilterCard = `
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="filter-bar">
|
||||
<input type="search" id="userListSearch" class="filter-search" placeholder="Search username…" value="${esc(filterSearch)}">
|
||||
<label style="font-size:.85rem;font-weight:500">Show:</label>
|
||||
<select id="userTestFilter">
|
||||
<option value="">All users</option>
|
||||
<option value="test" ${filterTestData === 'test' ? 'selected' : ''}>Test data only (dev_*)</option>
|
||||
<option value="hide-test" ${filterTestData === 'hide-test' ? 'selected' : ''}>Hide test data</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
` + groups.map(group => {
|
||||
const users = filterUsers(byRole[group.key] || []);
|
||||
if (!users.length) return '';
|
||||
</div>`;
|
||||
|
||||
container.innerHTML = testFilterCard + groups.map(group => {
|
||||
const allInRole = byRole[group.key] || [];
|
||||
if (!allInRole.length) return '';
|
||||
return `
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3 style="margin-bottom:12px">${group.label} (${users.length})</h3>
|
||||
<div class="card user-role-card" data-role="${group.key}" style="margin-bottom:16px">
|
||||
<h3 class="user-role-title" style="margin-bottom:12px">
|
||||
${group.label} (<span data-role-count="${group.key}">0</span>)
|
||||
</h3>
|
||||
<div class="filter-bar">
|
||||
<input type="search" class="filter-search user-role-search" data-role="${group.key}"
|
||||
placeholder="${searchPlaceholderForRole(group.key)}"
|
||||
value="${esc(filterSearchByRole[group.key] || '')}">
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
@ -117,33 +141,41 @@ function renderUsers() {
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${users.map(u => userRowHTML(u, myUsername)).join('')}
|
||||
</tbody>
|
||||
<tbody id="usersBody-${group.key}"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('userListSearch')?.addEventListener('input', (e) => {
|
||||
filterSearch = e.target.value.trim();
|
||||
renderUsers();
|
||||
});
|
||||
document.getElementById('userTestFilter')?.addEventListener('change', (e) => {
|
||||
filterTestData = e.target.value;
|
||||
renderUsers();
|
||||
});
|
||||
|
||||
container.querySelectorAll('.delete-user-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => deleteUser(btn.dataset.id, btn.dataset.name));
|
||||
container.addEventListener('input', (e) => {
|
||||
const input = e.target.closest('.user-role-search');
|
||||
if (!input) return;
|
||||
const roleKey = input.dataset.role;
|
||||
if (!roleKey) return;
|
||||
filterSearchByRole[roleKey] = input.value;
|
||||
updateRoleGroupTable(roleKey, myUsername);
|
||||
});
|
||||
|
||||
groups.forEach(group => {
|
||||
if ((byRole[group.key] || []).length) {
|
||||
updateRoleGroupTable(group.key, myUsername);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function filterUsers(users) {
|
||||
function filterUsers(users, roleKey) {
|
||||
let list = users;
|
||||
if (filterSearch) {
|
||||
const q = filterSearch.toLowerCase();
|
||||
list = list.filter(u => (u.username || '').toLowerCase().includes(q));
|
||||
const q = (filterSearchByRole[roleKey] || '').trim().toLowerCase();
|
||||
if (q) {
|
||||
list = list.filter(u => {
|
||||
const hay = [u.username, u.location, u.supervisorUsername].filter(Boolean).join(' ').toLowerCase();
|
||||
return hay.includes(q);
|
||||
});
|
||||
}
|
||||
if (filterTestData === 'test') {
|
||||
list = list.filter(u => isDevTestUsername(u.username));
|
||||
@ -153,6 +185,39 @@ function filterUsers(users) {
|
||||
return list;
|
||||
}
|
||||
|
||||
function updateRoleGroupTable(roleKey, myUsername) {
|
||||
const tbody = document.getElementById(`usersBody-${roleKey}`);
|
||||
const countEl = document.querySelector(`[data-role-count="${roleKey}"]`);
|
||||
if (!tbody) return;
|
||||
|
||||
const byRole = usersByRoleMap();
|
||||
const allInRole = byRole[roleKey] || [];
|
||||
const users = filterUsers(allInRole, roleKey);
|
||||
const colSpan = 5;
|
||||
|
||||
if (countEl) {
|
||||
const q = (filterSearchByRole[roleKey] || '').trim();
|
||||
countEl.textContent = q && users.length !== allInRole.length
|
||||
? `${users.length} of ${allInRole.length}`
|
||||
: String(users.length);
|
||||
}
|
||||
|
||||
if (!users.length) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="${colSpan}" style="text-align:center;color:var(--text-secondary);padding:24px">
|
||||
${allInRole.length ? 'No users match' : 'No users'}
|
||||
</td>
|
||||
</tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = users.map(u => userRowHTML(u, myUsername)).join('');
|
||||
tbody.querySelectorAll('.delete-user-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => deleteUser(btn.dataset.id, btn.dataset.name));
|
||||
});
|
||||
}
|
||||
|
||||
function userRowHTML(u, myUsername) {
|
||||
const isSelf = u.username === myUsername;
|
||||
const detail = u.role === 'coach'
|
||||
|
||||
Reference in New Issue
Block a user