added quesionnaire export and import
This commit is contained in:
@ -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 ?? '';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user