showing quesiton-keys instead of ids
This commit is contained in:
@ -54,10 +54,16 @@ if (!$questionnaire) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Questions ordered
|
// Questions ordered
|
||||||
$qStmt = $pdo->prepare("SELECT questionID, defaultText, type, orderIndex FROM question WHERE questionnaireID = :id ORDER BY orderIndex");
|
$qStmt = $pdo->prepare("SELECT questionID, defaultText, type, orderIndex, configJson FROM question WHERE questionnaireID = :id ORDER BY orderIndex");
|
||||||
$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 = [];
|
||||||
|
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 = [];
|
||||||
@ -118,7 +124,7 @@ foreach ($clients as $c) {
|
|||||||
foreach ($questions as $q) {
|
foreach ($questions as $q) {
|
||||||
$qid = $q['questionID'];
|
$qid = $q['questionID'];
|
||||||
$a = $answerMap[$qid] ?? null;
|
$a = $answerMap[$qid] ?? null;
|
||||||
$row[$q['defaultText']] = qdb_format_client_answer_display($q, $a, $optionTextMap);
|
$row[$questionColumnLabels[$qid]] = qdb_format_client_answer_display($q, $a, $optionTextMap);
|
||||||
}
|
}
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
}
|
}
|
||||||
@ -143,7 +149,9 @@ 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) $header[] = $q['defaultText'];
|
foreach ($questions as $q) {
|
||||||
|
$header[] = $questionColumnLabels[$q['questionID']] ?? $q['defaultText'];
|
||||||
|
}
|
||||||
fputcsv($out, $header);
|
fputcsv($out, $header);
|
||||||
}
|
}
|
||||||
fclose($out);
|
fclose($out);
|
||||||
|
|||||||
@ -893,6 +893,12 @@ function qdb_question_local_id(string $questionID, ?string $questionnaireID = nu
|
|||||||
return $pos !== false ? substr($questionID, $pos + 2) : $questionID;
|
return $pos !== false ? substr($questionID, $pos + 2) : $questionID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Column / export header: stable question key, else local id (q1, q2, …). */
|
||||||
|
function qdb_question_column_label(array $config, string $defaultText, string $questionID, ?string $questionnaireID = null): string {
|
||||||
|
$key = qdb_question_key($config, $defaultText);
|
||||||
|
return $key !== '' ? $key : qdb_question_local_id($questionID, $questionnaireID);
|
||||||
|
}
|
||||||
|
|
||||||
function qdb_make_question_id(string $questionnaireID, string $localId): string {
|
function qdb_make_question_id(string $questionnaireID, string $localId): string {
|
||||||
return $questionnaireID . '__' . $localId;
|
return $questionnaireID . '__' . $localId;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,10 +34,17 @@ if (!$questionnaire) {
|
|||||||
json_error('NOT_FOUND', 'Questionnaire not found', 404);
|
json_error('NOT_FOUND', 'Questionnaire not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$qStmt = $pdo->prepare("SELECT questionID, defaultText, type, orderIndex FROM question WHERE questionnaireID = :id ORDER BY orderIndex");
|
$qStmt = $pdo->prepare("SELECT questionID, defaultText, type, orderIndex, configJson FROM question WHERE questionnaireID = :id ORDER BY orderIndex");
|
||||||
$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 = [];
|
||||||
|
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) {
|
||||||
@ -119,7 +126,9 @@ if (!empty($rows)) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$header = ['clientCode', 'coachID', 'status', 'sumPoints', 'startedAt', 'completedAt'];
|
$header = ['clientCode', 'coachID', 'status', 'sumPoints', 'startedAt', 'completedAt'];
|
||||||
foreach ($questions as $q) $header[] = $q['defaultText'];
|
foreach ($questions as $q) {
|
||||||
|
$header[] = $questionColumnLabels[$q['questionID']] ?? $q['defaultText'];
|
||||||
|
}
|
||||||
fputcsv($out, $header);
|
fputcsv($out, $header);
|
||||||
}
|
}
|
||||||
fclose($out);
|
fclose($out);
|
||||||
|
|||||||
@ -34,6 +34,8 @@ $questionIDs = array_column($questions, 'questionID');
|
|||||||
foreach ($questions as &$q) {
|
foreach ($questions as &$q) {
|
||||||
$q['isRequired'] = (int)$q['isRequired'];
|
$q['isRequired'] = (int)$q['isRequired'];
|
||||||
$q['orderIndex'] = (int)$q['orderIndex'];
|
$q['orderIndex'] = (int)$q['orderIndex'];
|
||||||
|
$cfg = json_decode($q['configJson'] ?? '{}', true) ?: [];
|
||||||
|
$q['questionKey'] = qdb_question_column_label($cfg, $q['defaultText'], $q['questionID'], $qnID);
|
||||||
$ao = $pdo->prepare("SELECT answerOptionID, defaultText, points, orderIndex
|
$ao = $pdo->prepare("SELECT answerOptionID, defaultText, points, orderIndex
|
||||||
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex");
|
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex");
|
||||||
$ao->execute([':qid' => $q['questionID']]);
|
$ao->execute([':qid' => $q['questionID']]);
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { apiGet } from '../api.js';
|
|||||||
import { showToast } from '../app.js';
|
import { showToast } from '../app.js';
|
||||||
import {
|
import {
|
||||||
isDevTestClientCode,
|
isDevTestClientCode,
|
||||||
questionLocalKey,
|
questionDisplayKey,
|
||||||
|
questionGermanLabel,
|
||||||
testDataRowClassForClient,
|
testDataRowClassForClient,
|
||||||
} from '../test-data.js';
|
} from '../test-data.js';
|
||||||
|
|
||||||
@ -106,7 +107,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 keys; hover headers for full text. Test rows (DEV-CL-*) have a yellow background.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-wrapper" id="resultsTableWrap">
|
<div class="table-wrapper" id="resultsTableWrap">
|
||||||
@ -121,7 +122,7 @@ function renderResults() {
|
|||||||
|
|
||||||
const qKeyList = document.getElementById('questionKeyList');
|
const qKeyList = document.getElementById('questionKeyList');
|
||||||
qKeyList.innerHTML = questionsDef
|
qKeyList.innerHTML = questionsDef
|
||||||
.map(q => `<option value="${esc(questionLocalKey(q.questionID, q.defaultText))}"></option>`)
|
.map(q => `<option value="${esc(questionDisplayKey(q))}"></option>`)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
document.getElementById('filterSupervisor').addEventListener('change', (e) => {
|
document.getElementById('filterSupervisor').addEventListener('change', (e) => {
|
||||||
@ -202,11 +203,12 @@ function renderTableHead() {
|
|||||||
{ key: 'completedAt', label: 'Completed' },
|
{ key: 'completedAt', label: 'Completed' },
|
||||||
];
|
];
|
||||||
const questionCols = questionsDef.map(q => {
|
const questionCols = questionsDef.map(q => {
|
||||||
const qKey = questionLocalKey(q.questionID, q.defaultText);
|
const qKey = questionDisplayKey(q);
|
||||||
|
const german = questionGermanLabel(q);
|
||||||
return {
|
return {
|
||||||
key: `q_${q.questionID}`,
|
key: `q_${q.questionID}`,
|
||||||
label: qKey,
|
label: qKey,
|
||||||
title: q.defaultText || qKey,
|
title: german || qKey,
|
||||||
questionID: q.questionID,
|
questionID: q.questionID,
|
||||||
questionKey: qKey,
|
questionKey: qKey,
|
||||||
};
|
};
|
||||||
@ -446,7 +448,7 @@ function exportCSV() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const headers = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'completedAt',
|
const headers = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'completedAt',
|
||||||
...questionsDef.map(q => questionLocalKey(q.questionID, q.defaultText))];
|
...questionsDef.map(q => questionDisplayKey(q))];
|
||||||
|
|
||||||
const rows = clients.map(c => {
|
const rows = clients.map(c => {
|
||||||
const base = [
|
const base = [
|
||||||
|
|||||||
@ -16,7 +16,9 @@ export function testDataRowClassForUser(username) {
|
|||||||
return isDevTestUsername(username) ? ' row-test-data' : '';
|
return isDevTestUsername(username) ? ' row-test-data' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Local question key from full questionID (hash__q5 → q5). */
|
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 = '') {
|
export function questionLocalKey(questionID, fallback = '') {
|
||||||
const id = String(questionID ?? '');
|
const id = String(questionID ?? '');
|
||||||
const sep = id.lastIndexOf('__');
|
const sep = id.lastIndexOf('__');
|
||||||
@ -24,6 +26,39 @@ export function questionLocalKey(questionID, fallback = '') {
|
|||||||
return fallback || id;
|
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 '';
|
||||||
|
}
|
||||||
|
|
||||||
/** 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