prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id"); $qn->execute([':id' => $qnID]); $questionnaire = $qn->fetch(PDO::FETCH_ASSOC); if (!$questionnaire) { qdb_discard($tmpDb, $lockFp); json_error('NOT_FOUND', 'Questionnaire not found', 404); } $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'); $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']; } } [$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); $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; $reachable = qrb_reachable_question_ids($pdo, $qnID, $answerMap, $optionTextMap); foreach ($questions as $q) { $qid = $q['questionID']; $header = $q['defaultText']; if (!isset($reachable[$qid])) { $row[$header] = ''; continue; } if (isset($answerMap[$qid])) { $row[$header] = qrb_format_cell($answerMap[$qid], $optionTextMap); } else { $row[$header] = ''; } } $rows[] = $row; } qdb_discard($tmpDb, $lockFp); // CSV output (not JSON -- overrides the router's Content-Type) $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'); 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', 'coachID', 'status', 'sumPoints', 'startedAt', 'completedAt']; foreach ($questions as $q) $header[] = $q['defaultText']; fputcsv($out, $header); } fclose($out);