extending results page to show metadata and group by clientid to improve tracking progress
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
@ -48,7 +48,7 @@ function renderExport() {
|
||||
${bundleBlock}
|
||||
<div class="card">
|
||||
<p style="margin:0 0 12px;color:var(--text-secondary)">
|
||||
Select a questionnaire and click Export to download a CSV file with all client responses (filtered by your role's visibility).
|
||||
Download CSV files with client responses (filtered by your role). Use <strong>All versions</strong> for every upload per client, including cycle, session, and start/finish times.
|
||||
</p>
|
||||
<div class="filter-bar">
|
||||
<input type="search" id="exportListSearch" class="filter-search" placeholder="Search questionnaire name…" value="${esc(filterSearch)}">
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { apiGet } from '../api.js';
|
||||
import { apiGet, apiUrl, apiDownloadFetch } from '../api.js';
|
||||
import { homeNavButton, showToast } from '../app.js';
|
||||
import { buildResultColumns, resultColumnCellValue } from '../test-data.js';
|
||||
|
||||
@ -7,6 +7,10 @@ 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;
|
||||
@ -31,6 +35,9 @@ export async function resultsPage(params) {
|
||||
filterSearch = '';
|
||||
filterQuestion = '';
|
||||
page = 0;
|
||||
showAllVersions = true;
|
||||
showMetadata = true;
|
||||
questionnaireId = params.id;
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
@ -49,18 +56,29 @@ export async function resultsPage(params) {
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await apiGet(`results.php?questionnaireID=${encodeURIComponent(params.id)}`);
|
||||
questionnaireMeta = data.questionnaire;
|
||||
questionsDef = data.questions || [];
|
||||
allClients = data.clients || [];
|
||||
document.getElementById('resultsTitle').textContent = `Results: ${questionnaireMeta.name} v${questionnaireMeta.version}`;
|
||||
renderResults();
|
||||
await loadResultsData();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('resultsContent').innerHTML = `<p class="error-text">${esc(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@ -68,12 +86,20 @@ function renderResults() {
|
||||
// Collect unique coaches and statuses for filters
|
||||
const coachOptions = getCoachFilterOptions();
|
||||
const supervisorOptions = getSupervisorFilterOptions();
|
||||
const statuses = [...new Set(allClients.map(c => c.status))].filter(Boolean).sort();
|
||||
const statuses = [...new Set(getSourceRows().map(c => c.status))].filter(Boolean).sort();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="filter-bar">
|
||||
<label style="font-size:.85rem;font-weight:500">Filter:</label>
|
||||
<label style="font-size:.85rem;display:flex;align-items:center;gap:6px">
|
||||
<input type="checkbox" id="toggleAllVersions" ${showAllVersions ? 'checked' : ''}>
|
||||
All versions (grouped by client)
|
||||
</label>
|
||||
<label style="font-size:.85rem;display:flex;align-items:center;gap:6px">
|
||||
<input type="checkbox" id="toggleShowMetadata" ${showMetadata ? 'checked' : ''}>
|
||||
Show metadata
|
||||
</label>
|
||||
<select id="filterSupervisor">
|
||||
<option value="">All supervisors</option>
|
||||
${supervisorOptions.map(name => `<option value="${esc(name)}" ${filterSupervisor === name ? 'selected' : ''}>${esc(name)}</option>`).join('')}
|
||||
@ -100,9 +126,13 @@ function renderResults() {
|
||||
<button type="button" class="btn btn-sm" id="gotoQuestionBtn">Go to column</button>
|
||||
<button type="button" class="btn btn-sm" id="clearFiltersBtn">Clear filters</button>
|
||||
<span style="margin-left:auto;font-size:.85rem;color:var(--text-secondary)" id="resultCount"></span>
|
||||
<a id="exportCsvLink" class="btn btn-sm" href="#">Export CSV</a>
|
||||
<button type="button" class="btn btn-sm" id="exportCsvLink">Export CSV</button>
|
||||
</div>
|
||||
<p class="data-toolbar-hint" style="margin-top:8px">Each question has its own column; glass-scale symptoms are separate columns. Hover headers for context.</p>
|
||||
<p class="data-toolbar-hint" style="margin-top:8px">
|
||||
${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.`}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="table-wrapper" id="resultsTableWrap">
|
||||
@ -172,6 +202,31 @@ function renderResults() {
|
||||
page = 0;
|
||||
renderResults();
|
||||
});
|
||||
document.getElementById('toggleAllVersions').addEventListener('change', async (e) => {
|
||||
showAllVersions = e.target.checked;
|
||||
page = 0;
|
||||
sortCol = null;
|
||||
document.getElementById('resultsContent').innerHTML = '<div class="spinner"></div>';
|
||||
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();
|
||||
@ -181,16 +236,54 @@ function renderResults() {
|
||||
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 = [
|
||||
{ key: 'clientCode', label: 'Client' },
|
||||
{ key: 'coachUsername', label: 'Counselor' },
|
||||
{ key: 'supervisorUsername', label: 'Supervisor' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'sumPoints', label: 'Score' },
|
||||
{ key: 'completedAt', label: 'Completed' },
|
||||
];
|
||||
const fixedCols = getTableFixedColumns();
|
||||
const allCols = [...fixedCols, ...resultColumns.map(col => ({
|
||||
key: col.key,
|
||||
label: col.label,
|
||||
@ -237,7 +330,7 @@ function scrollToQuestionColumn(query) {
|
||||
|
||||
function getCoachFilterOptions() {
|
||||
const map = new Map();
|
||||
allClients.forEach(c => {
|
||||
getSourceRows().forEach(c => {
|
||||
if (!c.coachID || map.has(c.coachID)) return;
|
||||
map.set(c.coachID, c.coachUsername || c.coachID);
|
||||
});
|
||||
@ -245,7 +338,7 @@ function getCoachFilterOptions() {
|
||||
}
|
||||
|
||||
function getSupervisorFilterOptions() {
|
||||
return [...new Set(allClients.map(c => c.supervisorUsername).filter(Boolean))].sort((a, b) =>
|
||||
return [...new Set(getSourceRows().map(c => c.supervisorUsername).filter(Boolean))].sort((a, b) =>
|
||||
a.localeCompare(b)
|
||||
);
|
||||
}
|
||||
@ -258,15 +351,15 @@ function supervisorDisplayName(c) {
|
||||
return c.supervisorUsername || '—';
|
||||
}
|
||||
|
||||
function getFilteredClients() {
|
||||
let clients = allClients;
|
||||
if (filterSupervisor) clients = clients.filter(c => c.supervisorUsername === filterSupervisor);
|
||||
if (filterCoach) clients = clients.filter(c => c.coachID === filterCoach);
|
||||
if (filterStatus) clients = clients.filter(c => c.status === filterStatus);
|
||||
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;
|
||||
clients = clients.filter(c => {
|
||||
rows = rows.filter(c => {
|
||||
const ts = c.completedAt;
|
||||
if (!ts) return false;
|
||||
if (fromTs !== null && ts < fromTs) return false;
|
||||
@ -276,17 +369,28 @@ function getFilteredClients() {
|
||||
}
|
||||
if (filterSearch) {
|
||||
const q = filterSearch.toLowerCase();
|
||||
clients = clients.filter(c => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
return clients;
|
||||
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) {
|
||||
@ -330,13 +434,50 @@ function dateInputToUnixEnd(yyyyMmDd) {
|
||||
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 `<td class="sticky-col${groupStart ? ' client-group-start' : ''}">${esc(c.clientCode)}</td>`;
|
||||
case 'version':
|
||||
return `<td>${c.version ?? '—'}</td>`;
|
||||
case 'programCycleNumber':
|
||||
return `<td>${c.programCycleNumber ?? '—'}</td>`;
|
||||
case 'programSessionNumber':
|
||||
return `<td>${c.programSessionNumber ?? '—'}</td>`;
|
||||
case 'coachUsername':
|
||||
return `<td>${esc(coachDisplayName(c))}</td>`;
|
||||
case 'supervisorUsername':
|
||||
return `<td>${esc(supervisorDisplayName(c))}</td>`;
|
||||
case 'status': {
|
||||
const statusBadge = c.status
|
||||
? `<span class="badge badge-${c.status.toLowerCase().replace(/\s+/g, '_')}">${esc(c.status)}</span>`
|
||||
: '—';
|
||||
return `<td>${statusBadge}</td>`;
|
||||
}
|
||||
case 'sumPoints':
|
||||
return `<td>${c.sumPoints !== null && c.sumPoints !== undefined ? c.sumPoints : '—'}</td>`;
|
||||
case 'startedAt':
|
||||
return `<td>${formatUnixDate(c.startedAt)}</td>`;
|
||||
case 'completedAt':
|
||||
return `<td>${formatUnixDate(c.completedAt)}</td>`;
|
||||
case 'submittedAt':
|
||||
return `<td>${formatUnixDate(c.submittedAt)}</td>`;
|
||||
default:
|
||||
return '<td>—</td>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTableRows() {
|
||||
const body = document.getElementById('resultsBody');
|
||||
let clients = getFilteredClients();
|
||||
let rows = getFilteredRows();
|
||||
|
||||
// Sort
|
||||
// Sort (preserve client grouping when sorting by client in all-versions mode)
|
||||
if (sortCol) {
|
||||
clients = [...clients].sort((a, b) => {
|
||||
rows = [...rows].sort((a, b) => {
|
||||
let va = getCellValue(a, sortCol);
|
||||
let vb = getCellValue(b, sortCol);
|
||||
if (va == null) va = '';
|
||||
@ -349,20 +490,20 @@ function renderTableRows() {
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('resultCount').textContent = `${clients.length} client${clients.length === 1 ? '' : 's'}`;
|
||||
const rowLabel = showAllVersions ? 'submission' : 'client';
|
||||
document.getElementById('resultCount').textContent = `${rows.length} ${rowLabel}${rows.length === 1 ? '' : 's'}`;
|
||||
|
||||
renderPagination(clients.length);
|
||||
renderPagination(rows.length);
|
||||
|
||||
if (!clients.length) {
|
||||
if (!rows.length) {
|
||||
body.innerHTML = `<tr><td colspan="99" style="text-align:center;color:var(--text-secondary);padding:30px">No results found</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(clients.length / PAGE_SIZE));
|
||||
const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
|
||||
if (page >= totalPages) page = totalPages - 1;
|
||||
clients = clients.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
||||
const pageRows = rows.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
||||
|
||||
// Build option text lookup
|
||||
const optionMap = {};
|
||||
questionsDef.forEach(q => {
|
||||
(q.answerOptions || []).forEach(o => {
|
||||
@ -370,9 +511,11 @@ function renderTableRows() {
|
||||
});
|
||||
});
|
||||
|
||||
body.innerHTML = clients.map(c => {
|
||||
const completedDate = c.completedAt ? new Date(c.completedAt * 1000).toLocaleDateString() : '—';
|
||||
const statusBadge = c.status ? `<span class="badge badge-${c.status.toLowerCase().replace(/\s+/g,'_')}">${esc(c.status)}</span>` : '—';
|
||||
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];
|
||||
@ -381,25 +524,24 @@ function renderTableRows() {
|
||||
return `<td>${esc(String(val))}</td>`;
|
||||
}).join('');
|
||||
|
||||
return `<tr>
|
||||
<td class="sticky-col">${esc(c.clientCode)}</td>
|
||||
<td>${esc(coachDisplayName(c))}</td>
|
||||
<td>${esc(supervisorDisplayName(c))}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${c.sumPoints !== null ? c.sumPoints : '—'}</td>
|
||||
<td>${completedDate}</td>
|
||||
${questionCells}
|
||||
</tr>`;
|
||||
const fixedCells = fixedCols.map(col => fixedColumnCellHtml(c, col, { groupStart })).join('');
|
||||
|
||||
return `<tr class="${groupStart ? 'client-group-row' : ''}">${fixedCells}${questionCells}</tr>`;
|
||||
}).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];
|
||||
@ -414,51 +556,35 @@ function getCellValue(client, key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
const clients = getFilteredClients();
|
||||
const optionMap = {};
|
||||
questionsDef.forEach(q => {
|
||||
(q.answerOptions || []).forEach(o => {
|
||||
optionMap[o.answerOptionID] = o.defaultText;
|
||||
});
|
||||
});
|
||||
|
||||
const headers = ['clientCode', 'Counselor', 'supervisor', 'status', 'sumPoints', 'completedAt',
|
||||
...resultColumns.map(col => col.label)];
|
||||
|
||||
const rows = clients.map(c => {
|
||||
const base = [
|
||||
c.clientCode, coachDisplayName(c), c.supervisorUsername || '', c.status, c.sumPoints ?? '',
|
||||
c.completedAt ? new Date(c.completedAt * 1000).toISOString() : ''
|
||||
];
|
||||
const answerCols = resultColumns.map(col => {
|
||||
const ans = c.answers && c.answers[col.questionID];
|
||||
const val = resultColumnCellValue(col, ans, optionMap);
|
||||
return val != null ? val : '';
|
||||
});
|
||||
return [...base, ...answerCols];
|
||||
});
|
||||
|
||||
let csv = '\uFEFF'; // BOM
|
||||
csv += headers.map(csvEsc).join(',') + '\n';
|
||||
rows.forEach(r => { csv += r.map(csvEsc).join(',') + '\n'; });
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${questionnaireMeta.name}_v${questionnaireMeta.version}_results.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('CSV exported', 'success');
|
||||
}
|
||||
|
||||
function csvEsc(val) {
|
||||
const s = String(val ?? '');
|
||||
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
||||
return '"' + s.replace(/"/g, '""') + '"';
|
||||
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;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
|
||||
Reference in New Issue
Block a user