import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
import { canEdit, showToast, getRole } from '../app.js';
import { navigate } from '../router.js';
export async function dashboardPage() {
const app = document.getElementById('app');
app.innerHTML = `
`;
if (canEdit()) {
document.getElementById('headerActions').innerHTML = `
Import questionnaires
+ New Questionnaire
`;
bindImportBundle();
}
try {
const data = await apiGet('questionnaires.php');
const categoryKeys = data.categoryKeys || [];
renderGrid(data.questionnaires || [], categoryKeys);
renderCategoryKeysPanel(categoryKeys);
bindCategoryLabelEditors();
} catch (e) {
showToast(e.message, 'error');
document.getElementById('qGrid').innerHTML = `${e.message}
`;
}
}
function renderCategoryKeysPanel(categoryKeys) {
const panel = document.getElementById('categoryKeysPanel');
if (!panel) return;
if (!canEdit()) {
panel.innerHTML = '';
return;
}
if (!categoryKeys.length) {
panel.innerHTML = `
Category keys
Set a category key on a questionnaire (editor → settings) to group it on the app opening screen.
German group titles can be edited below once a key is in use.
`;
return;
}
panel.innerHTML = `
Category keys
German labels for the app opening screen. Other languages: Translations .
Delete removes the key from questionnaires, all translations, and catalog defaults.
`;
panel.querySelectorAll('.delete-cat-key-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const key = btn.dataset.key;
const count = parseInt(btn.dataset.count || '0', 10);
let msg = `Delete category key "${key}"?\n\nThis removes all translations and catalog defaults for questionnaire_group_${key}.`;
if (count > 0) {
msg += `\n\n${count} questionnaire(s) will have their category key cleared.`;
}
if (!confirm(msg)) return;
btn.disabled = true;
try {
const result = await apiPost('questionnaires.php', { action: 'deleteCategoryKey', categoryKey: key });
const cleared = result.questionnairesCleared ?? count;
showToast(
cleared
? `Deleted "${key}" and cleared ${cleared} questionnaire(s)`
: `Deleted "${key}"`,
'success'
);
await dashboardPage();
} catch (err) {
showToast(err.message, 'error');
btn.disabled = false;
}
});
});
}
function categoryGermanInputHTML(categoryKey, label) {
if (!canEdit() || !categoryKey) {
return esc(label);
}
return ` `;
}
function bindCategoryLabelEditors() {
if (!canEdit()) return;
document.querySelectorAll('.category-german-input').forEach(input => {
input.dataset.lastSaved = input.value.trim();
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
input.blur();
}
});
input.addEventListener('blur', () => saveCategoryGermanLabel(input));
});
}
async function saveCategoryGermanLabel(input) {
const categoryKey = input.dataset.categoryKey?.trim();
const label = input.value.trim();
if (!categoryKey) return;
const prev = input.dataset.lastSaved ?? '';
if (label === prev) return;
if (!label) {
showToast('German label cannot be empty', 'error');
input.value = prev;
return;
}
input.disabled = true;
try {
await apiPost('questionnaires.php', {
action: 'updateCategoryLabel',
categoryKey,
germanLabel: label,
});
input.dataset.lastSaved = label;
syncCategoryGermanInputs(categoryKey, label);
showToast('Category label saved', 'success');
} catch (err) {
showToast(err.message, 'error');
input.value = prev;
} finally {
input.disabled = false;
}
}
function syncCategoryGermanInputs(categoryKey, label) {
document.querySelectorAll('.category-german-input').forEach(el => {
if (el.dataset.categoryKey === categoryKey) {
el.value = label;
el.dataset.lastSaved = label;
}
});
}
const UNCATEGORIZED = '__uncategorized__';
function groupQuestionnairesByCategory(questionnaires, categoryKeys) {
const labelByKey = Object.fromEntries(
(categoryKeys || [])
.filter(r => !r.orphaned)
.map(r => [r.categoryKey, r.germanLabel || r.categoryKey])
);
const sorted = [...questionnaires].sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0));
const buckets = new Map();
for (const q of sorted) {
const key = (q.categoryKey || '').trim() || UNCATEGORIZED;
if (!buckets.has(key)) buckets.set(key, []);
buckets.get(key).push(q);
}
const sections = [...buckets.entries()].map(([key, items]) => ({
categoryKey: key === UNCATEGORIZED ? '' : key,
title: key === UNCATEGORIZED ? 'Uncategorized' : (labelByKey[key] || key),
items,
sortKey: Math.min(...items.map(q => q.orderIndex ?? 0)),
}));
sections.sort((a, b) => a.sortKey - b.sortKey);
return sections;
}
function renderQuestionnaireCard(q) {
const stateClass = `badge-${(q.state || 'draft').toLowerCase().replace(/\s+/g, '_')}`;
const showPts = parseInt(q.showPoints || 0, 10);
return `
v${esc(q.version || '—')}
${q.questionCount || 0} question${q.questionCount == 1 ? '' : 's'}
#${q.orderIndex ?? '—'}
${showPts ? 'pts ' : ''}
`;
}
function renderGrid(questionnaires, categoryKeys = []) {
const grid = document.getElementById('qGrid');
if (!questionnaires.length) {
grid.innerHTML = `
No questionnaires yet
Create your first questionnaire to get started.
`;
return;
}
const sections = groupQuestionnairesByCategory(questionnaires, categoryKeys);
grid.innerHTML = `
${sections.map(section => `
${categoryGermanInputHTML(section.categoryKey, section.title)}
${section.items.map(q => renderQuestionnaireCard(q)).join('')}
`).join('')}
`;
grid.querySelectorAll('.q-card').forEach(card => {
card.addEventListener('click', (e) => {
if (e.target.closest('.q-card-actions') || e.target.closest('.drag-handle')) return;
navigate(`#/questionnaire/${card.dataset.id}`);
});
});
grid.querySelectorAll('.delete-q-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
if (!confirm('Delete this questionnaire and all its data?')) return;
try {
await apiDelete('questionnaires.php', { questionnaireID: btn.dataset.id });
showToast('Questionnaire deleted', 'success');
btn.closest('.q-card').remove();
} catch (err) {
showToast(err.message, 'error');
}
});
});
if (canEdit()) initGridDrag(questionnaires);
}
function collectDashboardCardOrder() {
const ids = [];
document.querySelectorAll('.q-category-group').forEach(section => {
section.querySelectorAll('.q-category-grid .q-card').forEach(card => {
ids.push(card.dataset.id);
});
});
return ids;
}
function initGridDrag(questionnaires) {
const root = document.getElementById('qGrid');
if (!root) return;
let dragItem = null;
let dragRow = null;
root.addEventListener('dragstart', (e) => {
const card = e.target.closest('.q-card');
if (!card || !e.target.closest('.drag-handle')) { e.preventDefault(); return; }
dragItem = card;
dragRow = card.closest('.q-category-grid');
card.style.opacity = '0.5';
e.dataTransfer.effectAllowed = 'move';
});
root.addEventListener('dragover', (e) => {
e.preventDefault();
if (!dragItem || !dragRow) return;
const row = e.target.closest('.q-category-grid');
if (!row || row !== dragRow) return;
const cards = [...row.querySelectorAll('.q-card')].filter(c => c !== dragItem);
let insertBefore = null;
for (const child of cards) {
const box = child.getBoundingClientRect();
if (e.clientX < box.left + box.width / 2) {
insertBefore = child;
break;
}
}
if (insertBefore) row.insertBefore(dragItem, insertBefore);
else row.appendChild(dragItem);
});
root.addEventListener('dragend', async () => {
if (!dragItem) return;
dragItem.style.opacity = '';
const newOrder = collectDashboardCardOrder();
dragItem = null;
dragRow = null;
for (let i = 0; i < newOrder.length; i++) {
const id = newOrder[i];
const q = questionnaires.find(x => x.questionnaireID === id);
if (q && parseInt(q.orderIndex, 10) !== i) {
try {
await apiPut('questionnaires.php', { questionnaireID: id, orderIndex: i });
q.orderIndex = i;
} catch (err) {
showToast(err.message, 'error');
return;
}
}
}
showToast('Order saved', 'success');
});
}
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 ?? '';
return d.innerHTML;
}