refactored export functionality
This commit is contained in:
145
api/export.php
145
api/export.php
@ -1,143 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
/**
|
||||||
|
* Legacy direct export endpoint (website Export page). Delegates to shared handler.
|
||||||
|
*/
|
||||||
require_once __DIR__ . '/../common.php';
|
require_once __DIR__ . '/../common.php';
|
||||||
require_once __DIR__ . '/../db_init.php';
|
require_once __DIR__ . '/../db_init.php';
|
||||||
|
|
||||||
$tokenRec = require_valid_token();
|
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||||
|
$method = 'GET';
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
require __DIR__ . '/../handlers/export.php';
|
||||||
header('Content-Type: application/json; charset=UTF-8');
|
|
||||||
http_response_code(405);
|
|
||||||
echo json_encode(["error" => "Method not allowed"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$qnID = $_GET['questionnaireID'] ?? '';
|
|
||||||
$format = strtolower($_GET['format'] ?? 'csv');
|
|
||||||
|
|
||||||
if (!$qnID) {
|
|
||||||
header('Content-Type: application/json; charset=UTF-8');
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(["error" => "questionnaireID query param required"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
|
||||||
|
|
||||||
// Questionnaire metadata
|
|
||||||
$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
|
|
||||||
$qn->execute([':id' => $qnID]);
|
|
||||||
$questionnaire = $qn->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if (!$questionnaire) {
|
|
||||||
qdb_discard($tmpDb, $lockFp);
|
|
||||||
header('Content-Type: application/json; charset=UTF-8');
|
|
||||||
http_response_code(404);
|
|
||||||
echo json_encode(["error" => "Questionnaire not found"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Questions ordered
|
|
||||||
$qStmt = $pdo->prepare("SELECT questionID, defaultText, orderIndex FROM question WHERE questionnaireID = :id ORDER BY orderIndex");
|
|
||||||
$qStmt->execute([':id' => $qnID]);
|
|
||||||
$questions = $qStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
$questionIDs = array_column($questions, 'questionID');
|
|
||||||
|
|
||||||
// Build answer-option lookup for resolving display text
|
|
||||||
$optionTextMap = [];
|
|
||||||
foreach ($questionIDs as $qid) {
|
|
||||||
$aoStmt = $pdo->prepare("SELECT answerOptionID, defaultText FROM answer_option WHERE questionID = :qid");
|
|
||||||
$aoStmt->execute([':qid' => $qid]);
|
|
||||||
foreach ($aoStmt->fetchAll(PDO::FETCH_ASSOC) as $ao) {
|
|
||||||
$optionTextMap[$ao['answerOptionID']] = $ao['defaultText'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RBAC filter
|
|
||||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
|
||||||
|
|
||||||
$sql = "
|
|
||||||
SELECT cl.clientCode, cl.coachID,
|
|
||||||
co.username AS coachUsername,
|
|
||||||
sv.username AS supervisorUsername,
|
|
||||||
cq.status, cq.sumPoints, cq.startedAt, cq.completedAt
|
|
||||||
FROM client cl
|
|
||||||
INNER JOIN completed_questionnaire cq ON cq.clientCode = cl.clientCode AND cq.questionnaireID = :qnid
|
|
||||||
LEFT JOIN coach co ON co.coachID = cl.coachID
|
|
||||||
LEFT JOIN supervisor sv ON sv.supervisorID = co.supervisorID
|
|
||||||
WHERE $rbacClause
|
|
||||||
ORDER BY cl.clientCode
|
|
||||||
";
|
|
||||||
$params = array_merge([':qnid' => $qnID], $rbacParams);
|
|
||||||
$cStmt = $pdo->prepare($sql);
|
|
||||||
$cStmt->execute($params);
|
|
||||||
$clients = $cStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Fetch answers for each client
|
|
||||||
$qPlaceholders = !empty($questionIDs) ? implode(',', array_fill(0, count($questionIDs), '?')) : "'__none__'";
|
|
||||||
$answerStmt = $pdo->prepare("
|
|
||||||
SELECT questionID, answerOptionID, freeTextValue, numericValue
|
|
||||||
FROM client_answer
|
|
||||||
WHERE clientCode = ? AND questionID IN ($qPlaceholders)
|
|
||||||
");
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
foreach ($clients as $c) {
|
|
||||||
$row = [
|
|
||||||
'clientCode' => $c['clientCode'],
|
|
||||||
'coach' => $c['coachUsername'] ?? $c['coachID'],
|
|
||||||
'supervisor' => $c['supervisorUsername'] ?? '',
|
|
||||||
'status' => $c['status'],
|
|
||||||
'sumPoints' => $c['sumPoints'],
|
|
||||||
'startedAt' => $c['startedAt'] ? date('Y-m-d H:i', (int)$c['startedAt']) : '',
|
|
||||||
'completedAt'=> $c['completedAt'] ? date('Y-m-d H:i', (int)$c['completedAt']) : '',
|
|
||||||
];
|
|
||||||
|
|
||||||
$bindParams = array_merge([$c['clientCode']], $questionIDs);
|
|
||||||
$answerStmt->execute($bindParams);
|
|
||||||
$answers = $answerStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
$answerMap = [];
|
|
||||||
foreach ($answers as $a) $answerMap[$a['questionID']] = $a;
|
|
||||||
|
|
||||||
foreach ($questions as $q) {
|
|
||||||
$qid = $q['questionID'];
|
|
||||||
if (isset($answerMap[$qid])) {
|
|
||||||
$a = $answerMap[$qid];
|
|
||||||
if ($a['answerOptionID'] && isset($optionTextMap[$a['answerOptionID']])) {
|
|
||||||
$row[$q['defaultText']] = $optionTextMap[$a['answerOptionID']];
|
|
||||||
} elseif ($a['freeTextValue'] !== null && $a['freeTextValue'] !== '') {
|
|
||||||
$row[$q['defaultText']] = $a['freeTextValue'];
|
|
||||||
} elseif ($a['numericValue'] !== null) {
|
|
||||||
$row[$q['defaultText']] = $a['numericValue'];
|
|
||||||
} else {
|
|
||||||
$row[$q['defaultText']] = '';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$row[$q['defaultText']] = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$rows[] = $row;
|
|
||||||
}
|
|
||||||
|
|
||||||
qdb_discard($tmpDb, $lockFp);
|
|
||||||
|
|
||||||
// Output CSV
|
|
||||||
$safeName = preg_replace('/[^a-zA-Z0-9_-]/', '_', $questionnaire['name']);
|
|
||||||
$filename = $safeName . '_v' . $questionnaire['version'] . '.csv';
|
|
||||||
|
|
||||||
header('Content-Type: text/csv; charset=UTF-8');
|
|
||||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
|
||||||
|
|
||||||
$out = fopen('php://output', 'w');
|
|
||||||
// BOM for Excel UTF-8 recognition
|
|
||||||
fwrite($out, "\xEF\xBB\xBF");
|
|
||||||
|
|
||||||
if (!empty($rows)) {
|
|
||||||
fputcsv($out, array_keys($rows[0]));
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
fputcsv($out, array_values($row));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$header = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'startedAt', 'completedAt'];
|
|
||||||
foreach ($questions as $q) $header[] = $q['defaultText'];
|
|
||||||
fputcsv($out, $header);
|
|
||||||
}
|
|
||||||
fclose($out);
|
|
||||||
|
|||||||
@ -78,6 +78,12 @@ function qdb_open(bool $writable = false): array {
|
|||||||
}
|
}
|
||||||
$pdo->exec("PRAGMA user_version = " . QDB_VERSION . ";");
|
$pdo->exec("PRAGMA user_version = " . QDB_VERSION . ";");
|
||||||
$pdo->exec("PRAGMA foreign_keys = ON;");
|
$pdo->exec("PRAGMA foreign_keys = ON;");
|
||||||
|
|
||||||
|
// Persist upgrade even when opened read-only (otherwise migration is lost on discard)
|
||||||
|
if (!$writable) {
|
||||||
|
qdb_save($tmpDb, $lockFp);
|
||||||
|
return qdb_open(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$pdo, $tmpDb, $lockFp];
|
return [$pdo, $tmpDb, $lockFp];
|
||||||
|
|||||||
@ -88,6 +88,14 @@ if ($method === 'POST') {
|
|||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Latest cache = full replace for this questionnaire (drop answers on unreachable branches)
|
||||||
|
$pdo->prepare("
|
||||||
|
DELETE FROM client_answer
|
||||||
|
WHERE clientCode = :cc AND questionID IN (
|
||||||
|
SELECT questionID FROM question WHERE questionnaireID = :qn
|
||||||
|
)
|
||||||
|
")->execute([':cc' => $clientCode, ':qn' => $qnID]);
|
||||||
|
|
||||||
$sumPoints = 0;
|
$sumPoints = 0;
|
||||||
$parsedAnswers = [];
|
$parsedAnswers = [];
|
||||||
|
|
||||||
@ -152,11 +160,6 @@ if ($method === 'POST') {
|
|||||||
$answerInsert = $pdo->prepare("
|
$answerInsert = $pdo->prepare("
|
||||||
INSERT INTO client_answer (clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
|
INSERT INTO client_answer (clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
|
||||||
VALUES (:cc, :qid, :aoid, :ftv, :nv, :at)
|
VALUES (:cc, :qid, :aoid, :ftv, :nv, :at)
|
||||||
ON CONFLICT(clientCode, questionID) DO UPDATE SET
|
|
||||||
answerOptionID = excluded.answerOptionID,
|
|
||||||
freeTextValue = excluded.freeTextValue,
|
|
||||||
numericValue = excluded.numericValue,
|
|
||||||
answeredAt = excluded.answeredAt
|
|
||||||
");
|
");
|
||||||
|
|
||||||
foreach ($parsedAnswers as $pa) {
|
foreach ($parsedAnswers as $pa) {
|
||||||
@ -354,7 +357,7 @@ if ($fetchClients) {
|
|||||||
'clientCode' => $code,
|
'clientCode' => $code,
|
||||||
'completedQuestionnaires' => $completions[$code] ?? [],
|
'completedQuestionnaires' => $completions[$code] ?? [],
|
||||||
];
|
];
|
||||||
}, $clientCodes);
|
}, array_values(array_filter($clientCodes, fn($code) => strcasecmp($code, 'DEVELOPER-SETTINGS') !== 0)));
|
||||||
|
|
||||||
qdb_discard($tmpDb, $lockFp);
|
qdb_discard($tmpDb, $lockFp);
|
||||||
json_success(['coachID' => $coachID, 'clients' => $clients]);
|
json_success(['coachID' => $coachID, 'clients' => $clients]);
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/questionnaire_reachability.php';
|
||||||
|
|
||||||
$tokenRec = require_valid_token();
|
$tokenRec = require_valid_token();
|
||||||
|
|
||||||
if ($method !== 'GET') {
|
if ($method !== 'GET') {
|
||||||
@ -79,21 +81,19 @@ foreach ($clients as $c) {
|
|||||||
$answerMap = [];
|
$answerMap = [];
|
||||||
foreach ($answers as $a) $answerMap[$a['questionID']] = $a;
|
foreach ($answers as $a) $answerMap[$a['questionID']] = $a;
|
||||||
|
|
||||||
|
$reachable = qrb_reachable_question_ids($pdo, $qnID, $answerMap, $optionTextMap);
|
||||||
|
|
||||||
foreach ($questions as $q) {
|
foreach ($questions as $q) {
|
||||||
$qid = $q['questionID'];
|
$qid = $q['questionID'];
|
||||||
if (isset($answerMap[$qid])) {
|
$header = $q['defaultText'];
|
||||||
$a = $answerMap[$qid];
|
if (!isset($reachable[$qid])) {
|
||||||
if ($a['answerOptionID'] && isset($optionTextMap[$a['answerOptionID']])) {
|
$row[$header] = '';
|
||||||
$row[$q['defaultText']] = $optionTextMap[$a['answerOptionID']];
|
continue;
|
||||||
} elseif ($a['freeTextValue'] !== null && $a['freeTextValue'] !== '') {
|
|
||||||
$row[$q['defaultText']] = $a['freeTextValue'];
|
|
||||||
} elseif ($a['numericValue'] !== null) {
|
|
||||||
$row[$q['defaultText']] = $a['numericValue'];
|
|
||||||
} else {
|
|
||||||
$row[$q['defaultText']] = '';
|
|
||||||
}
|
}
|
||||||
|
if (isset($answerMap[$qid])) {
|
||||||
|
$row[$header] = qrb_format_cell($answerMap[$qid], $optionTextMap);
|
||||||
} else {
|
} else {
|
||||||
$row[$q['defaultText']] = '';
|
$row[$header] = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/questionnaire_reachability.php';
|
||||||
|
|
||||||
$tokenRec = require_valid_token();
|
$tokenRec = require_valid_token();
|
||||||
|
|
||||||
if ($method !== 'GET') {
|
if ($method !== 'GET') {
|
||||||
@ -31,6 +33,15 @@ $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');
|
||||||
|
|
||||||
|
$optionTextMap = [];
|
||||||
|
foreach ($questionIDs as $qid) {
|
||||||
|
$aoStmt = $pdo->prepare("SELECT answerOptionID, defaultText FROM answer_option WHERE questionID = :qid");
|
||||||
|
$aoStmt->execute([':qid' => $qid]);
|
||||||
|
foreach ($aoStmt->fetchAll(PDO::FETCH_ASSOC) as $ao) {
|
||||||
|
$optionTextMap[$ao['answerOptionID']] = $ao['defaultText'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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'];
|
||||||
@ -95,7 +106,14 @@ if (!empty($questionIDs) && !empty($clients)) {
|
|||||||
'answeredAt' => $a['answeredAt'] !== null ? (int)$a['answeredAt'] : null,
|
'answeredAt' => $a['answeredAt'] !== null ? (int)$a['answeredAt'] : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
$c['answers'] = $answerMap;
|
$reachable = qrb_reachable_question_ids($pdo, $qnID, $answerMap, $optionTextMap);
|
||||||
|
$filtered = [];
|
||||||
|
foreach ($answerMap as $qid => $ans) {
|
||||||
|
if (isset($reachable[$qid])) {
|
||||||
|
$filtered[$qid] = $ans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$c['answers'] = $filtered;
|
||||||
}
|
}
|
||||||
unset($c);
|
unset($c);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
155
lib/questionnaire_reachability.php
Normal file
155
lib/questionnaire_reachability.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Branching walk for questionnaire exports/results (matches app all-in-one logic).
|
||||||
|
* Only questions on the active path from current answers are "reachable".
|
||||||
|
*/
|
||||||
|
|
||||||
|
function qrb_load_path_questions(PDO $pdo, string $questionnaireId): array {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT questionID, defaultText, type, orderIndex, configJson
|
||||||
|
FROM question
|
||||||
|
WHERE questionnaireID = :qn
|
||||||
|
ORDER BY orderIndex
|
||||||
|
");
|
||||||
|
$stmt->execute([':qn' => $questionnaireId]);
|
||||||
|
$path = [];
|
||||||
|
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
|
$parts = explode('__', $row['questionID']);
|
||||||
|
$shortId = end($parts);
|
||||||
|
$config = json_decode($row['configJson'] ?? '{}', true) ?: [];
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
$ao = $pdo->prepare("
|
||||||
|
SELECT defaultText, nextQuestionId
|
||||||
|
FROM answer_option
|
||||||
|
WHERE questionID = :qid
|
||||||
|
ORDER BY orderIndex
|
||||||
|
");
|
||||||
|
$ao->execute([':qid' => $row['questionID']]);
|
||||||
|
foreach ($ao->fetchAll(PDO::FETCH_ASSOC) as $opt) {
|
||||||
|
$options[] = [
|
||||||
|
'text' => $opt['defaultText'],
|
||||||
|
'nextQuestionId' => $opt['nextQuestionId'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$symptoms = [];
|
||||||
|
if (!empty($config['symptoms']) && is_array($config['symptoms'])) {
|
||||||
|
$symptoms = $config['symptoms'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$path[] = [
|
||||||
|
'questionID' => $row['questionID'],
|
||||||
|
'shortId' => $shortId,
|
||||||
|
'type' => $row['type'] ?? '',
|
||||||
|
'storageKey' => $row['defaultText'] !== '' ? $row['defaultText'] : $shortId,
|
||||||
|
'options' => $options,
|
||||||
|
'symptoms' => $symptoms,
|
||||||
|
'isLastPage' => ($row['type'] ?? '') === 'last_page',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array> $answerMap keyed by full questionID
|
||||||
|
* @return array<string, bool> reachable full questionIDs
|
||||||
|
*/
|
||||||
|
function qrb_reachable_question_ids(PDO $pdo, string $questionnaireId, array $answerMap, array $optionTextMap): array {
|
||||||
|
$path = qrb_load_path_questions($pdo, $questionnaireId);
|
||||||
|
$reachable = [];
|
||||||
|
$shortToIndex = [];
|
||||||
|
foreach ($path as $idx => $q) {
|
||||||
|
$shortToIndex[$q['shortId']] = $idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
$count = count($path);
|
||||||
|
while ($i < $count) {
|
||||||
|
$q = $path[$i];
|
||||||
|
if ($q['isLastPage']) {
|
||||||
|
$i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reachable[$q['questionID']] = true;
|
||||||
|
|
||||||
|
$nextTarget = qrb_resolve_next_target($q, $answerMap, $optionTextMap);
|
||||||
|
if ($nextTarget !== null) {
|
||||||
|
foreach ($path as $pq) {
|
||||||
|
if ($pq['isLastPage'] && $pq['shortId'] === $nextTarget) {
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($shortToIndex[$nextTarget])) {
|
||||||
|
$i = $shortToIndex[$nextTarget];
|
||||||
|
} else {
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
} elseif (qrb_has_branching_options($q) && !qrb_has_branch_answer($q, $answerMap, $optionTextMap)) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function qrb_resolve_next_target(array $q, array $answerMap, array $optionTextMap): ?string {
|
||||||
|
$answer = $answerMap[$q['questionID']] ?? null;
|
||||||
|
if ($answer === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedText = qrb_answer_display_value($answer, $optionTextMap);
|
||||||
|
if ($selectedText === null || $selectedText === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($q['options'] as $opt) {
|
||||||
|
if ($opt['text'] === $selectedText && $opt['nextQuestionId'] !== '') {
|
||||||
|
return $opt['nextQuestionId'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function qrb_has_branching_options(array $q): bool {
|
||||||
|
foreach ($q['options'] as $opt) {
|
||||||
|
if ($opt['nextQuestionId'] !== '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function qrb_has_branch_answer(array $q, array $answerMap, array $optionTextMap): bool {
|
||||||
|
$answer = $answerMap[$q['questionID']] ?? null;
|
||||||
|
if ($answer === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$v = qrb_answer_display_value($answer, $optionTextMap);
|
||||||
|
return $v !== null && $v !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function qrb_answer_display_value(array $answer, array $optionTextMap): ?string {
|
||||||
|
if (!empty($answer['answerOptionID']) && isset($optionTextMap[$answer['answerOptionID']])) {
|
||||||
|
return $optionTextMap[$answer['answerOptionID']];
|
||||||
|
}
|
||||||
|
if (isset($answer['freeTextValue']) && $answer['freeTextValue'] !== '') {
|
||||||
|
return (string)$answer['freeTextValue'];
|
||||||
|
}
|
||||||
|
if (isset($answer['numericValue']) && $answer['numericValue'] !== null && $answer['numericValue'] !== '') {
|
||||||
|
return (string)$answer['numericValue'];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function qrb_format_cell(array $answer, array $optionTextMap): string {
|
||||||
|
$v = qrb_answer_display_value($answer, $optionTextMap);
|
||||||
|
return $v ?? '';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user