From ea3a423186476dda1e84841fc0e5c8509f5cc969 Mon Sep 17 00:00:00 2001 From: Tom Hempel Date: Tue, 26 May 2026 17:07:18 +0200 Subject: [PATCH] UX enhancements on data views --- db_view_ordered.php | 27 ++++-- index.html | 154 +++++++++++++++++++++++++++++-- website/css/style.css | 78 +++++++++++++++- website/js/pages/assignments.js | 20 ++++- website/js/pages/clients.js | 90 +++++++++++++++++-- website/js/pages/results.js | 155 +++++++++++++++++++++++++++++--- website/js/pages/users.js | 43 ++++++++- website/js/test-data.js | 34 +++++++ 8 files changed, 558 insertions(+), 43 deletions(-) create mode 100644 website/js/test-data.js diff --git a/db_view_ordered.php b/db_view_ordered.php index 21b08f1..de77214 100644 --- a/db_view_ordered.php +++ b/db_view_ordered.php @@ -16,6 +16,17 @@ if (!is_array($order) || empty($order)) { http_response_code(500); echo "header_ if (!is_array($labels)) $labels = []; if (!is_array($vals)) $vals = []; +function qdb_header_question_key(string $headerId): string { + if ($headerId === 'client_code') { + return 'client_code'; + } + $dash = strpos($headerId, '-'); + if ($dash === false) { + return $headerId; + } + return substr($headerId, $dash + 1); +} + function t_val(string $id, string $raw, array $vals): string { if ($raw === '' || $raw === 'None' || $raw === 'Done' || $raw === 'Not Done') return $raw; $norm = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $raw)); @@ -92,10 +103,11 @@ try { } echo ''; @@ -108,13 +120,16 @@ try { echo ''; foreach ($order as $id) { + $key = qdb_header_question_key($id); $lbl = $labels[$id] ?? ''; - echo ''.htmlspecialchars($lbl).''; + $title = $lbl !== '' ? $lbl : $id; + echo ''.htmlspecialchars($key).''; } echo ''; foreach ($clients as $client) { - echo ''; + $rowClass = stripos($client, 'DEV-CL-') === 0 ? ' row-test-data' : ''; + echo ''; echo ''; foreach ($order as $id) { if ($id === 'client_code') { diff --git a/index.html b/index.html index cc8bab1..dc81c81 100644 --- a/index.html +++ b/index.html @@ -16,16 +16,32 @@ .role-admin { background:#e3f2fd; color:#1565c0; } .role-supervisor { background:#fff3e0; color:#e65100; } .role-coach { background:#e8f5e9; color:#2e7d32; } - .table-wrap { overflow-x: auto; border-radius: 8px; } - table { border-collapse: collapse; width: 100%; margin: 8px 0 24px; } - th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; } - #qdb-table thead tr:nth-child(1) th { position: sticky; top: 0; z-index: 3; background: #f7f7f7; } - #qdb-table thead tr:nth-child(2) th { position: sticky; top: var(--head1h, 34px); z-index: 2; background: #f7f7f7; } + .table-wrap { overflow: auto; border-radius: 8px; -webkit-overflow-scrolling: touch; } + #qdb-table { border-collapse: separate; border-spacing: 0; width: 100%; margin: 0; } + #qdb-table th, #qdb-table td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; } + #qdb-table thead tr:nth-child(1) th { + position: sticky; top: 0; z-index: 4; + background: #f7f7f7; box-shadow: 0 1px 0 #ddd; + } + #qdb-table thead tr:nth-child(2) th { + position: sticky; top: var(--head1h, 34px); z-index: 3; + background: #fafafa; box-shadow: 0 1px 0 #ddd; + } + #qdb-table thead tr:nth-child(1) th.chkcol { + z-index: 5; + } .row-highlight { animation: hi 1.2s ease-in-out 0s 1; } @keyframes hi { 0% { background:#fff9c4; } 100% { background:transparent; } } .find-wrap { margin-left:auto; display:flex; gap:6px; align-items:center; } .find-wrap input[type="text"] { padding:6px 8px; border:1px solid #ccc; border-radius:8px; min-width:160px; } #qdb-table th:nth-child(3), #qdb-table td:nth-child(3) { max-width:260px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } + #qdb-table tbody tr.row-test-data td { background:#fffde7; } + #qdb-table tbody tr.row-test-data:hover td { background:#fff9c4; } + #qdb-table tbody tr.row-filter-hidden { display:none; } + #qdb-table th.col-highlight { outline:2px solid #2563eb; outline-offset:-2px; } + .db-toolbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:10px; } + .db-toolbar input, .db-toolbar select { padding:6px 8px; border:1px solid #ccc; border-radius:8px; font-size:14px; } + .db-toolbar label { font-size:14px; } input[type="text"], input[type="password"], select { padding:8px; border:1px solid #ccc; border-radius:8px; font-size:14px; } .form-group { margin-bottom: 12px; } .form-group label { display:block; margin-bottom:4px; font-weight:500; } @@ -141,6 +157,23 @@ + +
Loading...
@@ -346,7 +379,13 @@ function sizeTableArea() { const rect = wrap.getBoundingClientRect(); const avail = window.innerHeight - rect.top - 16; wrap.style.maxHeight = (avail > 200 ? avail : 200) + 'px'; - wrap.style.overflowY = 'auto'; + wrap.style.overflow = 'auto'; + + const table = document.querySelector('#qdb-table'); + if (table?.tHead?.rows?.[0]) { + const h1 = table.tHead.rows[0].getBoundingClientRect().height || 34; + table.style.setProperty('--head1h', `${Math.round(h1)}px`); + } } async function loadDbView() { @@ -385,6 +424,8 @@ async function loadDbView() { $('#exportSelectedBtn').onclick = () => exportSelectedToXLSX(); buildClientSearchList(); + buildColumnKeyList(); + setupDbTableFilters(); const findInput = $('#findClient'); const findBtn = $('#findBtn'); @@ -393,10 +434,15 @@ async function loadDbView() { if (e.key === 'Enter') { e.preventDefault(); findBtn.click(); } }); - if (table.tHead && table.tHead.rows.length >= 1) { - const h1 = table.tHead.rows[0].getBoundingClientRect().height || 34; - table.style.setProperty('--head1h', `${Math.round(h1)}px`); + const colInput = $('#findColumn'); + const colBtn = $('#findColumnBtn'); + if (colBtn) colBtn.onclick = () => gotoColumn(colInput?.value?.trim() || ''); + if (colInput) { + colInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); gotoColumn(colInput.value.trim()); } + }); } + sizeTableArea(); } catch (e) { $('#dbContainer').innerHTML = 'Error: ' + e.message; @@ -438,6 +484,96 @@ function exportSelectedToXLSX() { XLSX.writeFile(wb, 'SelectedClients.xlsx'); } +/* ---------- DB TABLE FILTERS / NAV ---------- */ +function isTestClientCode(code) { + return String(code || '').trim().toUpperCase().startsWith('DEV-CL-'); +} + +function buildColumnKeyList() { + const table = document.querySelector('#qdb-table'); + const dl = document.querySelector('#columnKeys'); + if (!table || !dl) return; + dl.innerHTML = ''; + const head = table.tHead?.rows?.[0]; + if (!head) return; + const keys = new Set(); + for (let i = 1; i < head.cells.length; i++) { + const id = head.cells[i].getAttribute('data-id') || ''; + if (!id) continue; + if (id === 'client_code') keys.add('client_code'); + else { + const dash = id.indexOf('-'); + keys.add(dash >= 0 ? id.slice(dash + 1) : id); + } + } + Array.from(keys).sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + .forEach(k => { const opt = document.createElement('option'); opt.value = k; dl.appendChild(opt); }); +} + +function setupDbTableFilters() { + const table = document.querySelector('#qdb-table'); + const toolbar = $('#dbToolbar'); + if (!table || !toolbar) return; + toolbar.style.display = ''; + + const filterInput = $('#filterRows'); + const testFilter = $('#testDataFilter'); + const countEl = $('#visibleRowCount'); + + function applyFilters() { + const q = (filterInput?.value || '').trim().toLowerCase(); + const mode = testFilter?.value || ''; + let visible = 0; + table.querySelectorAll('tbody tr').forEach(tr => { + const code = (tr.getAttribute('data-client') || tr.cells[1]?.textContent || '').trim(); + const matchText = !q || code.toLowerCase().includes(q); + const isTest = isTestClientCode(code); + const matchTest = mode === 'test' ? isTest : mode === 'hide-test' ? !isTest : true; + const show = matchText && matchTest; + tr.classList.toggle('row-filter-hidden', !show); + if (show) visible++; + }); + if (countEl) countEl.textContent = `${visible} row(s) visible`; + } + + filterInput?.addEventListener('input', applyFilters); + testFilter?.addEventListener('change', applyFilters); + applyFilters(); +} + +function gotoColumn(query) { + if (!query) return; + const table = document.querySelector('#qdb-table'); + if (!table) return; + const q = query.toLowerCase(); + const head = table.tHead?.rows?.[0]; + if (!head) return; + + table.querySelectorAll('th.col-highlight').forEach(th => th.classList.remove('col-highlight')); + + let targetIdx = -1; + for (let i = 1; i < head.cells.length; i++) { + const th = head.cells[i]; + const id = (th.getAttribute('data-id') || '').toLowerCase(); + const keyCell = table.tHead.rows[1]?.cells[i]; + const key = (keyCell?.textContent || '').trim().toLowerCase(); + if (id === q || key === q || id.endsWith('-' + q) || id.includes(q)) { + targetIdx = i; + th.classList.add('col-highlight'); + th.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); + break; + } + } + if (targetIdx < 0) { + alert('No column matching "' + query + '"'); + return; + } + table.querySelectorAll('tbody td:nth-child(' + (targetIdx + 1) + ')').forEach(td => { + td.style.outline = '1px solid #93c5fd'; + setTimeout(() => { td.style.outline = ''; }, 2000); + }); +} + /* ---------- CLIENT SEARCH ---------- */ function buildClientSearchList() { const table = document.querySelector('#qdb-table'); diff --git a/website/css/style.css b/website/css/style.css index b6cd609..c81c97d 100644 --- a/website/css/style.css +++ b/website/css/style.css @@ -258,13 +258,16 @@ body { border-top: 1px solid var(--border); } -/* Tables */ +/* Tables — scroll inside wrapper so sticky headers stay visible */ .table-wrapper { - overflow-x: auto; + overflow: auto; + max-height: min(72vh, calc(100vh - 220px)); + -webkit-overflow-scrolling: touch; } table.data-table { width: 100%; - border-collapse: collapse; + border-collapse: separate; + border-spacing: 0; font-size: .875rem; } table.data-table th, @@ -282,11 +285,42 @@ table.data-table th { background: #f8fafc; position: sticky; top: 0; - z-index: 1; + z-index: 2; + box-shadow: 0 1px 0 var(--border); } table.data-table tr:hover td { background: #f8fafc; } +table.data-table tbody tr.row-test-data td { + background: #fffde7; +} +table.data-table tbody tr.row-test-data:hover td { + background: #fff9c4; +} +#qdb-table tbody tr.row-test-data td { + background: #fffde7; +} +#qdb-table tbody tr.row-test-data:hover td { + background: #fff9c4; +} +table.data-table .sticky-col { + position: sticky; + left: 0; + z-index: 1; + background: var(--surface); + box-shadow: 2px 0 4px rgba(0, 0, 0, 0.06); +} +table.data-table thead th.sticky-col { + z-index: 5; + top: 0; + background: #f8fafc; +} +table.data-table tbody tr.row-test-data td.sticky-col { + background: #fffde7; +} +table.data-table tbody tr.row-test-data:hover td.sticky-col { + background: #fff9c4; +} th.sortable { cursor: pointer; user-select: none; } th.sortable::after { content: ' \2195'; opacity: .4; } th.sort-asc::after { content: ' \2191'; opacity: 1; } @@ -484,6 +518,42 @@ th.sort-desc::after { content: ' \2193'; opacity: 1; } font-size: .85rem; background: var(--surface); } +.filter-bar input[type="search"], +.filter-bar input[type="text"].filter-search { + min-width: 180px; +} +.pagination-bar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-top: 12px; + font-size: .85rem; + color: var(--text-secondary); +} +.pagination-bar .btn { + min-width: 2.5rem; +} +.data-toolbar-hint { + font-size: .8rem; + color: var(--text-secondary); +} +#qdb-table th.col-key { + font-size: .75rem; + font-weight: 500; + text-transform: none; + letter-spacing: 0; + max-width: 140px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +#qdb-table thead tr:nth-child(1) th[data-id] { + font-size: .7rem; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; +} /* Empty state */ .empty-state { diff --git a/website/js/pages/assignments.js b/website/js/pages/assignments.js index 6d43057..a7968aa 100644 --- a/website/js/pages/assignments.js +++ b/website/js/pages/assignments.js @@ -1,5 +1,6 @@ import { apiGet, apiPost } from '../api.js'; import { showToast } from '../app.js'; +import { isDevTestClientCode, isDevTestUsername, testDataRowClassForClient } from '../test-data.js'; let coaches = []; let clients = []; @@ -61,8 +62,9 @@ function renderAssignments() {