just testing the environment
Some checks failed
Tests / test (push) Has been cancelled

This commit is contained in:
tom.hempel
2026-05-20 07:10:08 +02:00
parent 2a11dfd0d1
commit 06fc134299
56 changed files with 1890 additions and 361 deletions

View File

@ -1,27 +1,23 @@
import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from '../api.js';
import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from '../api.js';
import { canEdit, showToast } from '../app.js';
import { navigate } from '../router.js';
const LAYOUT_TYPES = [
{ value: 'radio_question', label: 'Radio (Single Choice)' },
{ value: 'multi_check_box_question', label: 'Checkbox (Multiple Choice)' },
{ value: 'glass_scale_question', label: 'Glass Scale' },
{ value: 'value_spinner', label: 'Value Spinner' },
{ value: 'date_spinner', label: 'Date Spinner' },
{ value: 'string_spinner', label: 'String Spinner' },
{ value: 'client_coach_code_question', label: 'Client / Coach Code' },
{ value: 'client_not_signed', label: 'Client Not Signed' },
{ value: 'last_page', label: 'Last Page' },
];
const OPTION_TYPES = new Set(['radio_question', 'multi_check_box_question']);
import {
OPTION_TYPES,
layoutSelectHTML,
layoutLabel,
parseConfig,
questionLocalIds,
configFormHTML,
readConfigFromForm,
esc,
} from '../editor-utils.js';
let questionnaire = null;
let questions = [];
let isNew = false;
let activeTab = 'questions';
// ── Entry point ──────────────────────────────────────────────────────────
// ── Entry point ──────────────────────────────────────────────────────────
export async function editorPage(params) {
const app = document.getElementById('app');
@ -66,195 +62,7 @@ export async function editorPage(params) {
}
}
// ── Layout helpers ───────────────────────────────────────────────────────
function layoutSelectHTML(selected = 'radio_question', id = '') {
return `<select ${id ? `id="${id}"` : ''} class="type-select">
${LAYOUT_TYPES.map(t =>
`<option value="${t.value}" ${selected === t.value ? 'selected' : ''}>${t.label}</option>`
).join('')}
</select>`;
}
function layoutLabel(value) {
const t = LAYOUT_TYPES.find(t => t.value === value);
return t ? t.label : value || 'unknown';
}
function parseConfig(q) {
try { return JSON.parse(q.configJson || '{}'); } catch { return {}; }
}
function questionLocalIds() {
return questions.map(q => {
const parts = q.questionID.split('__');
return { questionID: q.questionID, localId: parts[parts.length - 1], defaultText: q.defaultText };
});
}
// ── Config form HTML for each layout type ────────────────────────────────
function configFormHTML(layout, config, prefix) {
let html = '';
switch (layout) {
case 'radio_question':
html = `
<div class="form-group">
<label>Extra text key (optional)</label>
<input type="text" id="${prefix}_textKey" value="${esc(config.textKey || '')}" placeholder="e.g. resilience_reflection_prompt">
</div>`;
break;
case 'multi_check_box_question':
html = `
<div class="form-group">
<label>Min selections</label>
<input type="number" id="${prefix}_minSelection" value="${config.minSelection || 0}" min="0">
</div>`;
break;
case 'glass_scale_question':
html = `
<div class="form-group">
<label>Symptom keys (one per line)</label>
<textarea id="${prefix}_symptoms" rows="5" style="font-family:monospace;font-size:.85rem">${(config.symptoms || []).join('\n')}</textarea>
</div>`;
break;
case 'value_spinner':
html = `
<div class="form-row">
<div class="form-group">
<label>Min</label>
<input type="number" id="${prefix}_rangeMin" value="${config.range?.min ?? 0}">
</div>
<div class="form-group">
<label>Max</label>
<input type="number" id="${prefix}_rangeMax" value="${config.range?.max ?? 100}">
</div>
</div>`;
break;
case 'date_spinner': {
const notBefore = config.constraints?.notBefore || '';
const notAfter = config.constraints?.notAfter || '';
html = `
<div class="form-row">
<div class="form-group">
<label>Not before (question key)</label>
<input type="text" id="${prefix}_notBefore" value="${esc(notBefore)}" placeholder="e.g. departure_country">
</div>
<div class="form-group">
<label>Not after (question key)</label>
<input type="text" id="${prefix}_notAfter" value="${esc(notAfter)}" placeholder="e.g. since_in_germany">
</div>
</div>`;
break;
}
case 'string_spinner':
html = `
<div class="form-group">
<label>Static options (one per line)</label>
<textarea id="${prefix}_stringOptions" rows="4" style="font-family:monospace;font-size:.85rem">${(config.options || []).join('\n')}</textarea>
</div>`;
break;
case 'client_coach_code_question':
html = `
<div class="form-row">
<div class="form-group">
<label>Hint 1 key</label>
<input type="text" id="${prefix}_hint1" value="${esc(config.hint1 || '')}" placeholder="client_code">
</div>
<div class="form-group">
<label>Hint 2 key</label>
<input type="text" id="${prefix}_hint2" value="${esc(config.hint2 || '')}" placeholder="coach_code">
</div>
</div>`;
break;
case 'client_not_signed':
html = `
<div class="form-row">
<div class="form-group">
<label>Text key 1</label>
<input type="text" id="${prefix}_textKey1" value="${esc(config.textKey1 || '')}">
</div>
<div class="form-group">
<label>Text key 2</label>
<input type="text" id="${prefix}_textKey2" value="${esc(config.textKey2 || '')}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Hint key</label>
<input type="text" id="${prefix}_hint" value="${esc(config.hint || '')}">
</div>
</div>`;
break;
case 'last_page':
html = `
<div class="form-group">
<label>Text key</label>
<input type="text" id="${prefix}_textKey" value="${esc(config.textKey || '')}" placeholder="finish_data_entry">
</div>`;
break;
}
return html ? `<div class="config-section">${html}</div>` : '';
}
function readConfigFromForm(layout, prefix) {
const config = {};
const val = id => document.getElementById(`${prefix}_${id}`)?.value?.trim() || '';
switch (layout) {
case 'radio_question':
if (val('textKey')) config.textKey = val('textKey');
break;
case 'multi_check_box_question': {
const ms = parseInt(val('minSelection') || '0', 10);
if (ms > 0) config.minSelection = ms;
break;
}
case 'glass_scale_question': {
const raw = val('symptoms');
const syms = raw.split('\n').map(s => s.trim()).filter(Boolean);
if (syms.length) config.symptoms = syms;
break;
}
case 'value_spinner':
config.range = {
min: parseInt(val('rangeMin') || '0', 10),
max: parseInt(val('rangeMax') || '100', 10),
};
break;
case 'date_spinner': {
const nb = val('notBefore');
const na = val('notAfter');
if (nb || na) {
config.constraints = {};
if (nb) config.constraints.notBefore = nb;
if (na) config.constraints.notAfter = na;
}
break;
}
case 'string_spinner': {
const raw = val('stringOptions');
const opts = raw.split('\n').map(s => s.trim()).filter(Boolean);
if (opts.length) config.options = opts;
break;
}
case 'client_coach_code_question':
if (val('hint1')) config.hint1 = val('hint1');
if (val('hint2')) config.hint2 = val('hint2');
break;
case 'client_not_signed':
if (val('textKey1')) config.textKey1 = val('textKey1');
if (val('textKey2')) config.textKey2 = val('textKey2');
if (val('hint')) config.hint = val('hint');
break;
case 'last_page':
if (val('textKey')) config.textKey = val('textKey');
break;
}
return JSON.stringify(config);
}
// ── Main editor render ───────────────────────────────────────────────────
// ── Main editor render (helpers in editor-utils.js) ─────────────────────
function renderEditor() {
const container = document.getElementById('editorContent');
@ -367,7 +175,7 @@ function formatJson(str) {
}
}
// ── Save questionnaire meta ──────────────────────────────────────────────
// ── Save questionnaire meta ──────────────────────────────────────────────
async function saveMeta() {
const name = document.getElementById('metaName').value.trim();
@ -413,7 +221,7 @@ async function saveMeta() {
}
}
// ── Add question form ────────────────────────────────────────────────────
// ── Add question form ────────────────────────────────────────────────────
function showAddQuestionForm() {
const wrapper = document.getElementById('addQuestionFormWrapper');
@ -457,7 +265,7 @@ function showAddQuestionForm() {
<div class="form-group" style="width:160px;margin-bottom:0">
<select id="aq_opt_next">
<option value="">(next in order)</option>
${questionLocalIds().map(q => `<option value="${esc(q.localId)}">${esc(q.localId)} - ${esc(q.defaultText)}</option>`).join('')}
${questionLocalIds(questions).map(q => `<option value="${esc(q.localId)}">${esc(q.localId)} - ${esc(q.defaultText)}</option>`).join('')}
</select>
</div>
<button class="btn btn-sm" id="aq_opt_add" style="flex-shrink:0;white-space:nowrap">+ Add</button>
@ -588,7 +396,7 @@ function hideAddQuestionForm() {
if (wrapper) { wrapper.style.display = 'none'; wrapper.innerHTML = ''; }
}
// ── Questions list ───────────────────────────────────────────────────────
// ── Questions list ───────────────────────────────────────────────────────
function renderQuestions() {
const list = document.getElementById('questionList');
@ -628,7 +436,7 @@ function renderQuestions() {
});
}
// ── Question body (expanded) ─────────────────────────────────────────────
// ── Question body (expanded) ─────────────────────────────────────────────
function renderQuestionBody(q) {
const body = document.getElementById(`qbody-${q.questionID}`);
@ -734,7 +542,7 @@ function optionItemHTML(q, o, editable) {
`;
}
// ── Add option form ──────────────────────────────────────────────────────
// ── Add option form ──────────────────────────────────────────────────────
function showAddOptionForm(q) {
const wrapper = document.getElementById(`addOptForm-${q.questionID}`);
@ -743,7 +551,7 @@ function showAddOptionForm(q) {
toggleBtn.style.display = 'none';
wrapper.style.display = '';
const qIds = questionLocalIds();
const qIds = questionLocalIds(questions);
wrapper.innerHTML = `
<div class="inline-form-card" style="margin-top:8px">
@ -813,12 +621,12 @@ function hideAddOptionForm(q) {
if (toggleBtn) toggleBtn.style.display = '';
}
// ── Edit option form ─────────────────────────────────────────────────────
// ── Edit option form ─────────────────────────────────────────────────────
function showEditOptionForm(q, opt) {
const li = document.querySelector(`#opts-${q.questionID} .option-item[data-id="${opt.answerOptionID}"]`);
if (!li) return;
const qIds = questionLocalIds();
const qIds = questionLocalIds(questions);
li.innerHTML = `
<div class="inline-form-card" style="width:100%;padding:8px">
@ -861,7 +669,7 @@ function showEditOptionForm(q, opt) {
document.getElementById(`eo_cancel_${opt.answerOptionID}`).addEventListener('click', () => renderQuestionBody(q));
}
// ── Question CRUD ────────────────────────────────────────────────────────
// ── Question CRUD ────────────────────────────────────────────────────────
async function updateQuestion(questionID, fields) {
try {
@ -892,7 +700,7 @@ async function deleteQuestion(q) {
}
}
// ── Answer option CRUD ───────────────────────────────────────────────────
// ── Answer option CRUD ───────────────────────────────────────────────────
async function updateOption(q, answerOptionID, fields) {
try {
@ -920,7 +728,7 @@ async function deleteOption(q, answerOptionID) {
}
}
// ── Translations tab ─────────────────────────────────────────────────────
// ── Translations tab ─────────────────────────────────────────────────────
let transData = null;
@ -1169,7 +977,7 @@ function updateTransStats(entries, langCodes) {
`;
}
// ── Drag & drop (questions) ──────────────────────────────────────────────
// ── Drag & drop (questions) ──────────────────────────────────────────────
function initDragReorder() {
const list = document.getElementById('questionList');
@ -1264,9 +1072,3 @@ function getDragAfterElement(container, y, selector) {
return closest;
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s ?? '';
return d.innerHTML;
}