243 lines
8.8 KiB
JavaScript
243 lines
8.8 KiB
JavaScript
import { apiGet } from '../api.js';
|
|
import { canEdit, showToast } from '../app.js';
|
|
let questionnairesList = [];
|
|
let filterSearch = '';
|
|
|
|
export async function exportPage() {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = `
|
|
<div class="page-header">
|
|
<h1>Export Data</h1>
|
|
<div class="actions"><a href="#/" class="btn">← Dashboard</a></div>
|
|
</div>
|
|
<div class="card" id="exportContent"><div class="spinner"></div></div>
|
|
`;
|
|
|
|
try {
|
|
const data = await apiGet('questionnaires.php');
|
|
questionnairesList = data.questionnaires || [];
|
|
renderExport();
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
document.getElementById('exportContent').innerHTML = `<p class="error-text">${esc(e.message)}</p>`;
|
|
}
|
|
}
|
|
|
|
function renderExport() {
|
|
const container = document.getElementById('exportContent');
|
|
|
|
if (!questionnairesList.length) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<h3>No questionnaires</h3>
|
|
<p>Create questionnaires first to export data.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const bundleBlock = canEdit() ? `
|
|
<div class="card" style="margin-bottom:20px;padding:16px 20px">
|
|
<h3 style="margin:0 0 8px;font-size:1.05rem">Export questionnaires (JSON)</h3>
|
|
<p style="margin:0 0 14px;color:var(--text-secondary);font-size:.9rem">
|
|
Download all questionnaires with questions, answer options, and every translation.
|
|
Use <strong>Import questionnaires</strong> on the dashboard to restore this file on another server.
|
|
</p>
|
|
<button type="button" class="btn btn-primary" id="exportBundleBtn">Export all questionnaires (JSON)</button>
|
|
</div>
|
|
` : '';
|
|
|
|
container.innerHTML = `
|
|
${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).
|
|
</p>
|
|
<div class="filter-bar">
|
|
<input type="search" id="exportListSearch" class="filter-search" placeholder="Search questionnaire name…" value="${esc(filterSearch)}">
|
|
<span class="data-toolbar-hint" id="exportListCount"></span>
|
|
</div>
|
|
<div class="table-wrapper">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Questionnaire</th>
|
|
<th>Version</th>
|
|
<th>State</th>
|
|
<th>Questions</th>
|
|
<th>Completed</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="exportTableBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('exportListSearch').addEventListener('input', (e) => {
|
|
filterSearch = e.target.value;
|
|
renderExportTableBody();
|
|
});
|
|
|
|
renderExportTableBody();
|
|
wireExportActions();
|
|
}
|
|
|
|
function exportRowSearchText(q) {
|
|
return [q.name, q.questionnaireID, q.version, q.state].filter(Boolean).join(' ');
|
|
}
|
|
|
|
function renderExportTableBody() {
|
|
const body = document.getElementById('exportTableBody');
|
|
const countEl = document.getElementById('exportListCount');
|
|
if (!body) return;
|
|
|
|
const q = filterSearch.trim().toLowerCase();
|
|
const filtered = q
|
|
? questionnairesList.filter(item => exportRowSearchText(item).toLowerCase().includes(q))
|
|
: questionnairesList;
|
|
|
|
if (!filtered.length) {
|
|
body.innerHTML = `
|
|
<tr data-filter-placeholder="1">
|
|
<td colspan="6" style="text-align:center;color:var(--text-secondary);padding:24px">
|
|
No questionnaires match
|
|
</td>
|
|
</tr>`;
|
|
if (countEl) {
|
|
countEl.textContent = q
|
|
? `0 of ${questionnairesList.length} questionnaires`
|
|
: '';
|
|
}
|
|
return;
|
|
}
|
|
|
|
body.innerHTML = filtered.map(item => exportRowHTML(item)).join('');
|
|
|
|
if (countEl) {
|
|
countEl.textContent = q
|
|
? `${filtered.length} of ${questionnairesList.length} questionnaires`
|
|
: `${questionnairesList.length} questionnaire${questionnairesList.length === 1 ? '' : 's'}`;
|
|
}
|
|
|
|
body.querySelectorAll('.export-btn').forEach(btn => {
|
|
btn.addEventListener('click', onExportCsvClick);
|
|
});
|
|
body.querySelectorAll('.export-btn-all').forEach(btn => {
|
|
btn.addEventListener('click', onExportAllVersionsClick);
|
|
});
|
|
}
|
|
|
|
function exportRowHTML(q) {
|
|
return `
|
|
<tr>
|
|
<td><strong>${esc(q.name)}</strong></td>
|
|
<td>${esc(q.version || '—')}</td>
|
|
<td><span class="badge badge-${(q.state || 'draft').toLowerCase()}">${esc(q.state || 'draft')}</span></td>
|
|
<td>${q.questionCount || 0}</td>
|
|
<td>${q.completedCount ?? 0}</td>
|
|
<td class="export-actions-cell">
|
|
<button class="btn btn-sm btn-primary export-btn" data-id="${esc(q.questionnaireID)}" data-name="${esc(q.name)}" data-version="${esc(q.version)}" data-all="0">
|
|
Export current
|
|
</button>
|
|
<button class="btn btn-sm export-btn-all" data-id="${esc(q.questionnaireID)}" data-name="${esc(q.name)}">
|
|
All versions
|
|
</button>
|
|
<a href="#/questionnaire/${esc(q.questionnaireID)}/results" class="btn btn-sm">Results</a>
|
|
</td>
|
|
</tr>`;
|
|
}
|
|
|
|
function wireExportActions() {
|
|
document.getElementById('exportBundleBtn')?.addEventListener('click', async () => {
|
|
const btn = document.getElementById('exportBundleBtn');
|
|
btn.disabled = true;
|
|
const prev = btn.textContent;
|
|
btn.textContent = 'Preparing…';
|
|
try {
|
|
const token = localStorage.getItem('qdb_token');
|
|
const res = await fetch('../api/export?bundle=1', {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
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 = `questionnaires_bundle_${stamp}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
showToast('Questionnaire bundle downloaded', 'success');
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = prev;
|
|
}
|
|
});
|
|
}
|
|
|
|
async function downloadExportCsv(btn, url, filename) {
|
|
const token = localStorage.getItem('qdb_token');
|
|
const res = await fetch(url, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: 'Download failed' }));
|
|
throw new Error(err.error?.message || err.error || 'Download failed');
|
|
}
|
|
const blob = await res.blob();
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = blobUrl;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(blobUrl);
|
|
}
|
|
|
|
async function onExportCsvClick(ev) {
|
|
const btn = ev.currentTarget;
|
|
btn.disabled = true;
|
|
const prev = btn.textContent;
|
|
btn.textContent = 'Downloading…';
|
|
try {
|
|
const url = `../api/export?questionnaireID=${encodeURIComponent(btn.dataset.id)}`;
|
|
await downloadExportCsv(btn, url, `${btn.dataset.name}_v${btn.dataset.version}.csv`);
|
|
showToast('Download started', 'success');
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = prev;
|
|
}
|
|
}
|
|
|
|
async function onExportAllVersionsClick(ev) {
|
|
const btn = ev.currentTarget;
|
|
btn.disabled = true;
|
|
const prev = btn.textContent;
|
|
btn.textContent = 'Downloading…';
|
|
try {
|
|
const url = `../api/export?questionnaireID=${encodeURIComponent(btn.dataset.id)}&allVersions=1`;
|
|
const stamp = new Date().toISOString().slice(0, 10);
|
|
await downloadExportCsv(btn, url, `${btn.dataset.name}_all_versions_${stamp}.csv`);
|
|
showToast('All versions download started', '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;
|
|
}
|