From 3211d35b436b226eeb1c8cad266687d849bb7523 Mon Sep 17 00:00:00 2001 From: Tom Hempel Date: Mon, 8 Jun 2026 19:38:37 +0200 Subject: [PATCH] improved scoring ui + removed test data filters --- website/css/style.css | 168 +++++++++++++++------ website/js/pages/assignments.js | 25 +--- website/js/pages/clients.js | 249 +++++++++++++++++++++++--------- website/js/pages/results.js | 29 +--- website/js/pages/users.js | 28 +--- website/js/test-data.js | 18 --- 6 files changed, 317 insertions(+), 200 deletions(-) diff --git a/website/css/style.css b/website/css/style.css index 23ea546..9ea1452 100644 --- a/website/css/style.css +++ b/website/css/style.css @@ -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 { diff --git a/website/js/pages/assignments.js b/website/js/pages/assignments.js index 0aa114e..fe8e4ca 100644 --- a/website/js/pages/assignments.js +++ b/website/js/pages/assignments.js @@ -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() { -
@@ -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 ` -
  • +
  • ${esc(c.username)}
    ${c.supervisorUsername ? `
    ${esc(c.supervisorUsername)}
    ` @@ -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 ` - + diff --git a/website/js/pages/clients.js b/website/js/pages/clients.js index a2f7bbd..e966c0e 100644 --- a/website/js/pages/clients.js +++ b/website/js/pages/clients.js @@ -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() {
    -
    ${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 - ? '' - : ''; - const options = COACH_BANDS.map(b => { - const selected = current === b ? ' selected' : ''; - return ``; - }).join(''); + const triggerBadge = coachPending + ? bandBadgeHTML(null, { pending: true }) + : bandBadgeHTML(current); + const showAgree = coachPending || (current && current !== calcBand); + const options = COACH_BANDS.map(b => ` + `).join(''); + const agreeBtn = showAgree ? ` + ` : ''; return ` - `; + data-coach="${esc(current)}"> + + +
    `; } 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 `
    @@ -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 = ` + `; + 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 ` - + ${expandControl}${esc(c.clientCode)} diff --git a/website/js/pages/results.js b/website/js/pages/results.js index b7f4983..b972a40 100644 --- a/website/js/pages/results.js +++ b/website/js/pages/results.js @@ -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() { - @@ -114,7 +102,7 @@ function renderResults() { Export CSV
    -

    Each question has its own column; glass-scale symptoms are separate columns. Hover headers for context. Test rows (DEV-CL-*) are highlighted.

    +

    Each question has its own column; glass-scale symptoms are separate columns. Hover headers for context.

  • @@ -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 `${esc(String(val))}`; }).join(''); - return ` + return ` ${esc(c.clientCode)} ${esc(coachDisplayName(c))} ${esc(supervisorDisplayName(c))} diff --git a/website/js/pages/users.js b/website/js/pages/users.js index ecbe5e8..d35bf27 100644 --- a/website/js/pages/users.js +++ b/website/js/pages/users.js @@ -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} */ 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 = ` -
    -
    - - -
    -
    `; - - 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() {
    `; }).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 ` - + ${esc(u.username)} ${isSelf ? 'You' : ''} diff --git a/website/js/test-data.js b/website/js/test-data.js index 3e03823..d7a7111 100644 --- a/website/js/test-data.js +++ b/website/js/test-data.js @@ -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). */