const STABLE_KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/; /** Local question id from full questionID (hash__q5 → q5). */ export function questionLocalKey(questionID, fallback = '') { const id = String(questionID ?? ''); const sep = id.lastIndexOf('__'); if (sep >= 0) return id.slice(sep + 2); return fallback || id; } function parseQuestionConfig(q) { if (!q || typeof q !== 'object') return {}; let cfg = q.configJson; if (typeof cfg === 'string') { try { cfg = JSON.parse(cfg); } catch { cfg = {}; } } return cfg && typeof cfg === 'object' ? cfg : {}; } /** Stable question key for table headers / CSV (matches server qdb_question_column_label). */ export function questionDisplayKey(q) { if (!q) return ''; if (q.questionKey) return String(q.questionKey); const cfg = parseQuestionConfig(q); const fromCfg = String(cfg.questionKey || '').trim(); if (fromCfg && STABLE_KEY_RE.test(fromCfg)) return fromCfg; const dt = String(q.defaultText || '').trim(); if (dt && STABLE_KEY_RE.test(dt)) return dt; return questionLocalKey(q.questionID, dt); } /** German question text for header tooltips. */ export function questionGermanLabel(q) { const dt = String(q?.defaultText || '').trim(); const key = questionDisplayKey(q); if (dt && dt !== key) return dt; return ''; } /** One symptom value from parent glass-scale JSON answer. */ export function glassSymptomAnswerValue(raw, symptomKey) { if (!raw || !symptomKey) return ''; try { const o = JSON.parse(raw); if (o && typeof o === 'object' && !Array.isArray(o)) { const v = o[symptomKey]; return v != null ? String(v).trim() : ''; } } catch { /* ignore */ } return ''; } /** * Flat result/export columns: glass_scale questions → one column per symptom. */ export function buildResultColumns(questions) { const cols = []; for (const q of questions || []) { if (q.type === 'last_page') continue; const cfg = parseQuestionConfig(q); const symptoms = cfg.symptoms; if (q.type === 'glass_scale_question' && Array.isArray(symptoms) && symptoms.length > 0) { const parentKey = questionDisplayKey(q); for (const sk of symptoms) { const symptomKey = String(sk).trim(); if (!symptomKey) continue; cols.push({ key: `glass_${q.questionID}_${symptomKey}`, questionID: q.questionID, symptomKey, label: symptomKey, title: parentKey && parentKey !== symptomKey ? `${parentKey} · ${symptomKey}` : symptomKey, isGlassSymptom: true, question: q, }); } continue; } const label = questionDisplayKey(q); cols.push({ key: `q_${q.questionID}`, questionID: q.questionID, symptomKey: null, label, title: questionGermanLabel(q) || label, isGlassSymptom: false, question: q, }); } return cols; } /** Display text for one results table cell. */ export function resultColumnCellValue(col, answer, optionMap = {}) { if (!answer) return null; if (col.isGlassSymptom) { const v = glassSymptomAnswerValue(answer.freeTextValue, col.symptomKey); return v || null; } const q = col.question; if (answer.answerOptionID && optionMap[answer.answerOptionID]) { return optionMap[answer.answerOptionID]; } if (answer.freeTextValue) return answer.freeTextValue; if (answer.numericValue !== null && answer.numericValue !== undefined) { return answer.numericValue; } return null; } /** header_order.json column id → question key (questionnaire_x-y → y). */ export function headerColumnQuestionKey(headerId) { const id = String(headerId ?? ''); if (id === 'client_code') return 'client_code'; const dash = id.indexOf('-'); if (dash < 0) return id; return id.slice(dash + 1); }