added quesionnaire export and import

This commit is contained in:
2026-05-22 17:36:40 +02:00
parent be01b85569
commit 244fd16140
8 changed files with 523 additions and 18 deletions

View File

@ -1,4 +1,4 @@
import { apiGet, apiPut, apiDelete } from '../api.js';
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
import { canEdit, showToast, getRole } from '../app.js';
import { navigate } from '../router.js';
@ -14,8 +14,11 @@ export async function dashboardPage() {
if (canEdit()) {
document.getElementById('headerActions').innerHTML = `
<input type="file" id="importBundleFile" accept=".json,application/json" hidden>
<button type="button" class="btn" id="importBundleBtn">Import questionnaires</button>
<a href="#/questionnaire/new" class="btn btn-primary">+ New Questionnaire</a>
`;
bindImportBundle();
}
try {
@ -143,6 +146,61 @@ function initGridDrag(questionnaires) {
});
}
function bindImportBundle() {
const fileInput = document.getElementById('importBundleFile');
const btn = document.getElementById('importBundleBtn');
if (!fileInput || !btn) return;
btn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', async () => {
const file = fileInput.files?.[0];
fileInput.value = '';
if (!file) return;
let bundle;
try {
const text = await file.text();
bundle = JSON.parse(text);
} catch {
showToast('Invalid JSON file', 'error');
return;
}
if (!bundle?.questionnaires || !Array.isArray(bundle.questionnaires)) {
showToast('Not a questionnaire bundle (missing questionnaires array)', 'error');
return;
}
const count = bundle.questionnaires.length;
if (!confirm(`Import ${count} questionnaire(s) from this file?`)) return;
const replace = confirm(
'If a questionnaire ID from the file already exists:\n\n' +
'OK — replace that questionnaire\n' +
'Cancel — import as a new copy (new ID)'
);
btn.disabled = true;
const prev = btn.textContent;
btn.textContent = 'Importing…';
try {
const result = await apiPost('questionnaires.php', {
action: 'importBundle',
bundle,
replaceIfExists: replace,
});
const n = result.imported ?? count;
showToast(`Imported ${n} questionnaire(s)`, 'success');
await dashboardPage();
} catch (e) {
showToast(e.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = prev;
}
});
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s ?? '';

View File

@ -1,5 +1,5 @@
import { apiGet } from '../api.js';
import { showToast } from '../app.js';
import { canEdit, showToast } from '../app.js';
export async function exportPage() {
const app = document.getElementById('app');
@ -33,7 +33,19 @@ function renderExport(questionnaires) {
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}
<p style="margin-bottom:16px;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>
@ -66,6 +78,37 @@ function renderExport(questionnaires) {
</table>
`;
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;
}
});
container.querySelectorAll('.export-btn').forEach(btn => {
btn.addEventListener('click', async () => {
btn.disabled = true;

View File

@ -25,15 +25,24 @@ export function germanFor(entry) {
return entry.germanText || normalizeTranslations(entry.translations)[SOURCE_LANG] || entry.key || '';
}
/** Key column label (short id for questions/options; catalog key for UI strings). */
const HEX_ID_RE = /^[a-f0-9]{32}$/i;
/** Key column label (short id or readable preview — not the app lookup key). */
export function entryDisplayKey(entry) {
if (entry.displayKey) return entry.displayKey;
const dk = entry.displayKey || '';
if (dk && !HEX_ID_RE.test(dk)) return dk;
if (entry.type === 'question' || entry.type === 'answer_option') {
const key = (entry.key || '').trim();
if (key) {
return key.length > 56 ? `${key.slice(0, 56)}` : key;
}
}
if (entry.type === 'question' || entry.type === 'answer_option') {
const id = entry.entityId || '';
const sep = id.lastIndexOf('__');
return sep >= 0 ? id.slice(sep + 2) : id;
if (sep >= 0) return id.slice(sep + 2);
}
return entry.key || '';
return dk || entry.key || '';
}
export function translationFor(entry, lang) {