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:
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user