enhanced questionnaire version handling and handling uploads from outdated questionnaires through automatic revisioning and checks
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
@ -43,6 +43,7 @@ const GLASS_SCORE_LEVELS = [
|
||||
|
||||
let questionnaire = null;
|
||||
let questions = [];
|
||||
let retiredQuestions = [];
|
||||
let isNew = false;
|
||||
let activeTab = 'questions';
|
||||
let allQuestionnaires = [];
|
||||
@ -86,14 +87,19 @@ export async function editorPage(params) {
|
||||
renderEditor();
|
||||
} else {
|
||||
try {
|
||||
const quData = await apiGet(`questions.php?questionnaireID=${encodeURIComponent(params.id)}`);
|
||||
const quData = await apiGet(
|
||||
`questions.php?questionnaireID=${encodeURIComponent(params.id)}&includeRetired=1`
|
||||
);
|
||||
questionnaire = allQuestionnaires.find(q => q.questionnaireID === params.id);
|
||||
if (!questionnaire) {
|
||||
showToast('Questionnaire not found', 'error');
|
||||
navigate('#/questionnaires');
|
||||
return;
|
||||
}
|
||||
questions = quData.questions || [];
|
||||
questionnaire.structureRevision = quData.structureRevision ?? questionnaire.structureRevision ?? 1;
|
||||
const allQs = quData.questions || [];
|
||||
questions = allQs.filter(q => !q.retired);
|
||||
retiredQuestions = allQs.filter(q => q.retired);
|
||||
document.getElementById('editorTitle').textContent = questionnaire.name;
|
||||
renderEditor();
|
||||
} catch (e) {
|
||||
@ -772,6 +778,11 @@ function renderEditor() {
|
||||
<label>Version</label>
|
||||
<input type="text" id="metaVersion" value="${esc(questionnaire.version)}" ${editable ? '' : 'disabled'}>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Structure revision</label>
|
||||
<input type="text" value="${esc(String(questionnaire.structureRevision ?? 1))}" disabled
|
||||
title="Auto-incremented when questions or options change structurally">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>State</label>
|
||||
<select id="metaState" ${editable ? '' : 'disabled'}>
|
||||
@ -809,6 +820,13 @@ function renderEditor() {
|
||||
</div>
|
||||
<div id="addQuestionFormWrapper" style="display:none"></div>
|
||||
<ul class="question-list" id="questionList"></ul>
|
||||
<div id="retiredQuestionsSection" style="display:none;margin-top:20px">
|
||||
<details class="retired-questions-panel">
|
||||
<summary>Retired questions (<span id="retiredCount">0</span>)</summary>
|
||||
<p class="field-hint">Retired questions stay in upload history; counselors on older forms can still submit.</p>
|
||||
<ul class="question-list retired-question-list" id="retiredQuestionList"></ul>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tabContent-translations" style="display:none">
|
||||
@ -830,6 +848,7 @@ function renderEditor() {
|
||||
if (!isNew) {
|
||||
initTabs();
|
||||
renderQuestions();
|
||||
renderRetiredQuestions();
|
||||
if (editable) {
|
||||
document.getElementById('addQuestionBtn').addEventListener('click', showAddQuestionForm);
|
||||
initDragReorder();
|
||||
@ -837,6 +856,21 @@ function renderEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
function applyStructureRevision(rev) {
|
||||
if (rev == null) return;
|
||||
questionnaire.structureRevision = rev;
|
||||
const input = document.querySelector('.meta-form input[disabled][title*="Auto-incremented"]');
|
||||
if (input) input.value = String(rev);
|
||||
}
|
||||
|
||||
function notifyStructureBump(data) {
|
||||
const rev = data?.structureRevision;
|
||||
if (rev != null) {
|
||||
applyStructureRevision(rev);
|
||||
showToast(`Structure revision bumped to ${rev}.`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
const tabBar = document.getElementById('editorTabs');
|
||||
if (!tabBar) return;
|
||||
@ -1181,6 +1215,7 @@ function showAddQuestionForm() {
|
||||
|
||||
questions.push(newQ);
|
||||
renderQuestions();
|
||||
notifyStructureBump(qData);
|
||||
showToast('Question added', 'success');
|
||||
|
||||
document.getElementById('aq_text').value = '';
|
||||
@ -1211,6 +1246,30 @@ function hideAddQuestionForm() {
|
||||
|
||||
// ── Questions list ───────────────────────────────────────────────────────
|
||||
|
||||
function renderRetiredQuestions() {
|
||||
const section = document.getElementById('retiredQuestionsSection');
|
||||
const list = document.getElementById('retiredQuestionList');
|
||||
const countEl = document.getElementById('retiredCount');
|
||||
if (!section || !list) return;
|
||||
if (!retiredQuestions.length) {
|
||||
section.style.display = 'none';
|
||||
list.innerHTML = '';
|
||||
if (countEl) countEl.textContent = '0';
|
||||
return;
|
||||
}
|
||||
section.style.display = '';
|
||||
if (countEl) countEl.textContent = String(retiredQuestions.length);
|
||||
list.innerHTML = retiredQuestions.map((q, idx) => `
|
||||
<li class="question-item retired-question-item">
|
||||
<div class="question-header">
|
||||
<span class="q-text">${idx + 1}. <code>${esc(questionKeyOf(q) || '?')}</code> · ${esc((q.defaultText || '').slice(0, 48))}</span>
|
||||
<span class="badge badge-archived">Retired</span>
|
||||
${q.retiredInRevision ? `<span class="badge">rev ${q.retiredInRevision}</span>` : ''}
|
||||
</div>
|
||||
</li>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderQuestions() {
|
||||
const list = document.getElementById('questionList');
|
||||
if (!list) return;
|
||||
@ -1219,6 +1278,7 @@ function renderQuestions() {
|
||||
// update tab label
|
||||
const tabBtn = document.querySelector('#editorTabs button[data-tab="questions"]');
|
||||
if (tabBtn) tabBtn.textContent = `Questions (${questions.length})`;
|
||||
renderRetiredQuestions();
|
||||
|
||||
if (!questions.length) {
|
||||
list.innerHTML = `<li class="empty-state" style="padding:30px"><h3>No questions yet</h3><p>Add your first question above.</p></li>`;
|
||||
@ -1603,12 +1663,23 @@ async function updateQuestion(questionID, fields) {
|
||||
}
|
||||
|
||||
async function deleteQuestion(q) {
|
||||
if (!confirm(`Delete "${q.defaultText}" and all its answers?`)) return;
|
||||
if (q.retired) {
|
||||
showToast('This question is already retired', 'error');
|
||||
return;
|
||||
}
|
||||
const msg = 'Remove this question? If client data exists it will be retired (not deleted) so upload history stays intact.';
|
||||
if (!confirm(msg)) return;
|
||||
try {
|
||||
await apiDelete('questions.php', { questionID: q.questionID });
|
||||
const data = await apiDelete('questions.php', { questionID: q.questionID });
|
||||
questions = questions.filter(x => x.questionID !== q.questionID);
|
||||
if (data.retired) {
|
||||
retiredQuestions.push({ ...q, retired: true, retiredAt: Date.now() / 1000 | 0 });
|
||||
showToast('Question retired', 'success');
|
||||
} else {
|
||||
showToast('Question deleted', 'success');
|
||||
}
|
||||
notifyStructureBump(data);
|
||||
renderQuestions();
|
||||
showToast('Question deleted', 'success');
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
@ -1631,12 +1702,15 @@ async function updateOption(q, answerOptionID, fields) {
|
||||
}
|
||||
|
||||
async function deleteOption(q, answerOptionID) {
|
||||
if (!confirm('Delete this answer option?')) return;
|
||||
if (!confirm('Remove this answer option?')) return;
|
||||
try {
|
||||
await apiDelete('answer_options.php', { answerOptionID });
|
||||
q.answerOptions = (q.answerOptions || []).filter(o => o.answerOptionID !== answerOptionID);
|
||||
const data = await apiDelete('answer_options.php', { answerOptionID });
|
||||
if (!data.retired) {
|
||||
q.answerOptions = (q.answerOptions || []).filter(o => o.answerOptionID !== answerOptionID);
|
||||
}
|
||||
renderQuestionBody(q);
|
||||
showToast('Option deleted', 'success');
|
||||
notifyStructureBump(data);
|
||||
showToast(data.retired ? 'Option retired' : 'Option deleted', 'success');
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user