/** * All-in-one interactive preview for questionnaire editing. * All visible questions on one scrollable page; branching updates visibility live. * Answers stay in memory only — nothing is saved to the server. */ const GLASS_LABELS = [ { key: 'never_glass', label: 'Never' }, { key: 'little_glass', label: 'A little' }, { key: 'moderate_glass', label: 'Moderate' }, { key: 'much_glass', label: 'Much' }, { key: 'extreme_glass', label: 'Extreme' }, ]; /** Sample codes shown in preview (read-only), mimicking the app after load/login. */ const PREVIEW_CLIENT_CODE = 'CLIENT-042'; const PREVIEW_COACH_CODE = 'coach.demo'; const MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; } function parseConfig(q) { try { return JSON.parse(q.configJson || '{}'); } catch { return {}; } } function localId(q) { return q.localId || (q.questionID || '').split('__').pop() || ''; } function questionKey(q) { const cfg = parseConfig(q); return (q.questionKey || cfg.questionKey || '').trim(); } function optionKeyOf(o) { if (o.optionKey) return o.optionKey; const dt = (o.defaultText || '').trim(); return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(dt) ? dt : dt; } function optionLabel(o) { return (o.labelGerman || '').trim() || o.defaultText || o.optionKey || '?'; } function glassScaleOptions(q) { const opts = q.answerOptions || []; if (opts.length) { return opts.map(o => ({ key: optionKeyOf(o), label: optionLabel(o) })); } return GLASS_LABELS; } function valueSpinnerValues(config) { const range = config.range || { min: 0, max: 10 }; const min = Number(range.min ?? 0); const max = Number(range.max ?? 10); const lo = Math.min(min, max); const hi = Math.max(min, max); const values = []; for (let v = lo; v <= hi; v++) values.push(v); return values; } function hasBranchingOptions(q) { const layout = q.type || ''; const config = parseConfig(q); if (layout === 'radio_question') { return (q.answerOptions || []).some(o => o.nextQuestionId?.trim()); } if (layout === 'value_spinner') { const opts = config.valueOptions || config.options || []; return opts.some(o => o.nextQuestionId?.trim()) || Boolean(config.nextQuestionId?.trim()); } if (layout === 'string_spinner') { return Boolean(config.nextQuestionId?.trim() || config.otherNextQuestionId?.trim()); } if (layout === 'multi_check_box_question') { return Boolean(config.nextQuestionId?.trim() || config.otherNextQuestionId?.trim()); } const withQuestionNext = new Set([ 'free_text', 'date_spinner', 'slider_question', 'glass_scale_question', 'client_coach_code_question', ]); return withQuestionNext.has(layout) && Boolean(config.nextQuestionId?.trim()); } function resolveNextLocalId(q, answer) { const layout = q.type || ''; const config = parseConfig(q); if (layout === 'radio_question') { const key = typeof answer === 'string' ? answer : ''; const opt = (q.answerOptions || []).find(o => optionKeyOf(o) === key); return opt?.nextQuestionId?.trim() || null; } if (layout === 'value_spinner') { const num = parseInt(String(answer), 10); const branch = (config.valueOptions || config.options || []) .find(o => Number(o.value) === num); if (branch?.nextQuestionId?.trim()) return branch.nextQuestionId.trim(); return config.nextQuestionId?.trim() || null; } if (layout === 'string_spinner') { const val = String(answer ?? ''); const otherKey = config.otherOptionKey?.trim(); const otherNext = config.otherNextQuestionId?.trim(); if (otherKey && otherNext && val === otherKey) return otherNext; return config.nextQuestionId?.trim() || null; } if (layout === 'multi_check_box_question') { const keys = Array.isArray(answer) ? answer : []; const otherKey = config.otherOptionKey?.trim(); const otherNext = config.otherNextQuestionId?.trim(); if (otherKey && otherNext && keys.includes(otherKey)) return otherNext; return config.nextQuestionId?.trim() || null; } return config.nextQuestionId?.trim() || null; } function readAnswerFromBody(body, q) { if (!body) return null; const layout = q.type || ''; const config = parseConfig(q); switch (layout) { case 'radio_question': { const checked = body.querySelector('input[type=radio]:checked'); return checked ? checked.value : null; } case 'multi_check_box_question': { const boxes = body.querySelectorAll('input[type=checkbox]:checked'); return [...boxes].map(el => el.value); } case 'value_spinner': case 'string_spinner': { const sel = body.querySelector('select.qp-select'); const val = sel?.value ?? ''; return val === '' ? null : val; } case 'slider_question': { const range = body.querySelector('input[type=range]'); return range ? range.value : null; } case 'date_spinner': { const precision = config.precision || 'full'; const year = body.querySelector('[data-part=year]')?.value; if (!year) return null; if (precision === 'year') return year; const month = body.querySelector('[data-part=month]')?.value || '01'; if (precision === 'year_month') return `${year}-${month}`; const day = body.querySelector('[data-part=day]')?.value || '01'; return `${year}-${month}-${day}`; } case 'free_text': { const text = body.querySelector('input[type=text], textarea')?.value?.trim(); return text || null; } case 'glass_scale_question': { const symptoms = config.symptoms || []; if (!symptoms.length) return {}; const result = {}; for (const sym of symptoms) { const picked = body.querySelector(`input[data-symptom="${CSS.escape(sym)}"]:checked`); if (!picked) return null; result[sym] = picked.value; } return result; } case 'client_coach_code_question': { const client = body.querySelector('[data-field=client]')?.value?.trim(); const coach = body.querySelector('[data-field=coach]')?.value?.trim(); if (!client && !coach) return null; return { client_code: client || '', coach_code: coach || '' }; } case 'last_page': return true; default: return null; } } function hasAnswerInUi(q, sectionEl) { const body = sectionEl?.querySelector('.qp-question-body'); const layout = q.type || ''; const config = parseConfig(q); const answer = readAnswerFromBody(body, q); switch (layout) { case 'radio_question': return answer !== null; case 'multi_check_box_question': { const min = config.minSelection || 0; const count = Array.isArray(answer) ? answer.length : 0; return count >= Math.max(min, 0) && (count > 0 || !q.isRequired); } case 'value_spinner': case 'string_spinner': return answer !== null && answer !== ''; case 'slider_question': return answer !== null; case 'date_spinner': { const year = body?.querySelector('[data-part=year]')?.value; if (!year) return false; if (config.precision === 'year') return true; const month = body?.querySelector('[data-part=month]')?.value; if (!month) return false; if (config.precision === 'year_month') return true; return Boolean(body?.querySelector('[data-part=day]')?.value); } case 'free_text': return Boolean(answer); case 'glass_scale_question': { const symptoms = config.symptoms || []; if (!symptoms.length) return true; return answer !== null && typeof answer === 'object' && symptoms.every(s => answer[s]); } case 'client_coach_code_question': return Boolean(answer?.client_code && answer?.coach_code); case 'last_page': return true; default: return true; } } function branchAnswerFromSection(q, sectionEl) { if (hasBranchingOptions(q) && !hasAnswerInUi(q, sectionEl)) return null; const body = sectionEl?.querySelector('.qp-question-body'); return readAnswerFromBody(body, q); } function visibleQuestionIndices(questions, sectionByIdx) { const visible = new Set(); let i = 0; while (i < questions.length) { const q = questions[i]; if (q.type === 'last_page') { i++; continue; } visible.add(i); const sectionEl = sectionByIdx.get(i); const answer = sectionEl ? branchAnswerFromSection(q, sectionEl) : null; const nextId = answer !== null ? resolveNextLocalId(q, answer) : null; if (nextId) { const isLastPageTarget = questions.some( qq => qq.type === 'last_page' && localId(qq) === nextId, ); if (isLastPageTarget) break; const target = questions.findIndex(qq => localId(qq) === nextId); i = target >= 0 ? target : i + 1; } else if (hasBranchingOptions(q) && answer === null) { break; } else { i++; } } return visible; } function symptomRowLabel(q, symptomKey) { const rows = q.glassSymptoms || []; const row = rows.find(r => r.key === symptomKey); return (row?.labelDe || '').trim() || symptomKey; } function renderQuestionBody(q, sectionIdx) { const layout = q.type || ''; const config = parseConfig(q); const qText = q.defaultText || questionKey(q) || 'Question'; const prefix = `qp_${sectionIdx}`; let inner = ''; if (config.noteBefore) inner += `

${esc(config.noteBefore)}

`; inner += `

${esc(qText)}

`; switch (layout) { case 'radio_question': inner += `
`; for (const o of (q.answerOptions || [])) { const key = optionKeyOf(o); inner += ``; } inner += `
`; break; case 'multi_check_box_question': inner += `
`; for (const o of (q.answerOptions || [])) { const key = optionKeyOf(o); inner += ``; } if (config.otherOptionKey && config.otherNextQuestionId) { const ok = config.otherOptionKey; inner += ``; } inner += `
`; if (config.minSelection) { inner += `

Select at least ${config.minSelection} option(s).

`; } break; case 'value_spinner': { const values = valueSpinnerValues(config); inner += ``; break; } case 'slider_question': { const range = config.range || { min: 0, max: 100 }; const min = Number(range.min ?? 0); const max = Number(range.max ?? 100); const step = Number(config.step ?? 1) || 1; inner += `
${min}${config.unitLabel ? ' ' + esc(config.unitLabel) : ''}
`; break; } case 'date_spinner': { const precision = config.precision || 'full'; const years = []; const now = new Date().getFullYear(); for (let yr = now + 1; yr >= 1900; yr--) years.push(yr); inner += `
`; if (precision === 'full') { inner += ``; } if (precision !== 'year') { inner += ``; } inner += `
`; break; } case 'string_spinner': { inner += ``; break; } case 'free_text': inner += ``; break; case 'glass_scale_question': { const symptoms = config.symptoms || []; const scaleOpts = glassScaleOptions(q); if (!symptoms.length) { inner += `

Informational screen (no symptom rows).

`; } else { inner += `
`; for (const so of scaleOpts) inner += ``; inner += ``; symptoms.forEach((sym, si) => { inner += ``; scaleOpts.forEach(so => { inner += ``; }); inner += ``; }); inner += `
Symptom${esc(so.label)}
${esc(symptomRowLabel(q, sym))}
`; } break; } case 'client_coach_code_question': inner += `

In the app, client and Counselor codes are loaded automatically — Counselor review them here, not type them in.

`; break; case 'last_page': inner += `

${esc(config.textKey || 'End of questionnaire.')}

`; break; default: inner += `

Preview not available for layout ${esc(layout)}.

`; } if (config.noteAfter) inner += `

${esc(config.noteAfter)}

`; return inner; } function layoutLabel(type) { const labels = { radio_question: 'Radio', multi_check_box_question: 'Checkbox', glass_scale_question: 'Glass scale', value_spinner: 'Value spinner', slider_question: 'Slider', date_spinner: 'Date', string_spinner: 'String spinner', free_text: 'Free text', client_coach_code_question: 'Client / Counselor code', last_page: 'Last page', }; return labels[type] || type || 'Unknown'; } /** * @param {HTMLElement} container * @param {{ questionnaireName?: string, questions: object[] }} opts */ export function mountQuestionnairePreview(container, opts) { const questions = opts.questions || []; function render() { if (!questions.length) { container.innerHTML = `

No questions to preview. Add questions on the Questions tab first.

`; return; } const sections = questions.map((q, idx) => { const lid = localId(q); const num = idx + 1; return ` `; }).join(''); container.innerHTML = `
Preview mode — all questions on one page. Answers are not saved.
${sections}
`; wireEvents(); updateVisibility(); } function sectionElements() { return [...container.querySelectorAll('.qp-section')]; } function sectionByIdxMap() { const map = new Map(); for (const el of sectionElements()) { map.set(parseInt(el.dataset.idx, 10), el); } return map; } function updateVisibility() { const map = sectionByIdxMap(); const visible = visibleQuestionIndices(questions, map); let shown = 0; for (const el of sectionElements()) { const idx = parseInt(el.dataset.idx, 10); const isVisible = visible.has(idx); el.hidden = !isVisible; el.classList.toggle('qp-section-hidden', !isVisible); if (isVisible) shown++; } const counter = container.querySelector('#qpVisibleCount'); if (counter) { counter.textContent = `Showing ${shown} of ${questions.length} questions`; } } function wireEvents() { const form = container.querySelector('#qpForm'); if (!form) return; form.addEventListener('input', () => updateVisibility()); form.addEventListener('change', () => updateVisibility()); form.querySelectorAll('input[type=range]').forEach(slider => { const output = slider.closest('.qp-slider')?.querySelector('.qp-slider-value'); if (!output) return; slider.addEventListener('input', () => { output.textContent = slider.value; }); }); container.querySelector('[data-action=restart]')?.addEventListener('click', () => { form.reset(); form.querySelectorAll('input[type=radio], input[type=checkbox]').forEach(el => { el.checked = false; }); form.querySelectorAll('.qp-slider-value').forEach((output, i) => { const slider = form.querySelectorAll('input[type=range]')[i]; if (slider) output.textContent = slider.value; }); updateVisibility(); }); } render(); return { restart: () => container.querySelector('[data-action=restart]')?.click() }; }