import { apiPost } from '../api.js';
import { getRole, showToast } from '../app.js';
import { navigate } from '../router.js';
export function devPage() {
if (getRole() !== 'admin') {
navigate('#/');
return;
}
const app = document.getElementById('app');
app.innerHTML = `
Export all response data (ZIP)
Download one CSV per questionnaire in a single ZIP (full server data).
Current data uses live answers;
All versions includes every upload (one row per submission).
Export all — current data (ZIP)
Export all — all versions (ZIP)
Dev import
Local / test only. Imports prefixed dev users (dev_*),
clients (DEV-CL-*), and completed questionnaire runs.
Existing real users are not modified. Conflicts are skipped (usernames, client codes, completions).
Default password: socialvrlab. Questionnaires must already exist in the database.
Fixture JSON
Use dev-test-fixture.json from the repo root (generate via nat-as-server/scripts/generate_dev_fixture.py; uneven coach load, multi-version uploads, natural timestamps).
Import fixture
Remove test data
Reset database
Destructive. Deletes every client, completion, coach, and supervisor
(including non-dev accounts). Admin logins and questionnaire definitions are kept.
Remove all data (keep admins)
`;
let parsedFixture = null;
document.getElementById('devFixtureFile').addEventListener('change', async (e) => {
const file = e.target.files?.[0];
const summary = document.getElementById('devFixtureSummary');
const btn = document.getElementById('devImportBtn');
parsedFixture = null;
btn.disabled = true;
summary.style.display = 'none';
if (!file) return;
try {
const text = await file.text();
parsedFixture = JSON.parse(text);
const st = parsedFixture.stats || {};
summary.innerHTML = `
Preview:
${parsedFixture.admins?.length ?? st.admins ?? 0} admins,
${parsedFixture.supervisors?.length ?? st.supervisors ?? 0} supervisors,
${parsedFixture.coaches?.length ?? st.coaches ?? 0} coaches,
${parsedFixture.clients?.length ?? st.clients ?? 0} clients,
${parsedFixture.completions?.length ?? st.completionRecords ?? st.completions ?? 0} completions,
${st.totalUploads ?? '—'} uploads (${st.multiVersionPairs ?? '—'} re-uploaded)
`;
summary.style.display = '';
btn.disabled = false;
} catch (err) {
showToast('Invalid JSON: ' + err.message, 'error');
}
});
document.getElementById('devRemoveBtn').addEventListener('click', async () => {
if (!confirm(
'Remove all dev test data?\n\n'
+ 'Deletes users matching dev_*, clients DEV-CL-*, and their answers/completions.\n'
+ 'Real accounts are not affected.'
)) {
return;
}
const btn = document.getElementById('devRemoveBtn');
const pre = document.getElementById('devImportResult');
btn.disabled = true;
btn.textContent = 'Removing…';
pre.style.display = 'none';
try {
const res = await apiPost('dev', {
action: 'remove',
usernamePrefix: 'dev_',
clientCodePrefix: 'DEV-CL-',
});
if (!res.success) {
showToast(res.error || res.message || 'Remove failed', 'error');
return;
}
pre.textContent = JSON.stringify(res, null, 2);
pre.style.display = '';
const d = res.deleted || {};
showToast(
`Removed ${d.clients ?? 0} clients, ${d.users ?? 0} users, `
+ `${d.completed_questionnaires ?? 0} completions`,
'success'
);
} catch (e) {
showToast(e.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Remove test data';
}
});
document.getElementById('devWipeBtn').addEventListener('click', async () => {
if (!confirm(
'Remove ALL clients, completions, coaches, and supervisors?\n\n'
+ 'Admin accounts and questionnaires will NOT be deleted.\n'
+ 'This cannot be undone.'
)) {
return;
}
const btn = document.getElementById('devWipeBtn');
const pre = document.getElementById('devImportResult');
btn.disabled = true;
btn.textContent = 'Removing…';
pre.style.display = 'none';
try {
const res = await apiPost('dev', { action: 'wipeExceptAdmins' });
if (!res.success) {
showToast(res.error || res.message || 'Wipe failed', 'error');
return;
}
pre.textContent = JSON.stringify(res, null, 2);
pre.style.display = '';
const d = res.deleted || {};
const kept = res.kept?.admins ?? '?';
showToast(
`Wiped ${d.clients ?? 0} clients, ${d.users ?? 0} users, `
+ `${d.completed_questionnaires ?? 0} completions (${kept} admins kept)`,
'success'
);
} catch (e) {
showToast(e.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Remove all data (keep admins)';
}
});
wireAdminZipExport();
document.getElementById('devImportBtn').addEventListener('click', async () => {
if (!parsedFixture) return;
const btn = document.getElementById('devImportBtn');
const pre = document.getElementById('devImportResult');
btn.disabled = true;
btn.textContent = 'Importing…';
pre.style.display = 'none';
try {
const res = await apiPost('dev', { fixture: parsedFixture });
if (!res.success) {
showToast(res.error || 'Import failed', 'error');
return;
}
pre.textContent = JSON.stringify(res, null, 2);
pre.style.display = '';
const imp = res.imported || {};
const sk = res.skipped || {};
showToast(
`Imported: ${imp.completions ?? 0} completions, ${imp.submissions ?? 0} uploads, `
+ `${imp.clients ?? 0} clients, ${imp.coaches ?? 0} coaches `
+ `(skipped ${sk.completions ?? 0} existing)`,
'success'
);
if (res.errors?.length) {
showToast(`${res.errors.length} warning(s) — see log below`, 'error');
}
} catch (e) {
showToast(e.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Import fixture';
}
});
}
function filenameFromContentDisposition(res, fallback) {
const disp = res.headers.get('Content-Disposition');
if (!disp) return fallback;
const m = /filename="([^"]+)"/i.exec(disp);
return m ? m[1] : fallback;
}
async function downloadExportZip(url, defaultFilename) {
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 = filenameFromContentDisposition(res, defaultFilename);
a.click();
URL.revokeObjectURL(blobUrl);
}
function wireAdminZipExport() {
document.getElementById('exportAllCurrentZipBtn')?.addEventListener('click', async () => {
const btn = document.getElementById('exportAllCurrentZipBtn');
btn.disabled = true;
const prev = btn.textContent;
btn.textContent = 'Preparing ZIP…';
try {
await downloadExportZip('../api/export?exportAll=1', 'responses_export_current.zip');
showToast('ZIP download started (current data)', 'success');
} catch (e) {
showToast(e.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = prev;
}
});
document.getElementById('exportAllVersionsZipBtn')?.addEventListener('click', async () => {
const btn = document.getElementById('exportAllVersionsZipBtn');
btn.disabled = true;
const prev = btn.textContent;
btn.textContent = 'Preparing ZIP…';
try {
await downloadExportZip('../api/export?exportAll=1&allVersions=1', 'responses_export_all_versions.zip');
showToast('ZIP download started (all versions)', 'success');
} catch (e) {
showToast(e.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = prev;
}
});
}