import { apiGet, apiUrl, apiDownloadFetch } from '../api.js'; import { homeNavButton, showToast } from '../app.js'; import { buildResultColumns, resultColumnCellValue } from '../test-data.js'; const PAGE_SIZE = 50; let sortCol = null; let sortDir = 'asc'; let allClients = []; let allSubmissions = []; let showAllVersions = true; let showMetadata = true; let questionnaireId = ''; let questionsDef = []; let resultColumns = []; let questionnaireMeta = null; let filterCoach = ''; let filterSupervisor = ''; let filterStatus = ''; let filterDateFrom = ''; let filterDateTo = ''; let filterSearch = ''; let filterQuestion = ''; let page = 0; export async function resultsPage(params) { const app = document.getElementById('app'); sortCol = null; sortDir = 'asc'; filterCoach = ''; filterSupervisor = ''; filterStatus = ''; filterDateFrom = ''; filterDateTo = ''; filterSearch = ''; filterQuestion = ''; page = 0; showAllVersions = true; showMetadata = true; questionnaireId = params.id; app.innerHTML = `
`; try { await loadResultsData(); } catch (e) { showToast(e.message, 'error'); document.getElementById('resultsContent').innerHTML = `

${esc(e.message)}

`; } } async function loadResultsData() { const versionParam = showAllVersions ? '&allVersions=1' : ''; const data = await apiGet(`results.php?questionnaireID=${encodeURIComponent(questionnaireId)}${versionParam}`); questionnaireMeta = data.questionnaire; questionsDef = data.questions || []; if (data.allVersions) { allSubmissions = data.submissions || []; allClients = []; } else { allClients = data.clients || []; allSubmissions = []; } document.getElementById('resultsTitle').textContent = `Results: ${questionnaireMeta.name} v${questionnaireMeta.version}`; renderResults(); } function renderResults() { const container = document.getElementById('resultsContent'); resultColumns = buildResultColumns(questionsDef); // Collect unique coaches and statuses for filters const coachOptions = getCoachFilterOptions(); const supervisorOptions = getSupervisorFilterOptions(); const statuses = [...new Set(getSourceRows().map(c => c.status))].filter(Boolean).sort(); container.innerHTML = `

${showAllVersions ? `Each row is one uploaded version, sorted by client then version.${showMetadata ? ' Cycle, session, and timestamps are shown per submission.' : ' Turn on metadata to see version, cycle, session, and timestamps.'}` : `Latest response per client.${showMetadata ? ' Cycle, session, and completion times are shown.' : ' Turn on metadata to see cycle, session, and timestamps.'} Each question has its own column.`}

`; const qKeyList = document.getElementById('questionKeyList'); qKeyList.innerHTML = resultColumns .map(col => ``) .join(''); document.getElementById('filterSupervisor').addEventListener('change', (e) => { filterSupervisor = e.target.value; page = 0; renderTableRows(); }); document.getElementById('filterCoach').addEventListener('change', (e) => { filterCoach = e.target.value; page = 0; renderTableRows(); }); document.getElementById('filterStatus').addEventListener('change', (e) => { filterStatus = e.target.value; page = 0; renderTableRows(); }); document.getElementById('filterDateFrom').addEventListener('change', (e) => { filterDateFrom = e.target.value; page = 0; renderTableRows(); }); document.getElementById('filterDateTo').addEventListener('change', (e) => { filterDateTo = e.target.value; page = 0; renderTableRows(); }); document.getElementById('filterSearch').addEventListener('input', (e) => { filterSearch = e.target.value.trim(); page = 0; renderTableRows(); }); document.getElementById('gotoQuestionBtn').addEventListener('click', () => { filterQuestion = document.getElementById('filterQuestion').value.trim(); scrollToQuestionColumn(filterQuestion); }); document.getElementById('filterQuestion').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); filterQuestion = e.target.value.trim(); scrollToQuestionColumn(filterQuestion); } }); document.getElementById('clearFiltersBtn').addEventListener('click', () => { filterCoach = ''; filterSupervisor = ''; filterStatus = ''; filterDateFrom = ''; filterDateTo = ''; filterSearch = ''; filterQuestion = ''; page = 0; renderResults(); }); document.getElementById('toggleAllVersions').addEventListener('change', async (e) => { showAllVersions = e.target.checked; page = 0; sortCol = null; document.getElementById('resultsContent').innerHTML = '
'; try { await loadResultsData(); } catch (err) { showToast(err.message, 'error'); } }); document.getElementById('toggleShowMetadata').addEventListener('change', (e) => { showMetadata = e.target.checked; if (!showMetadata && sortCol && META_COL_KEYS.has(sortCol)) { sortCol = null; } renderTableHead(); renderTableRows(); const hint = document.querySelector('#resultsContent .data-toolbar-hint'); if (hint) { hint.textContent = showAllVersions ? `Each row is one uploaded version, sorted by client then version.${showMetadata ? ' Cycle, session, and timestamps are shown per submission.' : ' Turn on metadata to see version, cycle, session, and timestamps.'}` : `Latest response per client.${showMetadata ? ' Cycle, session, and completion times are shown.' : ' Turn on metadata to see cycle, session, and timestamps.'} Each question has its own column.`; } }); document.getElementById('exportCsvLink').addEventListener('click', (e) => { e.preventDefault(); exportCSV(); }); renderTableHead(); renderTableRows(); } function getSourceRows() { return showAllVersions ? allSubmissions : allClients; } const META_COL_KEYS = new Set([ 'version', 'programCycleNumber', 'programSessionNumber', 'startedAt', 'completedAt', 'submittedAt', ]); const ALL_VERSIONS_FIXED_COLUMNS = [ { key: 'clientCode', label: 'Client' }, { key: 'version', label: 'Version' }, { key: 'programCycleNumber', label: 'Cycle' }, { key: 'programSessionNumber', label: 'Session' }, { key: 'coachUsername', label: 'Counselor' }, { key: 'supervisorUsername', label: 'Supervisor' }, { key: 'status', label: 'Status' }, { key: 'sumPoints', label: 'Score' }, { key: 'startedAt', label: 'Started' }, { key: 'completedAt', label: 'Completed' }, { key: 'submittedAt', label: 'Uploaded' }, ]; const CURRENT_FIXED_COLUMNS = [ { key: 'clientCode', label: 'Client' }, { key: 'coachUsername', label: 'Counselor' }, { key: 'supervisorUsername', label: 'Supervisor' }, { key: 'status', label: 'Status' }, { key: 'sumPoints', label: 'Score' }, { key: 'programCycleNumber', label: 'Cycle' }, { key: 'programSessionNumber', label: 'Session' }, { key: 'startedAt', label: 'Started' }, { key: 'completedAt', label: 'Completed' }, ]; function getTableFixedColumns() { const full = showAllVersions ? ALL_VERSIONS_FIXED_COLUMNS : CURRENT_FIXED_COLUMNS; if (showMetadata) return full; return full.filter(col => !META_COL_KEYS.has(col.key)); } function renderTableHead() { const head = document.getElementById('resultsHead'); const fixedCols = getTableFixedColumns(); const allCols = [...fixedCols, ...resultColumns.map(col => ({ key: col.key, label: col.label, title: col.title, }))]; head.innerHTML = allCols.map((col, idx) => { let cls = 'sortable'; if (sortCol === col.key) cls += sortDir === 'asc' ? ' sort-asc' : ' sort-desc'; if (idx === 0) cls += ' sticky-col'; const title = col.title ? ` title="${esc(col.title)}"` : ''; return `${esc(col.label)}`; }).join(''); head.querySelectorAll('th.sortable').forEach(th => { th.addEventListener('click', () => { const key = th.dataset.key; if (sortCol === key) sortDir = sortDir === 'asc' ? 'desc' : 'asc'; else { sortCol = key; sortDir = 'asc'; } page = 0; renderTableHead(); renderTableRows(); }); }); } function scrollToQuestionColumn(query) { if (!query) return; const q = query.toLowerCase(); const th = [...document.querySelectorAll('#resultsHead th')].find(el => { const key = (el.dataset.key || '').toLowerCase(); const text = (el.textContent || '').trim().toLowerCase(); const title = (el.getAttribute('title') || '').toLowerCase(); return text === q || title === q || key.endsWith(q) || text.includes(q); }); if (!th) { showToast(`No column matching "${query}"`, 'error'); return; } th.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); th.style.outline = '2px solid var(--primary)'; setTimeout(() => { th.style.outline = ''; }, 2000); } function getCoachFilterOptions() { const map = new Map(); getSourceRows().forEach(c => { if (!c.coachID || map.has(c.coachID)) return; map.set(c.coachID, c.coachUsername || c.coachID); }); return [...map.entries()].sort((a, b) => a[1].localeCompare(b[1])); } function getSupervisorFilterOptions() { return [...new Set(getSourceRows().map(c => c.supervisorUsername).filter(Boolean))].sort((a, b) => a.localeCompare(b) ); } function coachDisplayName(c) { return c.coachUsername || c.coachID || '—'; } function supervisorDisplayName(c) { return c.supervisorUsername || '—'; } function getFilteredRows() { let rows = [...getSourceRows()]; if (filterSupervisor) rows = rows.filter(c => c.supervisorUsername === filterSupervisor); if (filterCoach) rows = rows.filter(c => c.coachID === filterCoach); if (filterStatus) rows = rows.filter(c => c.status === filterStatus); if (filterDateFrom || filterDateTo) { const fromTs = filterDateFrom ? dateInputToUnixStart(filterDateFrom) : null; const toTs = filterDateTo ? dateInputToUnixEnd(filterDateTo) : null; rows = rows.filter(c => { const ts = c.completedAt; if (!ts) return false; if (fromTs !== null && ts < fromTs) return false; if (toTs !== null && ts > toTs) return false; return true; }); } if (filterSearch) { const q = filterSearch.toLowerCase(); rows = rows.filter(c => { const hay = [ c.clientCode, c.coachUsername, c.supervisorUsername, c.status, showAllVersions ? String(c.version ?? '') : '', c.programCycleNumber != null ? String(c.programCycleNumber) : '', c.programSessionNumber != null ? String(c.programSessionNumber) : '', c.programSessionID, ].filter(Boolean).join(' ').toLowerCase(); return hay.includes(q); }); } if (showAllVersions) { rows.sort((a, b) => { const cc = a.clientCode.localeCompare(b.clientCode); if (cc !== 0) return cc; return (a.version ?? 0) - (b.version ?? 0); }); } return rows; } function renderPagination(totalRows) { const bar = document.getElementById('paginationBar'); if (!bar) return; const totalPages = Math.max(1, Math.ceil(totalRows / PAGE_SIZE)); if (page >= totalPages) page = totalPages - 1; if (page < 0) page = 0; if (totalRows <= PAGE_SIZE) { bar.innerHTML = totalRows ? `${totalRows} row${totalRows === 1 ? '' : 's'}` : ''; return; } const from = page * PAGE_SIZE + 1; const to = Math.min((page + 1) * PAGE_SIZE, totalRows); bar.innerHTML = ` Rows ${from}–${to} of ${totalRows} (page ${page + 1}/${totalPages}) `; document.getElementById('pagePrev')?.addEventListener('click', () => { page--; renderTableRows(); }); document.getElementById('pageNext')?.addEventListener('click', () => { page++; renderTableRows(); }); } function dateInputToUnixStart(yyyyMmDd) { const [y, m, d] = yyyyMmDd.split('-').map(Number); return Math.floor(new Date(y, m - 1, d).getTime() / 1000); } function dateInputToUnixEnd(yyyyMmDd) { const [y, m, d] = yyyyMmDd.split('-').map(Number); return Math.floor(new Date(y, m - 1, d, 23, 59, 59, 999).getTime() / 1000); } function formatUnixDate(ts) { return ts ? new Date(ts * 1000).toLocaleDateString() : '—'; } function fixedColumnCellHtml(c, col, { groupStart = false } = {}) { switch (col.key) { case 'clientCode': return `${esc(c.clientCode)}`; case 'version': return `${c.version ?? '—'}`; case 'programCycleNumber': return `${c.programCycleNumber ?? '—'}`; case 'programSessionNumber': return `${c.programSessionNumber ?? '—'}`; case 'coachUsername': return `${esc(coachDisplayName(c))}`; case 'supervisorUsername': return `${esc(supervisorDisplayName(c))}`; case 'status': { const statusBadge = c.status ? `${esc(c.status)}` : '—'; return `${statusBadge}`; } case 'sumPoints': return `${c.sumPoints !== null && c.sumPoints !== undefined ? c.sumPoints : '—'}`; case 'startedAt': return `${formatUnixDate(c.startedAt)}`; case 'completedAt': return `${formatUnixDate(c.completedAt)}`; case 'submittedAt': return `${formatUnixDate(c.submittedAt)}`; default: return '—'; } } function renderTableRows() { const body = document.getElementById('resultsBody'); let rows = getFilteredRows(); // Sort (preserve client grouping when sorting by client in all-versions mode) if (sortCol) { rows = [...rows].sort((a, b) => { let va = getCellValue(a, sortCol); let vb = getCellValue(b, sortCol); if (va == null) va = ''; if (vb == null) vb = ''; if (typeof va === 'number' && typeof vb === 'number') { return sortDir === 'asc' ? va - vb : vb - va; } const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true }); return sortDir === 'asc' ? cmp : -cmp; }); } const rowLabel = showAllVersions ? 'submission' : 'client'; document.getElementById('resultCount').textContent = `${rows.length} ${rowLabel}${rows.length === 1 ? '' : 's'}`; renderPagination(rows.length); if (!rows.length) { body.innerHTML = `No results found`; return; } const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE)); if (page >= totalPages) page = totalPages - 1; const pageRows = rows.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); const optionMap = {}; questionsDef.forEach(q => { (q.answerOptions || []).forEach(o => { optionMap[o.answerOptionID] = o.defaultText; }); }); const fixedCols = getTableFixedColumns(); let prevClientCode = null; body.innerHTML = pageRows.map(c => { const groupStart = showAllVersions && c.clientCode !== prevClientCode; prevClientCode = c.clientCode; const questionCells = resultColumns.map(col => { const ans = c.answers && c.answers[col.questionID]; const val = resultColumnCellValue(col, ans, optionMap); if (val == null || val === '') return '—'; return `${esc(String(val))}`; }).join(''); const fixedCells = fixedCols.map(col => fixedColumnCellHtml(c, col, { groupStart })).join(''); return `${fixedCells}${questionCells}`; }).join(''); } function getCellValue(client, key) { if (key === 'clientCode') return client.clientCode; if (key === 'version') return client.version; if (key === 'coachUsername') return coachDisplayName(client); if (key === 'supervisorUsername') return supervisorDisplayName(client); if (key === 'status') return client.status; if (key === 'sumPoints') return client.sumPoints; if (key === 'programCycleNumber') return client.programCycleNumber; if (key === 'programSessionNumber') return client.programSessionNumber; if (key === 'startedAt') return client.startedAt; if (key === 'completedAt') return client.completedAt; if (key === 'submittedAt') return client.submittedAt; const col = resultColumns.find(c => c.key === key); if (col) { const ans = client.answers && client.answers[col.questionID]; const optionMap = {}; questionsDef.forEach(q => { (q.answerOptions || []).forEach(o => { optionMap[o.answerOptionID] = o.defaultText; }); }); return resultColumnCellValue(col, ans, optionMap); } return null; } async function exportCSV() { const versionParam = showAllVersions ? '&allVersions=1' : ''; const btn = document.getElementById('exportCsvLink'); const prev = btn.textContent; btn.disabled = true; btn.textContent = 'Downloading…'; try { const res = await apiDownloadFetch(apiUrl(`export?questionnaireID=${encodeURIComponent(questionnaireId)}${versionParam}`)); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error?.message || err.error || 'Export failed'); } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const stamp = new Date().toISOString().slice(0, 10); a.download = showAllVersions ? `${questionnaireMeta.name}_all_versions_${stamp}.csv` : `${questionnaireMeta.name}_v${questionnaireMeta.version}_results.csv`; a.click(); URL.revokeObjectURL(url); showToast('CSV exported', 'success'); } catch (e) { showToast(e.message, 'error'); } finally { btn.disabled = false; btn.textContent = prev; } } function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }