UX enhancements on data views
This commit is contained in:
154
index.html
154
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 @@
|
||||
</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');
|
||||
|
||||
Reference in New Issue
Block a user