better ux with search functionality

This commit is contained in:
2026-05-28 10:58:31 +02:00
parent 8915c8d182
commit b049a9ab9f
10 changed files with 997 additions and 252 deletions

View File

@ -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'