258 lines
8.4 KiB
PHP
258 lines
8.4 KiB
PHP
<?php
|
|
/**
|
|
* Mobile app answer ingest/export helpers (submit payload ↔ client_answer rows).
|
|
*/
|
|
|
|
/** Encode multi-select keys the same way the Android app stores them locally. */
|
|
function qdb_encode_multi_check_values(array $keys): string {
|
|
$clean = [];
|
|
foreach ($keys as $k) {
|
|
$k = trim((string)$k);
|
|
if ($k !== '') {
|
|
$clean[$k] = true;
|
|
}
|
|
}
|
|
return json_encode(array_keys($clean), JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
/** @return list<string> */
|
|
function qdb_decode_multi_check_values(?string $raw): array {
|
|
if ($raw === null || trim($raw) === '') {
|
|
return [];
|
|
}
|
|
$trimmed = trim($raw);
|
|
if ($trimmed[0] === '[') {
|
|
$decoded = json_decode($trimmed, true);
|
|
if (is_array($decoded)) {
|
|
$out = [];
|
|
foreach ($decoded as $v) {
|
|
$s = trim((string)$v);
|
|
if ($s !== '') {
|
|
$out[] = $s;
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
}
|
|
$sep = str_contains($trimmed, ',') ? ',' : (str_contains($trimmed, ';') ? ';' : null);
|
|
if ($sep !== null) {
|
|
$parts = [];
|
|
foreach (explode($sep, $trimmed) as $p) {
|
|
$s = trim($p, " \t\n\r\0\x0B\"'");
|
|
if ($s !== '') {
|
|
$parts[] = $s;
|
|
}
|
|
}
|
|
return $parts;
|
|
}
|
|
return [$trimmed];
|
|
}
|
|
|
|
/**
|
|
* Group raw POST answer rows by question short id; merge multi_check option keys.
|
|
*
|
|
* @param array<string, string> $shortIdToType question localId => layout type
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
function qdb_group_app_submit_answers(array $answers, array $shortIdToType, array $symptomParentMap): array {
|
|
$grouped = [];
|
|
$order = [];
|
|
|
|
foreach ($answers as $a) {
|
|
if (!is_array($a)) {
|
|
continue;
|
|
}
|
|
$shortId = trim($a['questionID'] ?? '');
|
|
if ($shortId === '') {
|
|
continue;
|
|
}
|
|
|
|
if (isset($symptomParentMap[$shortId])) {
|
|
$order[] = $shortId;
|
|
$grouped[$shortId] = $a;
|
|
continue;
|
|
}
|
|
|
|
$type = $shortIdToType[$shortId] ?? '';
|
|
if ($type === 'multi_check_box_question') {
|
|
if (!isset($grouped[$shortId])) {
|
|
$order[] = $shortId;
|
|
$grouped[$shortId] = [
|
|
'questionID' => $shortId,
|
|
'multiOptionKeys' => [],
|
|
'answeredAt' => $a['answeredAt'] ?? null,
|
|
];
|
|
}
|
|
$key = trim((string)($a['answerOptionKey'] ?? ''));
|
|
if ($key !== '') {
|
|
$grouped[$shortId]['multiOptionKeys'][$key] = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$order[] = $shortId;
|
|
$grouped[$shortId] = $a;
|
|
}
|
|
|
|
$out = [];
|
|
foreach ($order as $shortId) {
|
|
$row = $grouped[$shortId];
|
|
if (!empty($row['multiOptionKeys'])) {
|
|
$keys = array_keys($row['multiOptionKeys']);
|
|
$out[] = [
|
|
'questionID' => $shortId,
|
|
'freeTextValue' => qdb_encode_multi_check_values($keys),
|
|
'answeredAt' => $row['answeredAt'] ?? null,
|
|
];
|
|
} else {
|
|
unset($row['multiOptionKeys']);
|
|
$out[] = $row;
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Export one client's answers for one questionnaire into app submit shape.
|
|
*
|
|
* @return array{answers: list<array<string, mixed>>, sumPoints: int, completedAt: ?int}
|
|
*/
|
|
function qdb_export_app_questionnaire_answers(
|
|
PDO $pdo,
|
|
string $clientCode,
|
|
string $qnID
|
|
): array {
|
|
$qStmt = $pdo->prepare(
|
|
"SELECT questionID, type, configJson FROM question WHERE questionnaireID = :qn"
|
|
);
|
|
$qStmt->execute([':qn' => $qnID]);
|
|
$shortToFull = [];
|
|
$fullToShort = [];
|
|
$fullToType = [];
|
|
$symptomParentMap = qdb_glass_symptom_parent_map($pdo, $qnID);
|
|
|
|
foreach ($qStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$full = $row['questionID'];
|
|
$parts = explode('__', $full);
|
|
$short = end($parts);
|
|
$shortToFull[$short] = $full;
|
|
$fullToShort[$full] = $short;
|
|
$fullToType[$full] = $row['type'] ?? '';
|
|
}
|
|
|
|
$aoStmt = $pdo->prepare("
|
|
SELECT ao.answerOptionID, ao.questionID, ao.defaultText
|
|
FROM answer_option ao
|
|
JOIN question q ON q.questionID = ao.questionID
|
|
WHERE q.questionnaireID = :qn
|
|
");
|
|
$aoStmt->execute([':qn' => $qnID]);
|
|
$optionIdToKey = [];
|
|
foreach ($aoStmt->fetchAll(PDO::FETCH_ASSOC) as $ao) {
|
|
$optionIdToKey[$ao['answerOptionID']] = $ao['defaultText'];
|
|
}
|
|
|
|
$caStmt = $pdo->prepare("
|
|
SELECT ca.questionID, ca.answerOptionID, ca.freeTextValue, ca.numericValue, ca.answeredAt
|
|
FROM client_answer ca
|
|
JOIN question q ON q.questionID = ca.questionID
|
|
WHERE ca.clientCode = :cc AND q.questionnaireID = :qn
|
|
");
|
|
$caStmt->execute([':cc' => $clientCode, ':qn' => $qnID]);
|
|
$rows = $caStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$export = [];
|
|
foreach ($rows as $row) {
|
|
$fullQid = $row['questionID'];
|
|
$shortId = $fullToShort[$fullQid] ?? '';
|
|
if ($shortId === '') {
|
|
continue;
|
|
}
|
|
$type = $fullToType[$fullQid] ?? '';
|
|
|
|
if ($type === 'glass_scale_question' && ($row['freeTextValue'] ?? '') !== '') {
|
|
$decoded = json_decode($row['freeTextValue'], true);
|
|
if (is_array($decoded)) {
|
|
foreach ($decoded as $symptomKey => $label) {
|
|
$sk = trim((string)$symptomKey);
|
|
$lab = trim((string)$label);
|
|
if ($sk !== '' && $lab !== '') {
|
|
$export[] = [
|
|
'questionID' => $sk,
|
|
'answerOptionKey' => $lab,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($type === 'multi_check_box_question') {
|
|
foreach (qdb_decode_multi_check_values($row['freeTextValue'] ?? null) as $key) {
|
|
$export[] = [
|
|
'questionID' => $shortId,
|
|
'answerOptionKey' => $key,
|
|
];
|
|
}
|
|
if ($export === [] && !empty($row['answerOptionID']) && isset($optionIdToKey[$row['answerOptionID']])) {
|
|
$export[] = [
|
|
'questionID' => $shortId,
|
|
'answerOptionKey' => $optionIdToKey[$row['answerOptionID']],
|
|
];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$entry = ['questionID' => $shortId];
|
|
if (!empty($row['answerOptionID']) && isset($optionIdToKey[$row['answerOptionID']])) {
|
|
$entry['answerOptionKey'] = $optionIdToKey[$row['answerOptionID']];
|
|
}
|
|
if (isset($row['freeTextValue']) && $row['freeTextValue'] !== null && $row['freeTextValue'] !== '') {
|
|
$entry['freeTextValue'] = $row['freeTextValue'];
|
|
}
|
|
if (isset($row['numericValue']) && $row['numericValue'] !== null && $row['numericValue'] !== '') {
|
|
$entry['numericValue'] = (float)$row['numericValue'];
|
|
}
|
|
if (!empty($row['answeredAt'])) {
|
|
$entry['answeredAt'] = (int)$row['answeredAt'];
|
|
}
|
|
$export[] = $entry;
|
|
}
|
|
|
|
$cq = $pdo->prepare(
|
|
"SELECT sumPoints, completedAt FROM completed_questionnaire
|
|
WHERE clientCode = :cc AND questionnaireID = :qn"
|
|
);
|
|
$cq->execute([':cc' => $clientCode, ':qn' => $qnID]);
|
|
$meta = $cq->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
|
|
return [
|
|
'answers' => $export,
|
|
'sumPoints' => isset($meta['sumPoints']) ? (int)$meta['sumPoints'] : 0,
|
|
'completedAt' => isset($meta['completedAt']) ? (int)$meta['completedAt'] : null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Export all completed questionnaires for a client (coach RBAC must be applied by caller).
|
|
*
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
function qdb_export_app_client_answers_bundle(PDO $pdo, string $clientCode): array {
|
|
$stmt = $pdo->prepare(
|
|
"SELECT questionnaireID FROM completed_questionnaire WHERE clientCode = :cc"
|
|
);
|
|
$stmt->execute([':cc' => $clientCode]);
|
|
$items = [];
|
|
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $qnID) {
|
|
$block = qdb_export_app_questionnaire_answers($pdo, $clientCode, $qnID);
|
|
$items[] = [
|
|
'questionnaireID' => $qnID,
|
|
'completedAt' => $block['completedAt'],
|
|
'sumPoints' => $block['sumPoints'],
|
|
'answers' => $block['answers'],
|
|
];
|
|
}
|
|
return $items;
|
|
}
|