UX improvements in data views
This commit is contained in:
@ -58,12 +58,7 @@ $qStmt = $pdo->prepare("SELECT questionID, defaultText, type, orderIndex, config
|
|||||||
$qStmt->execute([':id' => $qnID]);
|
$qStmt->execute([':id' => $qnID]);
|
||||||
$questions = $qStmt->fetchAll(PDO::FETCH_ASSOC);
|
$questions = $qStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$questionIDs = array_column($questions, 'questionID');
|
$questionIDs = array_column($questions, 'questionID');
|
||||||
$questionColumnLabels = [];
|
$resultColumns = qdb_results_export_columns($questions, $qnID);
|
||||||
foreach ($questions as &$q) {
|
|
||||||
$cfg = json_decode($q['configJson'] ?? '{}', true) ?: [];
|
|
||||||
$questionColumnLabels[$q['questionID']] = qdb_question_column_label($cfg, $q['defaultText'], $q['questionID'], $qnID);
|
|
||||||
}
|
|
||||||
unset($q);
|
|
||||||
|
|
||||||
// Build answer-option lookup for resolving display text
|
// Build answer-option lookup for resolving display text
|
||||||
$optionTextMap = [];
|
$optionTextMap = [];
|
||||||
@ -121,10 +116,10 @@ foreach ($clients as $c) {
|
|||||||
$answerMap = [];
|
$answerMap = [];
|
||||||
foreach ($answers as $a) $answerMap[$a['questionID']] = $a;
|
foreach ($answers as $a) $answerMap[$a['questionID']] = $a;
|
||||||
|
|
||||||
foreach ($questions as $q) {
|
foreach ($resultColumns as $col) {
|
||||||
$qid = $q['questionID'];
|
$qid = $col['questionID'];
|
||||||
$a = $answerMap[$qid] ?? null;
|
$a = $answerMap[$qid] ?? null;
|
||||||
$row[$questionColumnLabels[$qid]] = qdb_format_client_answer_display($q, $a, $optionTextMap);
|
$row[$col['header']] = qdb_results_column_cell_value($col, $a, $optionTextMap);
|
||||||
}
|
}
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
}
|
}
|
||||||
@ -149,8 +144,8 @@ if (!empty($rows)) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$header = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'startedAt', 'completedAt'];
|
$header = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'startedAt', 'completedAt'];
|
||||||
foreach ($questions as $q) {
|
foreach ($resultColumns as $col) {
|
||||||
$header[] = $questionColumnLabels[$q['questionID']] ?? $q['defaultText'];
|
$header[] = $col['header'];
|
||||||
}
|
}
|
||||||
fputcsv($out, $header);
|
fputcsv($out, $header);
|
||||||
}
|
}
|
||||||
|
|||||||
67
common.php
67
common.php
@ -532,6 +532,73 @@ function qdb_merge_glass_symptom_json(?string $existingJson, array $symptomLabel
|
|||||||
return json_encode($data, JSON_UNESCAPED_UNICODE);
|
return json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Single symptom value from glass-scale JSON stored on the parent question row. */
|
||||||
|
function qdb_glass_symptom_answer_value(?string $json, string $symptomKey): string
|
||||||
|
{
|
||||||
|
$symptomKey = trim($symptomKey);
|
||||||
|
if ($symptomKey === '' || $json === null || trim($json) === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$decoded = json_decode($json, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return trim((string)($decoded[$symptomKey] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flat columns for results / CSV: one column per glass-scale symptom.
|
||||||
|
*
|
||||||
|
* @return list<array{header: string, questionID: string, symptomKey: ?string, kind: string, question: array}>
|
||||||
|
*/
|
||||||
|
function qdb_results_export_columns(array $questions, string $qnID): array
|
||||||
|
{
|
||||||
|
$columns = [];
|
||||||
|
foreach ($questions as $q) {
|
||||||
|
$cfg = json_decode($q['configJson'] ?? '{}', true) ?: [];
|
||||||
|
$type = $q['type'] ?? '';
|
||||||
|
if ($type === 'glass_scale_question') {
|
||||||
|
$symptoms = $cfg['symptoms'] ?? [];
|
||||||
|
if (is_array($symptoms) && $symptoms !== []) {
|
||||||
|
foreach ($symptoms as $symptomKey) {
|
||||||
|
$sk = trim((string)$symptomKey);
|
||||||
|
if ($sk === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$columns[] = [
|
||||||
|
'header' => $sk,
|
||||||
|
'questionID' => $q['questionID'],
|
||||||
|
'symptomKey' => $sk,
|
||||||
|
'kind' => 'glass_symptom',
|
||||||
|
'question' => $q,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$columns[] = [
|
||||||
|
'header' => qdb_question_column_label($cfg, $q['defaultText'], $q['questionID'], $qnID),
|
||||||
|
'questionID' => $q['questionID'],
|
||||||
|
'symptomKey' => null,
|
||||||
|
'kind' => 'question',
|
||||||
|
'question' => $q,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cell value for one export/results column (including glass symptom columns). */
|
||||||
|
function qdb_results_column_cell_value(array $column, ?array $answerRow, array $optionTextMap): string
|
||||||
|
{
|
||||||
|
if (($column['kind'] ?? '') === 'glass_symptom') {
|
||||||
|
return qdb_glass_symptom_answer_value(
|
||||||
|
$answerRow['freeTextValue'] ?? null,
|
||||||
|
(string)($column['symptomKey'] ?? '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return qdb_format_client_answer_display($column['question'], $answerRow, $optionTextMap);
|
||||||
|
}
|
||||||
|
|
||||||
/** Format one client_answer row for results table / CSV export. */
|
/** Format one client_answer row for results table / CSV export. */
|
||||||
function qdb_format_client_answer_display(array $question, ?array $answerRow, array $optionTextMap): string
|
function qdb_format_client_answer_display(array $question, ?array $answerRow, array $optionTextMap): string
|
||||||
{
|
{
|
||||||
|
|||||||
@ -38,13 +38,7 @@ $qStmt = $pdo->prepare("SELECT questionID, defaultText, type, orderIndex, config
|
|||||||
$qStmt->execute([':id' => $qnID]);
|
$qStmt->execute([':id' => $qnID]);
|
||||||
$questions = $qStmt->fetchAll(PDO::FETCH_ASSOC);
|
$questions = $qStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$questionIDs = array_column($questions, 'questionID');
|
$questionIDs = array_column($questions, 'questionID');
|
||||||
$questionColumnLabels = [];
|
$resultColumns = qdb_results_export_columns($questions, $qnID);
|
||||||
foreach ($questions as &$q) {
|
|
||||||
$cfg = json_decode($q['configJson'] ?? '{}', true) ?: [];
|
|
||||||
$label = qdb_question_column_label($cfg, $q['defaultText'], $q['questionID'], $qnID);
|
|
||||||
$questionColumnLabels[$q['questionID']] = $label;
|
|
||||||
}
|
|
||||||
unset($q);
|
|
||||||
|
|
||||||
$optionTextMap = [];
|
$optionTextMap = [];
|
||||||
foreach ($questionIDs as $qid) {
|
foreach ($questionIDs as $qid) {
|
||||||
@ -99,10 +93,10 @@ foreach ($clients as $c) {
|
|||||||
$answerMap = [];
|
$answerMap = [];
|
||||||
foreach ($answers as $a) $answerMap[$a['questionID']] = $a;
|
foreach ($answers as $a) $answerMap[$a['questionID']] = $a;
|
||||||
|
|
||||||
foreach ($questions as $q) {
|
foreach ($resultColumns as $col) {
|
||||||
$qid = $q['questionID'];
|
$qid = $col['questionID'];
|
||||||
$a = $answerMap[$qid] ?? null;
|
$a = $answerMap[$qid] ?? null;
|
||||||
$row[$q['defaultText']] = qdb_format_client_answer_display($q, $a, $optionTextMap);
|
$row[$col['header']] = qdb_results_column_cell_value($col, $a, $optionTextMap);
|
||||||
}
|
}
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
}
|
}
|
||||||
@ -126,8 +120,8 @@ if (!empty($rows)) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$header = ['clientCode', 'coachID', 'status', 'sumPoints', 'startedAt', 'completedAt'];
|
$header = ['clientCode', 'coachID', 'status', 'sumPoints', 'startedAt', 'completedAt'];
|
||||||
foreach ($questions as $q) {
|
foreach ($resultColumns as $col) {
|
||||||
$header[] = $questionColumnLabels[$q['questionID']] ?? $q['defaultText'];
|
$header[] = $col['header'];
|
||||||
}
|
}
|
||||||
fputcsv($out, $header);
|
fputcsv($out, $header);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { apiGet } from '../api.js';
|
import { apiGet } from '../api.js';
|
||||||
import { showToast } from '../app.js';
|
import { showToast } from '../app.js';
|
||||||
import {
|
import {
|
||||||
|
buildResultColumns,
|
||||||
isDevTestClientCode,
|
isDevTestClientCode,
|
||||||
questionDisplayKey,
|
resultColumnCellValue,
|
||||||
questionGermanLabel,
|
|
||||||
testDataRowClassForClient,
|
testDataRowClassForClient,
|
||||||
} from '../test-data.js';
|
} from '../test-data.js';
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ let sortCol = null;
|
|||||||
let sortDir = 'asc';
|
let sortDir = 'asc';
|
||||||
let allClients = [];
|
let allClients = [];
|
||||||
let questionsDef = [];
|
let questionsDef = [];
|
||||||
|
let resultColumns = [];
|
||||||
let questionnaireMeta = null;
|
let questionnaireMeta = null;
|
||||||
let filterCoach = '';
|
let filterCoach = '';
|
||||||
let filterSupervisor = '';
|
let filterSupervisor = '';
|
||||||
@ -64,6 +65,7 @@ export async function resultsPage(params) {
|
|||||||
|
|
||||||
function renderResults() {
|
function renderResults() {
|
||||||
const container = document.getElementById('resultsContent');
|
const container = document.getElementById('resultsContent');
|
||||||
|
resultColumns = buildResultColumns(questionsDef);
|
||||||
|
|
||||||
// Collect unique coaches and statuses for filters
|
// Collect unique coaches and statuses for filters
|
||||||
const coachOptions = getCoachFilterOptions();
|
const coachOptions = getCoachFilterOptions();
|
||||||
@ -107,7 +109,7 @@ function renderResults() {
|
|||||||
<span style="margin-left:auto;font-size:.85rem;color:var(--text-secondary)" id="resultCount"></span>
|
<span style="margin-left:auto;font-size:.85rem;color:var(--text-secondary)" id="resultCount"></span>
|
||||||
<a id="exportCsvLink" class="btn btn-sm" href="#">Export CSV</a>
|
<a id="exportCsvLink" class="btn btn-sm" href="#">Export CSV</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="data-toolbar-hint" style="margin-top:8px">Question columns show stable keys (e.g. country_of_origin); hover for German text. Test rows (DEV-CL-*) have a yellow background.</p>
|
<p class="data-toolbar-hint" style="margin-top:8px">Each question has its own column; glass-scale symptoms are separate columns. Hover headers for context. Test rows (DEV-CL-*) are highlighted.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-wrapper" id="resultsTableWrap">
|
<div class="table-wrapper" id="resultsTableWrap">
|
||||||
@ -121,8 +123,8 @@ function renderResults() {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const qKeyList = document.getElementById('questionKeyList');
|
const qKeyList = document.getElementById('questionKeyList');
|
||||||
qKeyList.innerHTML = questionsDef
|
qKeyList.innerHTML = resultColumns
|
||||||
.map(q => `<option value="${esc(questionDisplayKey(q))}"></option>`)
|
.map(col => `<option value="${esc(col.label)}"></option>`)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
document.getElementById('filterSupervisor').addEventListener('change', (e) => {
|
document.getElementById('filterSupervisor').addEventListener('change', (e) => {
|
||||||
@ -202,18 +204,11 @@ function renderTableHead() {
|
|||||||
{ key: 'sumPoints', label: 'Score' },
|
{ key: 'sumPoints', label: 'Score' },
|
||||||
{ key: 'completedAt', label: 'Completed' },
|
{ key: 'completedAt', label: 'Completed' },
|
||||||
];
|
];
|
||||||
const questionCols = questionsDef.map(q => {
|
const allCols = [...fixedCols, ...resultColumns.map(col => ({
|
||||||
const qKey = questionDisplayKey(q);
|
key: col.key,
|
||||||
const german = questionGermanLabel(q);
|
label: col.label,
|
||||||
return {
|
title: col.title,
|
||||||
key: `q_${q.questionID}`,
|
}))];
|
||||||
label: qKey,
|
|
||||||
title: german || qKey,
|
|
||||||
questionID: q.questionID,
|
|
||||||
questionKey: qKey,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const allCols = [...fixedCols, ...questionCols];
|
|
||||||
|
|
||||||
head.innerHTML = allCols.map((col, idx) => {
|
head.innerHTML = allCols.map((col, idx) => {
|
||||||
let cls = 'sortable';
|
let cls = 'sortable';
|
||||||
@ -389,19 +384,11 @@ function renderTableRows() {
|
|||||||
const completedDate = c.completedAt ? new Date(c.completedAt * 1000).toLocaleDateString() : '—';
|
const completedDate = c.completedAt ? new Date(c.completedAt * 1000).toLocaleDateString() : '—';
|
||||||
const statusBadge = c.status ? `<span class="badge badge-${c.status.toLowerCase().replace(/\s+/g,'_')}">${esc(c.status)}</span>` : '—';
|
const statusBadge = c.status ? `<span class="badge badge-${c.status.toLowerCase().replace(/\s+/g,'_')}">${esc(c.status)}</span>` : '—';
|
||||||
|
|
||||||
const questionCells = questionsDef.map(q => {
|
const questionCells = resultColumns.map(col => {
|
||||||
const ans = c.answers && c.answers[q.questionID];
|
const ans = c.answers && c.answers[col.questionID];
|
||||||
if (!ans) return '<td>—</td>';
|
const val = resultColumnCellValue(col, ans, optionMap);
|
||||||
if (q.type === 'glass_scale_question') {
|
if (val == null || val === '') return '<td>—</td>';
|
||||||
const text = formatGlassAnswer(ans.freeTextValue);
|
return `<td>${esc(String(val))}</td>`;
|
||||||
return `<td>${esc(text || '—')}</td>`;
|
|
||||||
}
|
|
||||||
if (ans.answerOptionID && optionMap[ans.answerOptionID]) {
|
|
||||||
return `<td>${esc(optionMap[ans.answerOptionID])}</td>`;
|
|
||||||
}
|
|
||||||
if (ans.freeTextValue) return `<td>${esc(ans.freeTextValue)}</td>`;
|
|
||||||
if (ans.numericValue !== null && ans.numericValue !== undefined) return `<td>${ans.numericValue}</td>`;
|
|
||||||
return '<td>—</td>';
|
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
return `<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
|
return `<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
|
||||||
@ -423,17 +410,16 @@ function getCellValue(client, key) {
|
|||||||
if (key === 'status') return client.status;
|
if (key === 'status') return client.status;
|
||||||
if (key === 'sumPoints') return client.sumPoints;
|
if (key === 'sumPoints') return client.sumPoints;
|
||||||
if (key === 'completedAt') return client.completedAt;
|
if (key === 'completedAt') return client.completedAt;
|
||||||
if (key.startsWith('q_')) {
|
const col = resultColumns.find(c => c.key === key);
|
||||||
const qid = key.substring(2);
|
if (col) {
|
||||||
const q = questionsDef.find(qq => qq.questionID === qid);
|
const ans = client.answers && client.answers[col.questionID];
|
||||||
const ans = client.answers && client.answers[qid];
|
const optionMap = {};
|
||||||
if (!ans) return null;
|
questionsDef.forEach(q => {
|
||||||
if (q?.type === 'glass_scale_question') {
|
(q.answerOptions || []).forEach(o => {
|
||||||
return formatGlassAnswer(ans.freeTextValue) || null;
|
optionMap[o.answerOptionID] = o.defaultText;
|
||||||
}
|
});
|
||||||
if (ans.numericValue !== null && ans.numericValue !== undefined) return ans.numericValue;
|
});
|
||||||
if (ans.freeTextValue) return ans.freeTextValue;
|
return resultColumnCellValue(col, ans, optionMap);
|
||||||
return ans.answerOptionID || null;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -448,23 +434,17 @@ function exportCSV() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const headers = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'completedAt',
|
const headers = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'completedAt',
|
||||||
...questionsDef.map(q => questionDisplayKey(q))];
|
...resultColumns.map(col => col.label)];
|
||||||
|
|
||||||
const rows = clients.map(c => {
|
const rows = clients.map(c => {
|
||||||
const base = [
|
const base = [
|
||||||
c.clientCode, coachDisplayName(c), c.supervisorUsername || '', c.status, c.sumPoints ?? '',
|
c.clientCode, coachDisplayName(c), c.supervisorUsername || '', c.status, c.sumPoints ?? '',
|
||||||
c.completedAt ? new Date(c.completedAt * 1000).toISOString() : ''
|
c.completedAt ? new Date(c.completedAt * 1000).toISOString() : ''
|
||||||
];
|
];
|
||||||
const answerCols = questionsDef.map(q => {
|
const answerCols = resultColumns.map(col => {
|
||||||
const ans = c.answers && c.answers[q.questionID];
|
const ans = c.answers && c.answers[col.questionID];
|
||||||
if (!ans) return '';
|
const val = resultColumnCellValue(col, ans, optionMap);
|
||||||
if (q.type === 'glass_scale_question') {
|
return val != null ? val : '';
|
||||||
return formatGlassAnswer(ans.freeTextValue);
|
|
||||||
}
|
|
||||||
if (ans.answerOptionID && optionMap[ans.answerOptionID]) return optionMap[ans.answerOptionID];
|
|
||||||
if (ans.freeTextValue) return ans.freeTextValue;
|
|
||||||
if (ans.numericValue !== null && ans.numericValue !== undefined) return ans.numericValue;
|
|
||||||
return '';
|
|
||||||
});
|
});
|
||||||
return [...base, ...answerCols];
|
return [...base, ...answerCols];
|
||||||
});
|
});
|
||||||
@ -491,22 +471,6 @@ function csvEsc(val) {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatGlassAnswer(raw) {
|
|
||||||
if (!raw) return '';
|
|
||||||
try {
|
|
||||||
const o = JSON.parse(raw);
|
|
||||||
if (o && typeof o === 'object' && !Array.isArray(o)) {
|
|
||||||
return Object.entries(o)
|
|
||||||
.filter(([, v]) => v != null && String(v).trim() !== '')
|
|
||||||
.map(([k, v]) => `${k}: ${v}`)
|
|
||||||
.join('; ');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* plain text fallback */
|
|
||||||
}
|
|
||||||
return String(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
const d = document.createElement('div');
|
const d = document.createElement('div');
|
||||||
d.textContent = s ?? '';
|
d.textContent = s ?? '';
|
||||||
|
|||||||
@ -59,6 +59,80 @@ export function questionGermanLabel(q) {
|
|||||||
return '';
|
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 || []) {
|
||||||
|
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). */
|
/** header_order.json column id → question key (questionnaire_x-y → y). */
|
||||||
export function headerColumnQuestionKey(headerId) {
|
export function headerColumnQuestionKey(headerId) {
|
||||||
const id = String(headerId ?? '');
|
const id = String(headerId ?? '');
|
||||||
|
|||||||
Reference in New Issue
Block a user