made client data browsable with dropdown and version diffs

This commit is contained in:
2026-06-01 13:29:10 +02:00
parent bee7b74e53
commit 3d56abd124
10 changed files with 971 additions and 83 deletions

View File

@ -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,
];
}