*/ 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 $shortIdToType question localId => layout type * @return list> */ 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>, 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> */ 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; }