made client data browsable with dropdown and version diffs
This commit is contained in:
@ -23,11 +23,12 @@ function qdb_record_submission_after_submit(
|
||||
?int $startedAt,
|
||||
?int $completedAt,
|
||||
int $sumPoints,
|
||||
string $assignedByCoach
|
||||
string $assignedByCoach,
|
||||
?int $submittedAt = null
|
||||
): string {
|
||||
$version = qdb_next_submission_version($pdo, $clientCode, $questionnaireID);
|
||||
$submissionID = bin2hex(random_bytes(16));
|
||||
$now = time();
|
||||
$submittedAtValue = $submittedAt ?? $completedAt ?? time();
|
||||
|
||||
$cq = $pdo->prepare(
|
||||
'SELECT status FROM completed_questionnaire WHERE clientCode = :cc AND questionnaireID = :qn'
|
||||
@ -50,7 +51,7 @@ function qdb_record_submission_after_submit(
|
||||
':cc' => $clientCode,
|
||||
':qn' => $questionnaireID,
|
||||
':ver' => $version,
|
||||
':sat' => $now,
|
||||
':sat' => $submittedAtValue,
|
||||
':uid' => $tokenRec['userID'] ?? '',
|
||||
':role' => $tokenRec['role'] ?? '',
|
||||
':abc' => $assignedByCoach !== '' ? $assignedByCoach : null,
|
||||
@ -451,3 +452,285 @@ function qdb_build_server_export_zip(PDO $pdo, array $tokenRec, bool $allVersion
|
||||
|
||||
return $tmpZip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete uploads, answers, and completions for client codes (does not delete client rows).
|
||||
*
|
||||
* @param list<string> $clientCodes
|
||||
* @return array{submissions: int, followup_notes: int, client_answers: int, completed_questionnaires: int}
|
||||
*/
|
||||
function qdb_delete_client_response_data(PDO $pdo, array $clientCodes): array {
|
||||
$clientCodes = array_values(array_unique(array_filter(
|
||||
$clientCodes,
|
||||
static fn($c) => is_string($c) && $c !== ''
|
||||
)));
|
||||
$deleted = [
|
||||
'submissions' => 0,
|
||||
'followup_notes' => 0,
|
||||
'client_answers' => 0,
|
||||
'completed_questionnaires' => 0,
|
||||
];
|
||||
if ($clientCodes === []) {
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
foreach (array_chunk($clientCodes, 200) as $chunk) {
|
||||
$ph = implode(',', array_fill(0, count($chunk), '?'));
|
||||
if (qdb_table_exists($pdo, 'questionnaire_submission')) {
|
||||
$stmt = $pdo->prepare("DELETE FROM questionnaire_submission WHERE clientCode IN ($ph)");
|
||||
$stmt->execute($chunk);
|
||||
$deleted['submissions'] += $stmt->rowCount();
|
||||
}
|
||||
if (qdb_table_exists($pdo, 'client_followup_note')) {
|
||||
$stmt = $pdo->prepare("DELETE FROM client_followup_note WHERE clientCode IN ($ph)");
|
||||
$stmt->execute($chunk);
|
||||
$deleted['followup_notes'] += $stmt->rowCount();
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM client_answer WHERE clientCode IN ($ph)");
|
||||
$stmt->execute($chunk);
|
||||
$deleted['client_answers'] += $stmt->rowCount();
|
||||
$stmt = $pdo->prepare("DELETE FROM completed_questionnaire WHERE clientCode IN ($ph)");
|
||||
$stmt->execute($chunk);
|
||||
$deleted['completed_questionnaires'] += $stmt->rowCount();
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove submission rows assigned to coaches (FK before coach delete).
|
||||
*
|
||||
* @param list<string> $coachIDs
|
||||
*/
|
||||
function qdb_delete_submissions_for_coaches(PDO $pdo, array $coachIDs): int {
|
||||
if (!qdb_table_exists($pdo, 'questionnaire_submission')) {
|
||||
return 0;
|
||||
}
|
||||
$coachIDs = array_values(array_unique(array_filter(
|
||||
$coachIDs,
|
||||
static fn($id) => is_string($id) && $id !== ''
|
||||
)));
|
||||
if ($coachIDs === []) {
|
||||
return 0;
|
||||
}
|
||||
$total = 0;
|
||||
foreach (array_chunk($coachIDs, 200) as $chunk) {
|
||||
$ph = implode(',', array_fill(0, count($chunk), '?'));
|
||||
$stmt = $pdo->prepare("DELETE FROM questionnaire_submission WHERE assignedByCoach IN ($ph)");
|
||||
$stmt->execute($chunk);
|
||||
$total += $stmt->rowCount();
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>> $answerMap questionID => answer row
|
||||
* @param array<string, array<string, mixed>>|null $compareMap live answers to diff against
|
||||
* @return list<array{label: string, value: string, changedFromLive: bool}>
|
||||
*/
|
||||
function qdb_answers_display_rows(
|
||||
PDO $pdo,
|
||||
array $resultColumns,
|
||||
array $answerMap,
|
||||
array $optionTextMap,
|
||||
array &$stringLabelCache,
|
||||
?array $compareMap = null
|
||||
): array {
|
||||
$rows = [];
|
||||
foreach ($resultColumns as $col) {
|
||||
$qid = $col['questionID'];
|
||||
$a = $answerMap[$qid] ?? null;
|
||||
$value = qdb_display_column_cell_value($pdo, $col, $a, $optionTextMap, $stringLabelCache);
|
||||
$changedFromLive = false;
|
||||
if ($compareMap !== null) {
|
||||
$liveValue = qdb_display_column_cell_value(
|
||||
$pdo,
|
||||
$col,
|
||||
$compareMap[$qid] ?? null,
|
||||
$optionTextMap,
|
||||
$stringLabelCache
|
||||
);
|
||||
$changedFromLive = $value !== $liveValue;
|
||||
}
|
||||
$rows[] = [
|
||||
'label' => $col['header'],
|
||||
'value' => $value,
|
||||
'changedFromLive' => $changedFromLive,
|
||||
];
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $questionIDs
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
function qdb_load_client_answer_map(
|
||||
PDO $pdo,
|
||||
string $clientCode,
|
||||
array $questionIDs,
|
||||
string $table = 'client_answer',
|
||||
?string $submissionID = null
|
||||
): array {
|
||||
if ($questionIDs === []) {
|
||||
return [];
|
||||
}
|
||||
$allowed = ['client_answer', 'client_answer_submission'];
|
||||
if (!in_array($table, $allowed, true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ph = implode(',', array_fill(0, count($questionIDs), '?'));
|
||||
if ($table === 'client_answer_submission') {
|
||||
if ($submissionID === null || $submissionID === '') {
|
||||
return [];
|
||||
}
|
||||
$sql = "SELECT questionID, answerOptionID, freeTextValue, numericValue
|
||||
FROM client_answer_submission
|
||||
WHERE submissionID = ? AND questionID IN ($ph)";
|
||||
$params = array_merge([$submissionID], $questionIDs);
|
||||
} else {
|
||||
$sql = "SELECT questionID, answerOptionID, freeTextValue, numericValue
|
||||
FROM client_answer
|
||||
WHERE clientCode = ? AND questionID IN ($ph)";
|
||||
$params = array_merge([$clientCode], $questionIDs);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$map = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$map[$row['questionID']] = $row;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full client response detail for the web clients list (RBAC-scoped).
|
||||
*/
|
||||
function qdb_client_detail(PDO $pdo, array $tokenRec, string $clientCode): array {
|
||||
$clientCode = trim($clientCode);
|
||||
if ($clientCode === '') {
|
||||
json_error('INVALID_FIELD', 'clientCode is required', 400);
|
||||
}
|
||||
|
||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT cl.clientCode, cl.coachID,
|
||||
co.username AS coachUsername,
|
||||
sv.username AS supervisorUsername
|
||||
FROM client cl
|
||||
LEFT JOIN coach co ON co.coachID = cl.coachID
|
||||
LEFT JOIN supervisor sv ON sv.supervisorID = co.supervisorID
|
||||
WHERE cl.clientCode = :cc AND $rbacClause"
|
||||
);
|
||||
$stmt->execute(array_merge([':cc' => $clientCode], $rbacParams));
|
||||
$client = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$client) {
|
||||
json_error('NOT_FOUND', 'Client not found', 404);
|
||||
}
|
||||
|
||||
$qnStmt = $pdo->prepare(
|
||||
"SELECT q.questionnaireID, q.name, q.version AS questionnaireVersion, q.orderIndex
|
||||
FROM questionnaire q
|
||||
WHERE q.questionnaireID IN (
|
||||
SELECT questionnaireID FROM completed_questionnaire WHERE clientCode = :cc
|
||||
UNION
|
||||
SELECT questionnaireID FROM questionnaire_submission WHERE clientCode = :cc2
|
||||
)
|
||||
ORDER BY q.orderIndex, q.name"
|
||||
);
|
||||
$qnStmt->execute([':cc' => $clientCode, ':cc2' => $clientCode]);
|
||||
$qnRows = $qnStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$questionnaires = [];
|
||||
foreach ($qnRows as $qn) {
|
||||
$qnID = $qn['questionnaireID'];
|
||||
$ctx = qdb_display_questionnaire_context($pdo, $qnID);
|
||||
$questionIDs = array_column($ctx['questions'], 'questionID');
|
||||
$resultColumns = $ctx['resultColumns'];
|
||||
$optionTextMap = $ctx['optionTextMap'];
|
||||
$stringLabelCache = $ctx['stringLabelCache'];
|
||||
|
||||
$cqStmt = $pdo->prepare(
|
||||
'SELECT status, sumPoints, startedAt, completedAt
|
||||
FROM completed_questionnaire
|
||||
WHERE clientCode = :cc AND questionnaireID = :qn'
|
||||
);
|
||||
$cqStmt->execute([':cc' => $clientCode, ':qn' => $qnID]);
|
||||
$cq = $cqStmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
$liveMap = qdb_load_client_answer_map($pdo, $clientCode, $questionIDs);
|
||||
$currentAnswers = qdb_answers_display_rows(
|
||||
$pdo,
|
||||
$resultColumns,
|
||||
$liveMap,
|
||||
$optionTextMap,
|
||||
$stringLabelCache,
|
||||
null
|
||||
);
|
||||
|
||||
$subStmt = $pdo->prepare(
|
||||
'SELECT submissionID, version, submittedAt, status, sumPoints, completedAt
|
||||
FROM questionnaire_submission
|
||||
WHERE clientCode = :cc AND questionnaireID = :qn
|
||||
ORDER BY version DESC'
|
||||
);
|
||||
$subStmt->execute([':cc' => $clientCode, ':qn' => $qnID]);
|
||||
$submissions = [];
|
||||
foreach ($subStmt->fetchAll(PDO::FETCH_ASSOC) as $s) {
|
||||
$subMap = qdb_load_client_answer_map(
|
||||
$pdo,
|
||||
$clientCode,
|
||||
$questionIDs,
|
||||
'client_answer_submission',
|
||||
$s['submissionID']
|
||||
);
|
||||
$versionAnswers = qdb_answers_display_rows(
|
||||
$pdo,
|
||||
$resultColumns,
|
||||
$subMap,
|
||||
$optionTextMap,
|
||||
$stringLabelCache,
|
||||
$liveMap
|
||||
);
|
||||
$changedCount = count(array_filter(
|
||||
$versionAnswers,
|
||||
static fn($r) => !empty($r['changedFromLive'])
|
||||
));
|
||||
$submissions[] = [
|
||||
'submissionID' => $s['submissionID'],
|
||||
'version' => (int)$s['version'],
|
||||
'submittedAt' => $s['submittedAt'] ? date('Y-m-d H:i', (int)$s['submittedAt']) : '',
|
||||
'status' => $s['status'] ?? '',
|
||||
'sumPoints' => $s['sumPoints'] !== null ? (int)$s['sumPoints'] : null,
|
||||
'completedAt' => $s['completedAt'] ? date('Y-m-d H:i', (int)$s['completedAt']) : '',
|
||||
'changedCount' => $changedCount,
|
||||
'answers' => $versionAnswers,
|
||||
];
|
||||
}
|
||||
|
||||
$questionnaires[] = [
|
||||
'questionnaireID' => $qnID,
|
||||
'name' => $qn['name'],
|
||||
'questionnaireVersion' => $qn['questionnaireVersion'] ?? '',
|
||||
'status' => $cq['status'] ?? '',
|
||||
'sumPoints' => isset($cq['sumPoints']) ? (int)$cq['sumPoints'] : null,
|
||||
'startedAt' => !empty($cq['startedAt']) ? date('Y-m-d H:i', (int)$cq['startedAt']) : '',
|
||||
'completedAt' => !empty($cq['completedAt']) ? date('Y-m-d H:i', (int)$cq['completedAt']) : '',
|
||||
'submissionCount' => count($submissions),
|
||||
'currentAnswers' => $currentAnswers,
|
||||
'submissions' => $submissions,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'client' => [
|
||||
'clientCode' => $client['clientCode'],
|
||||
'coachID' => $client['coachID'] ?? '',
|
||||
'coachUsername' => $client['coachUsername'] ?? '',
|
||||
'supervisorUsername' => $client['supervisorUsername'] ?? '',
|
||||
],
|
||||
'questionnaires' => $questionnaires,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user