"Method not allowed"]); exit; } if (!empty($_GET['bundle'])) { $role = $tokenRec['role'] ?? ''; if (!in_array($role, ['admin', 'supervisor'], true)) { header('Content-Type: application/json; charset=UTF-8'); http_response_code(403); echo json_encode(["error" => "Permission denied"]); exit; } [$pdo, $tmpDb, $lockFp] = qdb_open(false); $bundle = qdb_export_all_questionnaires_bundle($pdo); qdb_discard($tmpDb, $lockFp); $filename = 'questionnaires_bundle_' . date('Y-m-d_His') . '.json'; header('Content-Type: application/json; charset=UTF-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); echo json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); 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, type, orderIndex, configJson FROM question WHERE questionnaireID = :id ORDER BY orderIndex"); $qStmt->execute([':id' => $qnID]); $questions = $qStmt->fetchAll(PDO::FETCH_ASSOC); $questionIDs = array_column($questions, 'questionID'); $resultColumns = qdb_results_export_columns($questions, $qnID); // 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 ($resultColumns as $col) { $qid = $col['questionID']; $a = $answerMap[$qid] ?? null; $row[$col['header']] = qdb_results_column_cell_value($col, $a, $optionTextMap); } $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 ($resultColumns as $col) { $header[] = $col['header']; } fputcsv($out, $header); } fclose($out);