UX enhancements on data views

This commit is contained in:
2026-05-26 17:07:18 +02:00
parent 9d783b60a9
commit ea3a423186
8 changed files with 558 additions and 43 deletions

View File

@ -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 @@
</div>
</div>
<div class="db-toolbar" id="dbToolbar" style="display:none">
<label>Filter rows <input id="filterRows" type="search" placeholder="Client code contains…" style="min-width:200px"></label>
<label>Test data
<select id="testDataFilter">
<option value="">Show all</option>
<option value="test">Test only (DEV-CL-*)</option>
<option value="hide-test">Hide test data</option>
</select>
</label>
<label>Go to column
<input id="findColumn" type="text" list="columnKeys" placeholder="Question key…" style="min-width:160px">
<datalist id="columnKeys"></datalist>
</label>
<button type="button" id="findColumnBtn">Go</button>
<span id="visibleRowCount" class="muted"></span>
</div>
<div class="table-wrap" id="tableWrap">
<div id="dbContainer"><em>Loading...</em></div>
</div>
@ -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 = '<b>Error:</b> ' + 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');