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 = `
Filter:
All versions (grouped by client)
Show metadata
All supervisors
${supervisorOptions.map(name => `${esc(name)} `).join('')}
All counselors
${coachOptions.map(([id, label]) => `${esc(label)} `).join('')}
All statuses
${statuses.map(s => `${esc(s)} `).join('')}
Completed from
to
Go to column
Clear filters
Export CSV
${showAllVersions
? `Each row is one uploaded version, sorted by client then version.${showMetadata ? ' Metadata includes version, cycle, session, start/end/upload times, and submission details.' : ' Turn on metadata to see version, cycle, session, and timestamps.'}`
: `Latest response per client.${showMetadata ? ' Metadata includes cycle, session, and start/end times.' : ' 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 ? ' Metadata includes version, cycle, session, start/end/upload times, and submission details.' : ' Turn on metadata to see version, cycle, session, and timestamps.'}`
: `Latest response per client.${showMetadata ? ' Metadata includes cycle, session, and start/end times.' : ' 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',
'programSessionID',
'startedAt',
'completedAt',
'submittedAt',
'submissionID',
'structureRevision',
'submittedByRole',
'submittedBy',
]);
const IDENTITY_FIXED_COLUMNS = [
{ key: 'coachUsername', label: 'Counselor' },
{ key: 'supervisorUsername', label: 'Supervisor' },
{ key: 'status', label: 'Status' },
{ key: 'sumPoints', label: 'Score' },
];
const META_COLUMNS_ALL_VERSIONS = [
{ key: 'version', label: 'Version' },
{ key: 'programCycleNumber', label: 'Cycle' },
{ key: 'programSessionNumber', label: 'Session' },
{ key: 'programSessionID', label: 'Session ID' },
{ key: 'startedAt', label: 'Started' },
{ key: 'completedAt', label: 'Completed' },
{ key: 'submittedAt', label: 'Uploaded' },
{ key: 'submissionID', label: 'Submission ID' },
{ key: 'structureRevision', label: 'Structure rev.' },
{ key: 'submittedByRole', label: 'Submitted by role' },
{ key: 'submittedBy', label: 'Submitted by' },
];
const META_COLUMNS_CURRENT = [
{ key: 'programCycleNumber', label: 'Cycle' },
{ key: 'programSessionNumber', label: 'Session' },
{ key: 'programSessionID', label: 'Session ID' },
{ key: 'startedAt', label: 'Started' },
{ key: 'completedAt', label: 'Completed' },
];
function getTableFixedColumns() {
const clientCol = [{ key: 'clientCode', label: 'Client' }];
if (!showMetadata) {
return [...clientCol, ...IDENTITY_FIXED_COLUMNS];
}
const meta = showAllVersions ? META_COLUMNS_ALL_VERSIONS : META_COLUMNS_CURRENT;
return [...clientCol, ...meta, ...IDENTITY_FIXED_COLUMNS];
}
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 = `
Prev
Rows ${from}–${to} of ${totalRows} (page ${page + 1}/${totalPages})
= totalPages - 1 ? 'disabled' : ''}>Next
`;
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 formatUnixDateTime(ts) {
if (!ts) return '—';
return new Date(ts * 1000).toLocaleString(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
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 'programSessionID':
return `${c.programSessionID ? esc(c.programSessionID) : '—'} `;
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 `${formatUnixDateTime(c.startedAt)} `;
case 'completedAt':
return `${formatUnixDateTime(c.completedAt)} `;
case 'submittedAt':
return `${formatUnixDateTime(c.submittedAt)} `;
case 'submissionID':
return `${c.submissionID ? esc(c.submissionID) : '—'} `;
case 'structureRevision':
return `${c.structureRevision ?? '—'} `;
case 'submittedByRole':
return `${c.submittedByRole ? esc(c.submittedByRole) : '—'} `;
case 'submittedBy':
return `${c.submittedBy ? esc(c.submittedBy) : '—'} `;
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 === 'programSessionID') return client.programSessionID;
if (key === 'startedAt') return client.startedAt;
if (key === 'completedAt') return client.completedAt;
if (key === 'submittedAt') return client.submittedAt;
if (key === 'submissionID') return client.submissionID;
if (key === 'structureRevision') return client.structureRevision;
if (key === 'submittedByRole') return client.submittedByRole;
if (key === 'submittedBy') return client.submittedBy;
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;
}