improved scoring ui + removed test data filters
Some checks failed
PHPUnit / test (push) Has been cancelled

This commit is contained in:
2026-06-08 19:38:37 +02:00
parent 11bae269e9
commit 3211d35b43
6 changed files with 317 additions and 200 deletions

View File

@ -31,8 +31,6 @@
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--table-header-bg: #1e2430;
--row-hover-bg: #252c3a;
--test-row-bg: #2a2618;
--test-row-hover-bg: #3a321f;
--sticky-shadow: rgba(0, 0, 0, 0.35);
/* Incomplete translation highlight */
--trans-missing-bg: #1c222d;
@ -686,18 +684,6 @@ table.data-table th {
table.data-table tr:hover td {
background: var(--row-hover-bg);
}
table.data-table tbody tr.row-test-data td {
background: var(--test-row-bg);
}
table.data-table tbody tr.row-test-data:hover td {
background: var(--test-row-hover-bg);
}
#qdb-table tbody tr.row-test-data td {
background: var(--test-row-bg);
}
#qdb-table tbody tr.row-test-data:hover td {
background: var(--test-row-hover-bg);
}
table.data-table .sticky-col {
position: sticky;
left: 0;
@ -710,12 +696,6 @@ table.data-table thead th.sticky-col {
top: 0;
background: var(--table-header-bg);
}
table.data-table tbody tr.row-test-data td.sticky-col {
background: var(--test-row-bg);
}
table.data-table tbody tr.row-test-data:hover td.sticky-col {
background: var(--test-row-hover-bg);
}
th.sortable { cursor: pointer; user-select: none; }
th.sortable::after { content: ' \2195'; opacity: .4; }
th.sort-asc::after { content: ' \2191'; opacity: 1; }
@ -1250,18 +1230,6 @@ table.data-table tbody tr.row-orphan-category td {
border-color: var(--primary);
}
.coach-list-item.row-test-data {
background: var(--test-row-bg);
}
.coach-list-item.row-test-data:hover {
background: var(--test-row-hover-bg);
}
.coach-list-item.row-test-data.active {
background: var(--primary-subtle);
}
.coach-list-empty {
padding: 20px 12px;
text-align: center;
@ -3034,19 +3002,133 @@ select:disabled {
letter-spacing: .04em;
color: #ca8a04;
}
.client-coach-band-select {
min-width: 6.5rem;
/* Coach category picker (clients list) */
.coach-band-picker {
position: relative;
display: inline-flex;
max-width: 100%;
padding: 2px 6px;
font-size: .75rem;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--surface, #fff);
color: var(--text);
}
.client-coach-band-select:disabled {
opacity: 0.65;
cursor: wait;
.coach-band-trigger {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px 2px 4px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface);
color: inherit;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
max-width: 100%;
}
.coach-band-trigger:hover {
border-color: color-mix(in srgb, var(--primary) 45%, var(--border));
background: var(--surface-hover, rgba(0, 0, 0, 0.03));
}
.coach-band-picker.is-open .coach-band-trigger,
.coach-band-trigger:focus-visible {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 22%, transparent);
}
.coach-band-trigger-badge .band-badge {
margin: 0;
}
.coach-band-trigger-caret {
font-size: .65rem;
color: var(--text-secondary);
line-height: 1;
flex-shrink: 0;
}
.coach-band-picker.is-saving .coach-band-trigger {
opacity: 0.6;
pointer-events: none;
}
.coach-band-menu {
min-width: 11.5rem;
padding: 10px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--surface);
box-shadow: var(--shadow-lg);
}
.coach-band-menu-label {
margin: 0 0 8px;
font-size: .7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--text-secondary);
}
.coach-band-menu-options {
display: flex;
flex-direction: column;
gap: 4px;
}
.coach-band-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
color: inherit;
cursor: pointer;
text-align: left;
transition: background 0.12s, border-color 0.12s;
}
.coach-band-option:hover {
background: var(--surface-hover, rgba(0, 0, 0, 0.04));
}
.coach-band-option.is-selected {
border-color: color-mix(in srgb, var(--primary) 35%, var(--border));
background: color-mix(in srgb, var(--primary) 8%, var(--surface));
}
.coach-band-option-agree {
margin-top: 6px;
padding-top: 10px;
border-top: 1px solid var(--border);
border-radius: 0 0 8px 8px;
}
.coach-band-option-agree-label {
font-size: .75rem;
font-weight: 600;
color: var(--text-secondary);
}
.coach-band-confirm-compare {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 12px 16px;
margin: 8px 0 4px;
padding: 16px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--surface-elevated, rgba(0, 0, 0, 0.02));
}
.coach-band-confirm-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
min-width: 5.5rem;
}
.coach-band-confirm-label {
font-size: .7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--text-secondary);
}
.coach-band-confirm-arrow {
font-size: 1.25rem;
color: var(--text-secondary);
line-height: 1;
}
.client-profile-score-hint,
.client-profile-score-meta {

View File

@ -1,6 +1,5 @@
import { apiGet, apiPost } from '../api.js';
import { pageHeaderHTML, showToast } from '../app.js';
import { isDevTestClientCode, isDevTestUsername, testDataRowClassForClient } from '../test-data.js';
const CLIENT_PAGE_SIZE = 50;
@ -100,11 +99,6 @@ function renderAssignments() {
</select>
<input type="search" id="clientSearch" class="filter-search"
placeholder="Search client or coach…">
<select id="clientTestFilter" aria-label="Test data filter">
<option value="">All clients</option>
<option value="test">Test only</option>
<option value="hide-test">Hide test</option>
</select>
</div>
</div>
<div class="assign-table-panel">
@ -178,10 +172,6 @@ function renderAssignments() {
clientPage = 0;
renderClientRows();
});
document.getElementById('clientTestFilter').addEventListener('change', () => {
clientPage = 0;
renderClientRows();
});
document.getElementById('selectAllBtn').addEventListener('click', () => {
document.querySelectorAll('#clientTableBody .client-cb').forEach(cb => {
@ -313,10 +303,9 @@ function renderCoachList() {
function coachListItemHTML(c) {
const count = (clientsByCoach[c.coachID] || []).length;
const coachTest = isDevTestUsername(c.username) ? ' row-test-data' : '';
const active = selectedCoachID === c.coachID ? ' active' : '';
return `
<li class="coach-list-item${coachTest}${active}" data-id="${c.coachID}" role="option" tabindex="0">
<li class="coach-list-item${active}" data-id="${c.coachID}" role="option" tabindex="0">
<div class="coach-name">${esc(c.username)}</div>
${c.supervisorUsername
? `<div class="coach-supervisor">${esc(c.supervisorUsername)}</div>`
@ -336,8 +325,6 @@ function highlightCoachInList(coachID) {
function getVisibleClients() {
const coachFilter = document.getElementById('filterByCoach')?.value ?? '';
const search = document.getElementById('clientSearch')?.value.trim().toLowerCase() ?? '';
const testMode = document.getElementById('clientTestFilter')?.value || '';
let visible = clients;
if (coachFilter === '__unassigned__') {
visible = clients.filter(c => !c.coachID);
@ -350,11 +337,6 @@ function getVisibleClients() {
return hay.includes(search);
});
}
if (testMode === 'test') {
visible = visible.filter(c => isDevTestClientCode(c.clientCode));
} else if (testMode === 'hide-test') {
visible = visible.filter(c => !isDevTestClientCode(c.clientCode));
}
return visible;
}
@ -363,8 +345,7 @@ function updateClientSummary(filteredCount, pageSlice) {
if (!el) return;
const search = document.getElementById('clientSearch')?.value.trim() ?? '';
const coachFilter = document.getElementById('filterByCoach')?.value ?? '';
const testFilter = document.getElementById('clientTestFilter')?.value;
const hasFilter = search || coachFilter || testFilter;
const hasFilter = search || coachFilter;
const parts = [];
if (hasFilter && filteredCount !== clients.length) {
parts.push(`${filteredCount} of ${clients.length} match`);
@ -453,7 +434,7 @@ function renderClientRows() {
body.innerHTML = slice.map(c => {
const checked = selectedClientCodes.has(c.clientCode) ? ' checked' : '';
return `
<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
<tr>
<td>
<input type="checkbox" class="client-cb" data-code="${esc(c.clientCode)}" value="${esc(c.clientCode)}"${checked} aria-label="Select ${esc(c.clientCode)}">
</td>

View File

@ -1,6 +1,5 @@
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
import { pageHeaderHTML, showToast } from '../app.js';
import { isDevTestClientCode, testDataRowClassForClient } from '../test-data.js';
const PAGE_SIZE = 50;
@ -8,7 +7,6 @@ let clientsList = [];
let coachesList = [];
let scoringProfileCatalog = [];
let filterSearch = '';
let filterTestData = '';
let page = 0;
let expandedClientCode = null;
let expandedQnKey = null;
@ -74,11 +72,6 @@ function renderClients() {
<div class="card">
<div class="filter-bar">
<input type="search" id="clientListSearch" class="filter-search" placeholder="Search client or coach…" value="${esc(filterSearch)}">
<select id="clientTestFilter">
<option value="">All clients</option>
<option value="test" ${filterTestData === 'test' ? 'selected' : ''}>Test data only</option>
<option value="hide-test" ${filterTestData === 'hide-test' ? 'selected' : ''}>Hide test data</option>
</select>
<span class="data-toolbar-hint" id="clientListCount"></span>
</div>
${profileLegend}
@ -107,13 +100,7 @@ function renderClients() {
clearExpandIfHidden();
renderClientTableBody();
});
document.getElementById('clientTestFilter').addEventListener('change', (e) => {
filterTestData = e.target.value;
page = 0;
clearExpandIfHidden();
renderClientTableBody();
});
ensureCoachBandGlobalClick();
renderClientTableBody();
}
@ -126,11 +113,6 @@ function getFilteredClientsList() {
return hay.includes(q);
});
}
if (filterTestData === 'test') {
list = list.filter(c => isDevTestClientCode(c.clientCode));
} else if (filterTestData === 'hide-test') {
list = list.filter(c => !isDevTestClientCode(c.clientCode));
}
return list;
}
@ -167,12 +149,7 @@ function renderClientTableBody() {
body.querySelectorAll('.delete-client-btn').forEach(btn => {
btn.addEventListener('click', () => deleteClient(btn.dataset.code));
});
body.querySelectorAll('.client-coach-band-select').forEach(select => {
select.addEventListener('focus', () => {
select.dataset.prev = select.value;
});
select.addEventListener('change', () => onCoachBandChange(select));
});
bindCoachBandPickerEvents(body);
}
const pag = document.getElementById('clientsPagination');
@ -309,29 +286,47 @@ function bandLabel(band) {
return band.charAt(0).toUpperCase() + band.slice(1);
}
function coachBandSelectHTML(p, clientCode) {
function coachBandPickerHTML(p, clientCode) {
const calcBand = p.calculatedBand || p.band;
if (!calcBand || !clientCode || !p.profileID) {
return '';
}
const current = p.coachBand || '';
const coachPending = p.pendingReview !== false && !current;
const placeholder = coachPending
? '<option value="" selected disabled>Awaiting review</option>'
: '';
const options = COACH_BANDS.map(b => {
const selected = current === b ? ' selected' : '';
return `<option value="${esc(b)}"${selected}>${esc(bandLabel(b))}</option>`;
}).join('');
const triggerBadge = coachPending
? bandBadgeHTML(null, { pending: true })
: bandBadgeHTML(current);
const showAgree = coachPending || (current && current !== calcBand);
const options = COACH_BANDS.map(b => `
<button type="button" class="coach-band-option${current === b ? ' is-selected' : ''}"
data-band="${esc(b)}" role="option" aria-selected="${current === b}">
${bandBadgeHTML(b, { label: bandLabel(b) })}
</button>`).join('');
const agreeBtn = showAgree ? `
<button type="button" class="coach-band-option coach-band-option-agree${current === calcBand ? ' is-selected' : ''}"
data-band="${esc(calcBand)}" role="option">
<span class="coach-band-option-agree-label">Match calculated</span>
${bandBadgeHTML(calcBand, { label: bandLabel(calcBand) })}
</button>` : '';
return `
<select class="client-coach-band-select"
<div class="coach-band-picker"
data-client="${esc(clientCode)}"
data-profile="${esc(p.profileID)}"
data-profile-name="${esc(p.name || p.profileID)}"
data-calculated="${esc(calcBand)}"
data-weighted="${esc(String(p.weightedTotal ?? ''))}"
data-prev="${esc(current)}"
title="Set coach category">${placeholder}${options}</select>`;
data-coach="${esc(current)}">
<button type="button" class="coach-band-trigger" aria-haspopup="listbox" aria-expanded="false"
title="Change coach category">
<span class="coach-band-trigger-badge">${triggerBadge}</span>
<span class="coach-band-trigger-caret" aria-hidden="true">▾</span>
</button>
<div class="coach-band-menu" role="listbox" hidden>
<p class="coach-band-menu-label">Coach category</p>
<div class="coach-band-menu-options">${options}</div>
${agreeBtn}
</div>
</div>`;
}
function profileScoreBlockHTML(p, clientCode = '') {
@ -346,7 +341,7 @@ function profileScoreBlockHTML(p, clientCode = '') {
const coachPending = p.pendingReview !== false && !p.coachBand;
const coachDiffers = p.coachBand && p.coachBand !== calcBand;
const coachControl = clientCode
? coachBandSelectHTML(p, clientCode)
? coachBandPickerHTML(p, clientCode)
: (coachPending ? bandBadgeHTML(null, { pending: true }) : bandBadgeHTML(p.coachBand));
return `
<div class="client-profile-score-block${coachDiffers ? ' has-coach-override' : ''}" data-profile-id="${esc(p.profileID || '')}">
@ -484,31 +479,130 @@ function patchClientScoringBand(clientCode, profileID, coachBand) {
}
}
async function onCoachBandChange(select) {
const clientCode = select.dataset.client;
const profileID = select.dataset.profile;
const profileName = select.dataset.profileName || profileID;
const calculatedBand = select.dataset.calculated;
const weightedRaw = select.dataset.weighted;
function resetCoachBandMenuPosition(menu) {
if (!menu) return;
menu.style.position = '';
menu.style.left = '';
menu.style.top = '';
menu.style.minWidth = '';
menu.style.zIndex = '';
menu.style.visibility = '';
}
function positionCoachBandMenu(picker) {
const trigger = picker.querySelector('.coach-band-trigger');
const menu = picker.querySelector('.coach-band-menu');
if (!trigger || !menu) return;
menu.hidden = false;
menu.style.visibility = 'hidden';
const rect = trigger.getBoundingClientRect();
const menuWidth = Math.max(rect.width, 184);
const left = Math.min(rect.left, window.innerWidth - menuWidth - 8);
menu.style.position = 'fixed';
menu.style.left = `${Math.max(8, left)}px`;
menu.style.minWidth = `${menuWidth}px`;
menu.style.zIndex = '10001';
const menuHeight = menu.offsetHeight;
let top = rect.bottom + 6;
if (top + menuHeight > window.innerHeight - 8) {
top = rect.top - menuHeight - 6;
}
menu.style.top = `${Math.max(8, top)}px`;
menu.style.visibility = '';
}
function closeAllCoachBandMenus() {
document.querySelectorAll('.coach-band-picker.is-open').forEach(picker => {
picker.classList.remove('is-open');
const menu = picker.querySelector('.coach-band-menu');
const trigger = picker.querySelector('.coach-band-trigger');
resetCoachBandMenuPosition(menu);
if (menu) menu.hidden = true;
if (trigger) trigger.setAttribute('aria-expanded', 'false');
});
}
function toggleCoachBandMenu(picker) {
const isOpen = picker.classList.contains('is-open');
closeAllCoachBandMenus();
if (isOpen) return;
picker.classList.add('is-open');
const trigger = picker.querySelector('.coach-band-trigger');
positionCoachBandMenu(picker);
if (trigger) trigger.setAttribute('aria-expanded', 'true');
}
function openCoachBandConfirmModal({ clientCode, profileName, calculatedBand, coachBand }) {
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay coach-band-confirm-modal';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.innerHTML = `
<div class="modal-dialog">
<h2>Set coach category</h2>
<p class="modal-subtitle">
<strong>${esc(profileName)}</strong> · ${esc(clientCode)}
</p>
<div class="coach-band-confirm-compare">
<div class="coach-band-confirm-col">
<span class="coach-band-confirm-label">Calculated</span>
${bandBadgeHTML(calculatedBand, { label: bandLabel(calculatedBand) })}
</div>
<div class="coach-band-confirm-arrow" aria-hidden="true">→</div>
<div class="coach-band-confirm-col">
<span class="coach-band-confirm-label">Coach</span>
${bandBadgeHTML(coachBand, { label: bandLabel(coachBand) })}
</div>
</div>
<p class="modal-hint">This overrides the coach category for this client and profile.</p>
<div class="modal-actions">
<button type="button" class="btn" data-coach-cancel>Cancel</button>
<button type="button" class="btn btn-primary" data-coach-confirm>Save category</button>
</div>
</div>`;
document.body.appendChild(overlay);
document.body.classList.add('modal-open');
const close = (confirmed) => {
overlay.remove();
document.body.classList.remove('modal-open');
resolve(confirmed);
};
overlay.querySelector('[data-coach-cancel]').addEventListener('click', () => close(false));
overlay.querySelector('[data-coach-confirm]').addEventListener('click', () => close(true));
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close(false);
});
overlay.querySelector('.modal-dialog').addEventListener('click', (e) => e.stopPropagation());
const onKey = (e) => {
if (e.key === 'Escape') close(false);
};
document.addEventListener('keydown', onKey, { once: true });
overlay.querySelector('[data-coach-confirm]').focus();
});
}
async function promptCoachBandSave(picker, coachBand) {
const clientCode = picker.dataset.client;
const profileID = picker.dataset.profile;
const profileName = picker.dataset.profileName || profileID;
const calculatedBand = picker.dataset.calculated;
const current = picker.dataset.coach || '';
if (!coachBand || coachBand === current) return;
const confirmed = await openCoachBandConfirmModal({
clientCode,
profileName,
calculatedBand,
coachBand,
});
if (!confirmed) return;
const weightedRaw = picker.dataset.weighted;
const weightedTotal = weightedRaw !== '' ? Number(weightedRaw) : null;
const prev = select.dataset.prev || '';
const coachBand = select.value;
if (!coachBand) {
select.value = prev;
return;
}
if (coachBand === prev) {
return;
}
const msg = `Set coach category to "${bandLabel(coachBand)}" for ${profileName} (${clientCode})?\n\nCalculated category: ${bandLabel(calculatedBand)}.`;
if (!confirm(msg)) {
select.value = prev || '';
return;
}
select.disabled = true;
picker.classList.add('is-saving');
try {
await apiPut('clients.php', {
clientCode,
@ -518,17 +612,42 @@ async function onCoachBandChange(select) {
weightedTotal,
});
patchClientScoringBand(clientCode, profileID, coachBand);
select.dataset.prev = coachBand;
showToast(`Coach category saved for ${profileName}`, 'success');
renderClientTableBody();
} catch (e) {
select.value = prev || '';
showToast(e.message, 'error');
} finally {
select.disabled = false;
picker.classList.remove('is-saving');
}
}
function bindCoachBandPickerEvents(root = document) {
root.querySelectorAll('.coach-band-trigger').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
toggleCoachBandMenu(btn.closest('.coach-band-picker'));
});
});
root.querySelectorAll('.coach-band-option').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const picker = btn.closest('.coach-band-picker');
closeAllCoachBandMenus();
promptCoachBandSave(picker, btn.dataset.band);
});
});
}
let coachBandGlobalClickBound = false;
function ensureCoachBandGlobalClick() {
if (coachBandGlobalClickBound) return;
coachBandGlobalClickBound = true;
document.addEventListener('click', () => closeAllCoachBandMenus());
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeAllCoachBandMenus();
});
}
function clientHasResponseData(c) {
return Number(c.hasResponseData) === 1;
}
@ -548,7 +667,7 @@ function clientRowHTML(c) {
const colCount = scoringProfileCatalog.length > 0 ? 4 : 3;
return `
<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
<tr>
<td>
${expandControl}<strong>${esc(c.clientCode)}</strong>
</td>

View File

@ -1,11 +1,6 @@
import { apiGet } from '../api.js';
import { homeNavButton, showToast } from '../app.js';
import {
buildResultColumns,
isDevTestClientCode,
resultColumnCellValue,
testDataRowClassForClient,
} from '../test-data.js';
import { buildResultColumns, resultColumnCellValue } from '../test-data.js';
const PAGE_SIZE = 50;
@ -21,7 +16,6 @@ let filterStatus = '';
let filterDateFrom = '';
let filterDateTo = '';
let filterSearch = '';
let filterTestData = '';
let filterQuestion = '';
let page = 0;
@ -35,7 +29,6 @@ export async function resultsPage(params) {
filterDateFrom = '';
filterDateTo = '';
filterSearch = '';
filterTestData = '';
filterQuestion = '';
page = 0;
@ -102,11 +95,6 @@ function renderResults() {
<input type="date" id="filterDateTo" value="${esc(filterDateTo)}">
</label>
<input type="search" id="filterSearch" class="filter-search" placeholder="Search client, coach, status…" value="${esc(filterSearch)}">
<select id="filterTestData">
<option value="">All clients</option>
<option value="test" ${filterTestData === 'test' ? 'selected' : ''}>Test data only</option>
<option value="hide-test" ${filterTestData === 'hide-test' ? 'selected' : ''}>Hide test data</option>
</select>
<input type="search" id="filterQuestion" placeholder="Go to question key…" value="${esc(filterQuestion)}" style="min-width:160px" list="questionKeyList">
<datalist id="questionKeyList"></datalist>
<button type="button" class="btn btn-sm" id="gotoQuestionBtn">Go to column</button>
@ -114,7 +102,7 @@ function renderResults() {
<span style="margin-left:auto;font-size:.85rem;color:var(--text-secondary)" id="resultCount"></span>
<a id="exportCsvLink" class="btn btn-sm" href="#">Export CSV</a>
</div>
<p class="data-toolbar-hint" style="margin-top:8px">Each question has its own column; glass-scale symptoms are separate columns. Hover headers for context. Test rows (DEV-CL-*) are highlighted.</p>
<p class="data-toolbar-hint" style="margin-top:8px">Each question has its own column; glass-scale symptoms are separate columns. Hover headers for context.</p>
</div>
<div class="card">
<div class="table-wrapper" id="resultsTableWrap">
@ -162,11 +150,6 @@ function renderResults() {
page = 0;
renderTableRows();
});
document.getElementById('filterTestData').addEventListener('change', (e) => {
filterTestData = e.target.value;
page = 0;
renderTableRows();
});
document.getElementById('gotoQuestionBtn').addEventListener('click', () => {
filterQuestion = document.getElementById('filterQuestion').value.trim();
scrollToQuestionColumn(filterQuestion);
@ -185,7 +168,6 @@ function renderResults() {
filterDateFrom = '';
filterDateTo = '';
filterSearch = '';
filterTestData = '';
filterQuestion = '';
page = 0;
renderResults();
@ -304,11 +286,6 @@ function getFilteredClients() {
return hay.includes(q);
});
}
if (filterTestData === 'test') {
clients = clients.filter(c => isDevTestClientCode(c.clientCode));
} else if (filterTestData === 'hide-test') {
clients = clients.filter(c => !isDevTestClientCode(c.clientCode));
}
return clients;
}
@ -404,7 +381,7 @@ function renderTableRows() {
return `<td>${esc(String(val))}</td>`;
}).join('');
return `<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
return `<tr>
<td class="sticky-col">${esc(c.clientCode)}</td>
<td>${esc(coachDisplayName(c))}</td>
<td>${esc(supervisorDisplayName(c))}</td>

View File

@ -1,7 +1,6 @@
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
import { getRole, getUser, pageHeaderHTML, showToast } from '../app.js';
import { openAdminResetPasswordModal } from '../password-modal.js';
import { isDevTestUsername, testDataRowClassForUser } from '../test-data.js';
// Roles an admin can create; supervisors can only create coaches
const CREATEABLE_ROLES = {
@ -14,7 +13,6 @@ let supervisorsList = [];
let callerRole = '';
/** @type {Record<string, string>} */
let filterSearchByRole = { admin: '', supervisor: '', coach: '' };
let filterTestData = '';
function roleGroupsForCaller() {
return callerRole === 'supervisor'
@ -112,19 +110,7 @@ function renderUsers() {
const byRole = usersByRoleMap();
const groups = roleGroupsForCaller();
const testFilterCard = `
<div class="card" style="margin-bottom:16px">
<div class="filter-bar">
<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>`;
container.innerHTML = testFilterCard + groups.map(group => {
container.innerHTML = groups.map(group => {
const allInRole = byRole[group.key] || [];
if (!allInRole.length) return '';
return `
@ -154,11 +140,6 @@ function renderUsers() {
</div>`;
}).join('');
document.getElementById('userTestFilter')?.addEventListener('change', (e) => {
filterTestData = e.target.value;
renderUsers();
});
container.addEventListener('input', (e) => {
const input = e.target.closest('.user-role-search');
if (!input) return;
@ -184,11 +165,6 @@ function filterUsers(users, roleKey) {
return hay.includes(q);
});
}
if (filterTestData === 'test') {
list = list.filter(u => isDevTestUsername(u.username));
} else if (filterTestData === 'hide-test') {
list = list.filter(u => !isDevTestUsername(u.username));
}
return list;
}
@ -302,7 +278,7 @@ function userRowHTML(u, myUsername) {
}
return `
<tr class="${testDataRowClassForUser(u.username).trim()}">
<tr>
<td>
<strong>${esc(u.username)}</strong>
${isSelf ? '<span class="badge badge-you">You</span>' : ''}

View File

@ -1,21 +1,3 @@
/** Dev fixture prefixes (import script / dev_fixture.php). */
export function isDevTestClientCode(code) {
const c = String(code ?? '').trim();
return c.toUpperCase().startsWith('DEV-CL-');
}
export function isDevTestUsername(username) {
return String(username ?? '').trim().toLowerCase().startsWith('dev_');
}
export function testDataRowClassForClient(clientCode) {
return isDevTestClientCode(clientCode) ? ' row-test-data' : '';
}
export function testDataRowClassForUser(username) {
return isDevTestUsername(username) ? ' row-test-data' : '';
}
const STABLE_KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
/** Local question id from full questionID (hash__q5 → q5). */