prepare($sql); $stmt->execute($params); $clients = $stmt->fetchAll(PDO::FETCH_ASSOC); if (!$exportAll && empty($clients)) { qdb_discard($tmpDb, $lockFp); json_error('NOT_FOUND', 'Client not found or not authorized', 404); } $subStmt = $pdo->prepare(" SELECT qs.submissionID, qs.clientCode, qs.questionnaireID, qs.submissionVersion, qs.status, qs.startedAt, qs.completedAt, qs.uploadedAt, qs.sumPoints, qs.submittedByRole, qn.name AS questionnaireName FROM questionnaire_submission qs JOIN questionnaire qn ON qn.questionnaireID = qs.questionnaireID WHERE qs.clientCode = :cc ORDER BY qs.uploadedAt, qs.submissionVersion "); $ansStmt = $pdo->prepare(" SELECT sa.questionID, sa.answerOptionID, sa.freeTextValue, sa.numericValue, q.defaultText AS questionText, ao.defaultText AS optionText FROM submission_answer sa JOIN question q ON q.questionID = sa.questionID LEFT JOIN answer_option ao ON ao.answerOptionID = sa.answerOptionID WHERE sa.submissionID = :sid ORDER BY q.orderIndex "); $rows = []; foreach ($clients as $cl) { $cc = $cl['clientCode']; $subStmt->execute([':cc' => $cc]); foreach ($subStmt->fetchAll(PDO::FETCH_ASSOC) as $sub) { $ansStmt->execute([':sid' => $sub['submissionID']]); $answers = $ansStmt->fetchAll(PDO::FETCH_ASSOC); if (empty($answers)) { $rows[] = build_export_row($cl, $sub, null); continue; } foreach ($answers as $a) { $rows[] = build_export_row($cl, $sub, $a); } } } qdb_discard($tmpDb, $lockFp); try { [$pdoW, $tmpDbW, $lockFpW] = qdb_open(true); log_activity( $pdoW, $tokenRec, 'client_data_exported', 'client', $exportAll ? '*' : $clientCode, $exportAll ? 'Exported all client data' : "Exported data for $clientCode", ['exportAll' => $exportAll, 'clientCode' => $exportAll ? null : $clientCode, 'rowCount' => count($rows)] ); qdb_save($tmpDbW, $lockFpW); } catch (Throwable $e) { if (isset($tmpDbW, $lockFpW)) qdb_discard($tmpDbW, $lockFpW); error_log('Activity log failed on export: ' . $e->getMessage()); } $filename = $exportAll ? 'clients_export_' . date('Y-m-d') . '.csv' : 'client_' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $clientCode) . '_export.csv'; header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); echo "\xEF\xBB\xBF"; $out = fopen('php://output', 'w'); $headers = [ 'clientCode', 'coach', 'supervisor', 'questionnaireID', 'questionnaireName', 'submissionVersion', 'submissionID', 'status', 'sumPoints', 'startedAt', 'completedAt', 'uploadedAt', 'submittedByRole', 'questionText', 'answerOption', 'freeTextValue', 'numericValue', ]; fputcsv($out, $headers); foreach ($rows as $r) { fputcsv($out, [ $r['clientCode'], $r['coach'], $r['supervisor'], $r['questionnaireID'], $r['questionnaireName'], $r['submissionVersion'], $r['submissionID'], $r['status'], $r['sumPoints'], $r['startedAt'], $r['completedAt'], $r['uploadedAt'], $r['submittedByRole'], $r['questionText'], $r['answerOption'], $r['freeTextValue'], $r['numericValue'], ]); } fclose($out); exit; function build_export_row(array $cl, array $sub, ?array $a): array { $fmt = static function ($ms) { if ($ms === null || $ms === '') return ''; return date('Y-m-d H:i', (int)($ms / 1000)); }; $answerVal = ''; if ($a) { if (!empty($a['optionText'])) { $answerVal = $a['optionText']; } elseif ($a['freeTextValue'] !== null && $a['freeTextValue'] !== '') { $answerVal = $a['freeTextValue']; } elseif ($a['numericValue'] !== null) { $answerVal = (string)$a['numericValue']; } } return [ 'clientCode' => $cl['clientCode'], 'coach' => $cl['coachUsername'] ?? $cl['coachID'] ?? '', 'supervisor' => $cl['supervisorUsername'] ?? '', 'questionnaireID' => $sub['questionnaireID'], 'questionnaireName' => $sub['questionnaireName'] ?? '', 'submissionVersion' => (int)$sub['submissionVersion'], 'submissionID' => $sub['submissionID'], 'status' => $sub['status'], 'sumPoints' => $sub['sumPoints'] !== null ? (int)$sub['sumPoints'] : '', 'startedAt' => $fmt($sub['startedAt']), 'completedAt' => $fmt($sub['completedAt']), 'uploadedAt' => $fmt($sub['uploadedAt']), 'submittedByRole' => $sub['submittedByRole'] ?? '', 'questionText' => $a['questionText'] ?? '', 'answerOption' => $answerVal, 'freeTextValue' => $a['freeTextValue'] ?? '', 'numericValue' => $a['numericValue'] !== null ? $a['numericValue'] : '', ]; }