improved scoring ui + removed test data filters
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
@ -31,8 +31,6 @@
|
|||||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
--table-header-bg: #1e2430;
|
--table-header-bg: #1e2430;
|
||||||
--row-hover-bg: #252c3a;
|
--row-hover-bg: #252c3a;
|
||||||
--test-row-bg: #2a2618;
|
|
||||||
--test-row-hover-bg: #3a321f;
|
|
||||||
--sticky-shadow: rgba(0, 0, 0, 0.35);
|
--sticky-shadow: rgba(0, 0, 0, 0.35);
|
||||||
/* Incomplete translation highlight */
|
/* Incomplete translation highlight */
|
||||||
--trans-missing-bg: #1c222d;
|
--trans-missing-bg: #1c222d;
|
||||||
@ -686,18 +684,6 @@ table.data-table th {
|
|||||||
table.data-table tr:hover td {
|
table.data-table tr:hover td {
|
||||||
background: var(--row-hover-bg);
|
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 {
|
table.data-table .sticky-col {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -710,12 +696,6 @@ table.data-table thead th.sticky-col {
|
|||||||
top: 0;
|
top: 0;
|
||||||
background: var(--table-header-bg);
|
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 { cursor: pointer; user-select: none; }
|
||||||
th.sortable::after { content: ' \2195'; opacity: .4; }
|
th.sortable::after { content: ' \2195'; opacity: .4; }
|
||||||
th.sort-asc::after { content: ' \2191'; opacity: 1; }
|
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);
|
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 {
|
.coach-list-empty {
|
||||||
padding: 20px 12px;
|
padding: 20px 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -3034,19 +3002,133 @@ select:disabled {
|
|||||||
letter-spacing: .04em;
|
letter-spacing: .04em;
|
||||||
color: #ca8a04;
|
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%;
|
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 {
|
.coach-band-trigger {
|
||||||
opacity: 0.65;
|
display: inline-flex;
|
||||||
cursor: wait;
|
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-hint,
|
||||||
.client-profile-score-meta {
|
.client-profile-score-meta {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { apiGet, apiPost } from '../api.js';
|
import { apiGet, apiPost } from '../api.js';
|
||||||
import { pageHeaderHTML, showToast } from '../app.js';
|
import { pageHeaderHTML, showToast } from '../app.js';
|
||||||
import { isDevTestClientCode, isDevTestUsername, testDataRowClassForClient } from '../test-data.js';
|
|
||||||
|
|
||||||
const CLIENT_PAGE_SIZE = 50;
|
const CLIENT_PAGE_SIZE = 50;
|
||||||
|
|
||||||
@ -100,11 +99,6 @@ function renderAssignments() {
|
|||||||
</select>
|
</select>
|
||||||
<input type="search" id="clientSearch" class="filter-search"
|
<input type="search" id="clientSearch" class="filter-search"
|
||||||
placeholder="Search client or coach…">
|
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>
|
</div>
|
||||||
<div class="assign-table-panel">
|
<div class="assign-table-panel">
|
||||||
@ -178,10 +172,6 @@ function renderAssignments() {
|
|||||||
clientPage = 0;
|
clientPage = 0;
|
||||||
renderClientRows();
|
renderClientRows();
|
||||||
});
|
});
|
||||||
document.getElementById('clientTestFilter').addEventListener('change', () => {
|
|
||||||
clientPage = 0;
|
|
||||||
renderClientRows();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('selectAllBtn').addEventListener('click', () => {
|
document.getElementById('selectAllBtn').addEventListener('click', () => {
|
||||||
document.querySelectorAll('#clientTableBody .client-cb').forEach(cb => {
|
document.querySelectorAll('#clientTableBody .client-cb').forEach(cb => {
|
||||||
@ -313,10 +303,9 @@ function renderCoachList() {
|
|||||||
|
|
||||||
function coachListItemHTML(c) {
|
function coachListItemHTML(c) {
|
||||||
const count = (clientsByCoach[c.coachID] || []).length;
|
const count = (clientsByCoach[c.coachID] || []).length;
|
||||||
const coachTest = isDevTestUsername(c.username) ? ' row-test-data' : '';
|
|
||||||
const active = selectedCoachID === c.coachID ? ' active' : '';
|
const active = selectedCoachID === c.coachID ? ' active' : '';
|
||||||
return `
|
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>
|
<div class="coach-name">${esc(c.username)}</div>
|
||||||
${c.supervisorUsername
|
${c.supervisorUsername
|
||||||
? `<div class="coach-supervisor">${esc(c.supervisorUsername)}</div>`
|
? `<div class="coach-supervisor">${esc(c.supervisorUsername)}</div>`
|
||||||
@ -336,8 +325,6 @@ function highlightCoachInList(coachID) {
|
|||||||
function getVisibleClients() {
|
function getVisibleClients() {
|
||||||
const coachFilter = document.getElementById('filterByCoach')?.value ?? '';
|
const coachFilter = document.getElementById('filterByCoach')?.value ?? '';
|
||||||
const search = document.getElementById('clientSearch')?.value.trim().toLowerCase() ?? '';
|
const search = document.getElementById('clientSearch')?.value.trim().toLowerCase() ?? '';
|
||||||
const testMode = document.getElementById('clientTestFilter')?.value || '';
|
|
||||||
|
|
||||||
let visible = clients;
|
let visible = clients;
|
||||||
if (coachFilter === '__unassigned__') {
|
if (coachFilter === '__unassigned__') {
|
||||||
visible = clients.filter(c => !c.coachID);
|
visible = clients.filter(c => !c.coachID);
|
||||||
@ -350,11 +337,6 @@ function getVisibleClients() {
|
|||||||
return hay.includes(search);
|
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;
|
return visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,8 +345,7 @@ function updateClientSummary(filteredCount, pageSlice) {
|
|||||||
if (!el) return;
|
if (!el) return;
|
||||||
const search = document.getElementById('clientSearch')?.value.trim() ?? '';
|
const search = document.getElementById('clientSearch')?.value.trim() ?? '';
|
||||||
const coachFilter = document.getElementById('filterByCoach')?.value ?? '';
|
const coachFilter = document.getElementById('filterByCoach')?.value ?? '';
|
||||||
const testFilter = document.getElementById('clientTestFilter')?.value;
|
const hasFilter = search || coachFilter;
|
||||||
const hasFilter = search || coachFilter || testFilter;
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (hasFilter && filteredCount !== clients.length) {
|
if (hasFilter && filteredCount !== clients.length) {
|
||||||
parts.push(`${filteredCount} of ${clients.length} match`);
|
parts.push(`${filteredCount} of ${clients.length} match`);
|
||||||
@ -453,7 +434,7 @@ function renderClientRows() {
|
|||||||
body.innerHTML = slice.map(c => {
|
body.innerHTML = slice.map(c => {
|
||||||
const checked = selectedClientCodes.has(c.clientCode) ? ' checked' : '';
|
const checked = selectedClientCodes.has(c.clientCode) ? ' checked' : '';
|
||||||
return `
|
return `
|
||||||
<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" class="client-cb" data-code="${esc(c.clientCode)}" value="${esc(c.clientCode)}"${checked} aria-label="Select ${esc(c.clientCode)}">
|
<input type="checkbox" class="client-cb" data-code="${esc(c.clientCode)}" value="${esc(c.clientCode)}"${checked} aria-label="Select ${esc(c.clientCode)}">
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||||
import { pageHeaderHTML, showToast } from '../app.js';
|
import { pageHeaderHTML, showToast } from '../app.js';
|
||||||
import { isDevTestClientCode, testDataRowClassForClient } from '../test-data.js';
|
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@ -8,7 +7,6 @@ let clientsList = [];
|
|||||||
let coachesList = [];
|
let coachesList = [];
|
||||||
let scoringProfileCatalog = [];
|
let scoringProfileCatalog = [];
|
||||||
let filterSearch = '';
|
let filterSearch = '';
|
||||||
let filterTestData = '';
|
|
||||||
let page = 0;
|
let page = 0;
|
||||||
let expandedClientCode = null;
|
let expandedClientCode = null;
|
||||||
let expandedQnKey = null;
|
let expandedQnKey = null;
|
||||||
@ -74,11 +72,6 @@ function renderClients() {
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<input type="search" id="clientListSearch" class="filter-search" placeholder="Search client or coach…" value="${esc(filterSearch)}">
|
<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>
|
<span class="data-toolbar-hint" id="clientListCount"></span>
|
||||||
</div>
|
</div>
|
||||||
${profileLegend}
|
${profileLegend}
|
||||||
@ -107,13 +100,7 @@ function renderClients() {
|
|||||||
clearExpandIfHidden();
|
clearExpandIfHidden();
|
||||||
renderClientTableBody();
|
renderClientTableBody();
|
||||||
});
|
});
|
||||||
document.getElementById('clientTestFilter').addEventListener('change', (e) => {
|
ensureCoachBandGlobalClick();
|
||||||
filterTestData = e.target.value;
|
|
||||||
page = 0;
|
|
||||||
clearExpandIfHidden();
|
|
||||||
renderClientTableBody();
|
|
||||||
});
|
|
||||||
|
|
||||||
renderClientTableBody();
|
renderClientTableBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,11 +113,6 @@ function getFilteredClientsList() {
|
|||||||
return hay.includes(q);
|
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;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,12 +149,7 @@ function renderClientTableBody() {
|
|||||||
body.querySelectorAll('.delete-client-btn').forEach(btn => {
|
body.querySelectorAll('.delete-client-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => deleteClient(btn.dataset.code));
|
btn.addEventListener('click', () => deleteClient(btn.dataset.code));
|
||||||
});
|
});
|
||||||
body.querySelectorAll('.client-coach-band-select').forEach(select => {
|
bindCoachBandPickerEvents(body);
|
||||||
select.addEventListener('focus', () => {
|
|
||||||
select.dataset.prev = select.value;
|
|
||||||
});
|
|
||||||
select.addEventListener('change', () => onCoachBandChange(select));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pag = document.getElementById('clientsPagination');
|
const pag = document.getElementById('clientsPagination');
|
||||||
@ -309,29 +286,47 @@ function bandLabel(band) {
|
|||||||
return band.charAt(0).toUpperCase() + band.slice(1);
|
return band.charAt(0).toUpperCase() + band.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function coachBandSelectHTML(p, clientCode) {
|
function coachBandPickerHTML(p, clientCode) {
|
||||||
const calcBand = p.calculatedBand || p.band;
|
const calcBand = p.calculatedBand || p.band;
|
||||||
if (!calcBand || !clientCode || !p.profileID) {
|
if (!calcBand || !clientCode || !p.profileID) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const current = p.coachBand || '';
|
const current = p.coachBand || '';
|
||||||
const coachPending = p.pendingReview !== false && !current;
|
const coachPending = p.pendingReview !== false && !current;
|
||||||
const placeholder = coachPending
|
const triggerBadge = coachPending
|
||||||
? '<option value="" selected disabled>Awaiting review</option>'
|
? bandBadgeHTML(null, { pending: true })
|
||||||
: '';
|
: bandBadgeHTML(current);
|
||||||
const options = COACH_BANDS.map(b => {
|
const showAgree = coachPending || (current && current !== calcBand);
|
||||||
const selected = current === b ? ' selected' : '';
|
const options = COACH_BANDS.map(b => `
|
||||||
return `<option value="${esc(b)}"${selected}>${esc(bandLabel(b))}</option>`;
|
<button type="button" class="coach-band-option${current === b ? ' is-selected' : ''}"
|
||||||
}).join('');
|
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 `
|
return `
|
||||||
<select class="client-coach-band-select"
|
<div class="coach-band-picker"
|
||||||
data-client="${esc(clientCode)}"
|
data-client="${esc(clientCode)}"
|
||||||
data-profile="${esc(p.profileID)}"
|
data-profile="${esc(p.profileID)}"
|
||||||
data-profile-name="${esc(p.name || p.profileID)}"
|
data-profile-name="${esc(p.name || p.profileID)}"
|
||||||
data-calculated="${esc(calcBand)}"
|
data-calculated="${esc(calcBand)}"
|
||||||
data-weighted="${esc(String(p.weightedTotal ?? ''))}"
|
data-weighted="${esc(String(p.weightedTotal ?? ''))}"
|
||||||
data-prev="${esc(current)}"
|
data-coach="${esc(current)}">
|
||||||
title="Set coach category">${placeholder}${options}</select>`;
|
<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 = '') {
|
function profileScoreBlockHTML(p, clientCode = '') {
|
||||||
@ -346,7 +341,7 @@ function profileScoreBlockHTML(p, clientCode = '') {
|
|||||||
const coachPending = p.pendingReview !== false && !p.coachBand;
|
const coachPending = p.pendingReview !== false && !p.coachBand;
|
||||||
const coachDiffers = p.coachBand && p.coachBand !== calcBand;
|
const coachDiffers = p.coachBand && p.coachBand !== calcBand;
|
||||||
const coachControl = clientCode
|
const coachControl = clientCode
|
||||||
? coachBandSelectHTML(p, clientCode)
|
? coachBandPickerHTML(p, clientCode)
|
||||||
: (coachPending ? bandBadgeHTML(null, { pending: true }) : bandBadgeHTML(p.coachBand));
|
: (coachPending ? bandBadgeHTML(null, { pending: true }) : bandBadgeHTML(p.coachBand));
|
||||||
return `
|
return `
|
||||||
<div class="client-profile-score-block${coachDiffers ? ' has-coach-override' : ''}" data-profile-id="${esc(p.profileID || '')}">
|
<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) {
|
function resetCoachBandMenuPosition(menu) {
|
||||||
const clientCode = select.dataset.client;
|
if (!menu) return;
|
||||||
const profileID = select.dataset.profile;
|
menu.style.position = '';
|
||||||
const profileName = select.dataset.profileName || profileID;
|
menu.style.left = '';
|
||||||
const calculatedBand = select.dataset.calculated;
|
menu.style.top = '';
|
||||||
const weightedRaw = select.dataset.weighted;
|
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 weightedTotal = weightedRaw !== '' ? Number(weightedRaw) : null;
|
||||||
const prev = select.dataset.prev || '';
|
picker.classList.add('is-saving');
|
||||||
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;
|
|
||||||
try {
|
try {
|
||||||
await apiPut('clients.php', {
|
await apiPut('clients.php', {
|
||||||
clientCode,
|
clientCode,
|
||||||
@ -518,17 +612,42 @@ async function onCoachBandChange(select) {
|
|||||||
weightedTotal,
|
weightedTotal,
|
||||||
});
|
});
|
||||||
patchClientScoringBand(clientCode, profileID, coachBand);
|
patchClientScoringBand(clientCode, profileID, coachBand);
|
||||||
select.dataset.prev = coachBand;
|
|
||||||
showToast(`Coach category saved for ${profileName}`, 'success');
|
showToast(`Coach category saved for ${profileName}`, 'success');
|
||||||
renderClientTableBody();
|
renderClientTableBody();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
select.value = prev || '';
|
|
||||||
showToast(e.message, 'error');
|
showToast(e.message, 'error');
|
||||||
} finally {
|
} 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) {
|
function clientHasResponseData(c) {
|
||||||
return Number(c.hasResponseData) === 1;
|
return Number(c.hasResponseData) === 1;
|
||||||
}
|
}
|
||||||
@ -548,7 +667,7 @@ function clientRowHTML(c) {
|
|||||||
const colCount = scoringProfileCatalog.length > 0 ? 4 : 3;
|
const colCount = scoringProfileCatalog.length > 0 ? 4 : 3;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
${expandControl}<strong>${esc(c.clientCode)}</strong>
|
${expandControl}<strong>${esc(c.clientCode)}</strong>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import { apiGet } from '../api.js';
|
import { apiGet } from '../api.js';
|
||||||
import { homeNavButton, showToast } from '../app.js';
|
import { homeNavButton, showToast } from '../app.js';
|
||||||
import {
|
import { buildResultColumns, resultColumnCellValue } from '../test-data.js';
|
||||||
buildResultColumns,
|
|
||||||
isDevTestClientCode,
|
|
||||||
resultColumnCellValue,
|
|
||||||
testDataRowClassForClient,
|
|
||||||
} from '../test-data.js';
|
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@ -21,7 +16,6 @@ let filterStatus = '';
|
|||||||
let filterDateFrom = '';
|
let filterDateFrom = '';
|
||||||
let filterDateTo = '';
|
let filterDateTo = '';
|
||||||
let filterSearch = '';
|
let filterSearch = '';
|
||||||
let filterTestData = '';
|
|
||||||
let filterQuestion = '';
|
let filterQuestion = '';
|
||||||
let page = 0;
|
let page = 0;
|
||||||
|
|
||||||
@ -35,7 +29,6 @@ export async function resultsPage(params) {
|
|||||||
filterDateFrom = '';
|
filterDateFrom = '';
|
||||||
filterDateTo = '';
|
filterDateTo = '';
|
||||||
filterSearch = '';
|
filterSearch = '';
|
||||||
filterTestData = '';
|
|
||||||
filterQuestion = '';
|
filterQuestion = '';
|
||||||
page = 0;
|
page = 0;
|
||||||
|
|
||||||
@ -102,11 +95,6 @@ function renderResults() {
|
|||||||
<input type="date" id="filterDateTo" value="${esc(filterDateTo)}">
|
<input type="date" id="filterDateTo" value="${esc(filterDateTo)}">
|
||||||
</label>
|
</label>
|
||||||
<input type="search" id="filterSearch" class="filter-search" placeholder="Search client, coach, status…" value="${esc(filterSearch)}">
|
<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">
|
<input type="search" id="filterQuestion" placeholder="Go to question key…" value="${esc(filterQuestion)}" style="min-width:160px" list="questionKeyList">
|
||||||
<datalist id="questionKeyList"></datalist>
|
<datalist id="questionKeyList"></datalist>
|
||||||
<button type="button" class="btn btn-sm" id="gotoQuestionBtn">Go to column</button>
|
<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>
|
<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>
|
<a id="exportCsvLink" class="btn btn-sm" href="#">Export CSV</a>
|
||||||
</div>
|
</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>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-wrapper" id="resultsTableWrap">
|
<div class="table-wrapper" id="resultsTableWrap">
|
||||||
@ -162,11 +150,6 @@ function renderResults() {
|
|||||||
page = 0;
|
page = 0;
|
||||||
renderTableRows();
|
renderTableRows();
|
||||||
});
|
});
|
||||||
document.getElementById('filterTestData').addEventListener('change', (e) => {
|
|
||||||
filterTestData = e.target.value;
|
|
||||||
page = 0;
|
|
||||||
renderTableRows();
|
|
||||||
});
|
|
||||||
document.getElementById('gotoQuestionBtn').addEventListener('click', () => {
|
document.getElementById('gotoQuestionBtn').addEventListener('click', () => {
|
||||||
filterQuestion = document.getElementById('filterQuestion').value.trim();
|
filterQuestion = document.getElementById('filterQuestion').value.trim();
|
||||||
scrollToQuestionColumn(filterQuestion);
|
scrollToQuestionColumn(filterQuestion);
|
||||||
@ -185,7 +168,6 @@ function renderResults() {
|
|||||||
filterDateFrom = '';
|
filterDateFrom = '';
|
||||||
filterDateTo = '';
|
filterDateTo = '';
|
||||||
filterSearch = '';
|
filterSearch = '';
|
||||||
filterTestData = '';
|
|
||||||
filterQuestion = '';
|
filterQuestion = '';
|
||||||
page = 0;
|
page = 0;
|
||||||
renderResults();
|
renderResults();
|
||||||
@ -304,11 +286,6 @@ function getFilteredClients() {
|
|||||||
return hay.includes(q);
|
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;
|
return clients;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,7 +381,7 @@ function renderTableRows() {
|
|||||||
return `<td>${esc(String(val))}</td>`;
|
return `<td>${esc(String(val))}</td>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
return `<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
|
return `<tr>
|
||||||
<td class="sticky-col">${esc(c.clientCode)}</td>
|
<td class="sticky-col">${esc(c.clientCode)}</td>
|
||||||
<td>${esc(coachDisplayName(c))}</td>
|
<td>${esc(coachDisplayName(c))}</td>
|
||||||
<td>${esc(supervisorDisplayName(c))}</td>
|
<td>${esc(supervisorDisplayName(c))}</td>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||||
import { getRole, getUser, pageHeaderHTML, showToast } from '../app.js';
|
import { getRole, getUser, pageHeaderHTML, showToast } from '../app.js';
|
||||||
import { openAdminResetPasswordModal } from '../password-modal.js';
|
import { openAdminResetPasswordModal } from '../password-modal.js';
|
||||||
import { isDevTestUsername, testDataRowClassForUser } from '../test-data.js';
|
|
||||||
|
|
||||||
// Roles an admin can create; supervisors can only create coaches
|
// Roles an admin can create; supervisors can only create coaches
|
||||||
const CREATEABLE_ROLES = {
|
const CREATEABLE_ROLES = {
|
||||||
@ -14,7 +13,6 @@ let supervisorsList = [];
|
|||||||
let callerRole = '';
|
let callerRole = '';
|
||||||
/** @type {Record<string, string>} */
|
/** @type {Record<string, string>} */
|
||||||
let filterSearchByRole = { admin: '', supervisor: '', coach: '' };
|
let filterSearchByRole = { admin: '', supervisor: '', coach: '' };
|
||||||
let filterTestData = '';
|
|
||||||
|
|
||||||
function roleGroupsForCaller() {
|
function roleGroupsForCaller() {
|
||||||
return callerRole === 'supervisor'
|
return callerRole === 'supervisor'
|
||||||
@ -112,19 +110,7 @@ function renderUsers() {
|
|||||||
const byRole = usersByRoleMap();
|
const byRole = usersByRoleMap();
|
||||||
const groups = roleGroupsForCaller();
|
const groups = roleGroupsForCaller();
|
||||||
|
|
||||||
const testFilterCard = `
|
container.innerHTML = groups.map(group => {
|
||||||
<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 => {
|
|
||||||
const allInRole = byRole[group.key] || [];
|
const allInRole = byRole[group.key] || [];
|
||||||
if (!allInRole.length) return '';
|
if (!allInRole.length) return '';
|
||||||
return `
|
return `
|
||||||
@ -154,11 +140,6 @@ function renderUsers() {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
document.getElementById('userTestFilter')?.addEventListener('change', (e) => {
|
|
||||||
filterTestData = e.target.value;
|
|
||||||
renderUsers();
|
|
||||||
});
|
|
||||||
|
|
||||||
container.addEventListener('input', (e) => {
|
container.addEventListener('input', (e) => {
|
||||||
const input = e.target.closest('.user-role-search');
|
const input = e.target.closest('.user-role-search');
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
@ -184,11 +165,6 @@ function filterUsers(users, roleKey) {
|
|||||||
return hay.includes(q);
|
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;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,7 +278,7 @@ function userRowHTML(u, myUsername) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr class="${testDataRowClassForUser(u.username).trim()}">
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>${esc(u.username)}</strong>
|
<strong>${esc(u.username)}</strong>
|
||||||
${isSelf ? '<span class="badge badge-you">You</span>' : ''}
|
${isSelf ? '<span class="badge badge-you">You</span>' : ''}
|
||||||
|
|||||||
@ -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_]*$/;
|
const STABLE_KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
||||||
|
|
||||||
/** Local question id from full questionID (hash__q5 → q5). */
|
/** Local question id from full questionID (hash__q5 → q5). */
|
||||||
|
|||||||
Reference in New Issue
Block a user