enhanced questionnaire version handling and handling uploads from outdated questionnaires through automatic revisioning and checks
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
@ -3,6 +3,110 @@
|
||||
* Questionnaire submission versioning (archive each coach upload).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/questionnaire_structure.php';
|
||||
|
||||
/**
|
||||
* Build display columns from a stored structure manifest (historical submissions).
|
||||
*
|
||||
* @return array{questions: array, resultColumns: array, optionTextMap: array, stringLabelCache: array}
|
||||
*/
|
||||
function qdb_display_context_from_manifest(PDO $pdo, array $manifest): array {
|
||||
$stringLabelCache = [];
|
||||
$resultColumns = [];
|
||||
$questions = [];
|
||||
$optionTextMap = [];
|
||||
|
||||
foreach ($manifest['questions'] ?? [] as $qEntry) {
|
||||
if (!is_array($qEntry)) {
|
||||
continue;
|
||||
}
|
||||
$fullId = (string)($qEntry['questionID'] ?? '');
|
||||
if ($fullId === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$qStmt = $pdo->prepare(
|
||||
'SELECT questionID, defaultText, type, orderIndex, configJson FROM question WHERE questionID = :id'
|
||||
);
|
||||
$qStmt->execute([':id' => $fullId]);
|
||||
$qRow = $qStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$qRow) {
|
||||
$symptoms = $qEntry['symptoms'] ?? [];
|
||||
$qRow = [
|
||||
'questionID' => $fullId,
|
||||
'defaultText' => (string)($qEntry['questionKey'] ?? $qEntry['shortId'] ?? $fullId),
|
||||
'type' => (string)($qEntry['type'] ?? ''),
|
||||
'orderIndex' => 0,
|
||||
'configJson' => json_encode(
|
||||
['symptoms' => is_array($symptoms) ? $symptoms : []],
|
||||
JSON_UNESCAPED_UNICODE
|
||||
),
|
||||
];
|
||||
}
|
||||
$questions[] = $qRow;
|
||||
|
||||
$cfg = json_decode($qRow['configJson'] ?? '{}', true) ?: [];
|
||||
$type = $qRow['type'] ?? '';
|
||||
if ($type === 'glass_scale_question') {
|
||||
$symptoms = $cfg['symptoms'] ?? $qEntry['symptoms'] ?? [];
|
||||
if (is_array($symptoms) && $symptoms !== []) {
|
||||
foreach ($symptoms as $symptomKey) {
|
||||
$sk = trim((string)$symptomKey);
|
||||
if ($sk === '') {
|
||||
continue;
|
||||
}
|
||||
$resultColumns[] = [
|
||||
'header' => qdb_string_german_label($pdo, $sk, $stringLabelCache),
|
||||
'questionID' => $fullId,
|
||||
'symptomKey' => $sk,
|
||||
'kind' => 'glass_symptom',
|
||||
'question' => $qRow,
|
||||
];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$resultColumns[] = [
|
||||
'header' => qdb_question_german_label($pdo, $qRow),
|
||||
'questionID' => $fullId,
|
||||
'symptomKey' => null,
|
||||
'kind' => 'question',
|
||||
'question' => $qRow,
|
||||
];
|
||||
|
||||
$aoStmt = $pdo->prepare('SELECT answerOptionID, defaultText FROM answer_option WHERE questionID = :qid');
|
||||
$aoStmt->execute([':qid' => $fullId]);
|
||||
$dbOptions = $aoStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($dbOptions !== []) {
|
||||
foreach ($dbOptions as $ao) {
|
||||
$optionTextMap[$ao['answerOptionID']] = qdb_option_german_label(
|
||||
$pdo,
|
||||
$ao['answerOptionID'],
|
||||
$ao['defaultText']
|
||||
);
|
||||
}
|
||||
} else {
|
||||
foreach ($qEntry['options'] ?? [] as $opt) {
|
||||
if (!is_array($opt)) {
|
||||
continue;
|
||||
}
|
||||
$key = (string)($opt['key'] ?? '');
|
||||
if ($key !== '') {
|
||||
$optionTextMap[$key] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'questions' => $questions,
|
||||
'resultColumns' => $resultColumns,
|
||||
'optionTextMap' => $optionTextMap,
|
||||
'stringLabelCache' => $stringLabelCache,
|
||||
];
|
||||
}
|
||||
|
||||
function qdb_next_submission_version(PDO $pdo, string $clientCode, string $questionnaireID): int {
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT COALESCE(MAX(version), 0) + 1 FROM questionnaire_submission
|
||||
@ -24,11 +128,21 @@ function qdb_record_submission_after_submit(
|
||||
?int $completedAt,
|
||||
int $sumPoints,
|
||||
string $assignedByCoach,
|
||||
int $structureRevision = 1,
|
||||
?array $structureManifest = null,
|
||||
?array $questionIdsToCopy = null,
|
||||
?int $submittedAt = null
|
||||
): string {
|
||||
$version = qdb_next_submission_version($pdo, $clientCode, $questionnaireID);
|
||||
$submissionID = bin2hex(random_bytes(16));
|
||||
$submittedAtValue = $submittedAt ?? $completedAt ?? time();
|
||||
$snapshotJson = '{}';
|
||||
if ($structureManifest !== null) {
|
||||
$encoded = json_encode($structureManifest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($encoded !== false) {
|
||||
$snapshotJson = $encoded;
|
||||
}
|
||||
}
|
||||
|
||||
$cq = $pdo->prepare(
|
||||
'SELECT status FROM completed_questionnaire WHERE clientCode = :cc AND questionnaireID = :qn'
|
||||
@ -40,11 +154,13 @@ function qdb_record_submission_after_submit(
|
||||
'INSERT INTO questionnaire_submission (
|
||||
submissionID, clientCode, questionnaireID, version, submittedAt,
|
||||
submittedByUserID, submittedByRole, assignedByCoach,
|
||||
status, startedAt, completedAt, sumPoints
|
||||
status, startedAt, completedAt, sumPoints,
|
||||
structureRevision, structureSnapshotJson
|
||||
) VALUES (
|
||||
:sid, :cc, :qn, :ver, :sat,
|
||||
:uid, :role, :abc,
|
||||
:st, :sa, :ca, :sp
|
||||
:st, :sa, :ca, :sp,
|
||||
:srev, :snap
|
||||
)'
|
||||
)->execute([
|
||||
':sid' => $submissionID,
|
||||
@ -59,17 +175,31 @@ function qdb_record_submission_after_submit(
|
||||
':sa' => $startedAt,
|
||||
':ca' => $completedAt,
|
||||
':sp' => $sumPoints,
|
||||
':srev' => max(1, $structureRevision),
|
||||
':snap' => $snapshotJson,
|
||||
]);
|
||||
|
||||
$copy = $pdo->prepare(
|
||||
'INSERT INTO client_answer_submission (submissionID, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
|
||||
SELECT :sid, questionID, answerOptionID, freeTextValue, numericValue, answeredAt
|
||||
FROM client_answer
|
||||
WHERE clientCode = :cc AND questionID IN (
|
||||
SELECT questionID FROM question WHERE questionnaireID = :qn
|
||||
)'
|
||||
);
|
||||
$copy->execute([':sid' => $submissionID, ':cc' => $clientCode, ':qn' => $questionnaireID]);
|
||||
if ($questionIdsToCopy !== null && $questionIdsToCopy !== []) {
|
||||
$questionIdsToCopy = array_values(array_unique(array_filter($questionIdsToCopy, 'is_string')));
|
||||
$ph = implode(',', array_fill(0, count($questionIdsToCopy), '?'));
|
||||
$copy = $pdo->prepare(
|
||||
"INSERT INTO client_answer_submission (submissionID, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
|
||||
SELECT ?, questionID, answerOptionID, freeTextValue, numericValue, answeredAt
|
||||
FROM client_answer
|
||||
WHERE clientCode = ? AND questionID IN ($ph)"
|
||||
);
|
||||
$copy->execute(array_merge([$submissionID, $clientCode], $questionIdsToCopy));
|
||||
} else {
|
||||
$copy = $pdo->prepare(
|
||||
'INSERT INTO client_answer_submission (submissionID, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
|
||||
SELECT :sid, questionID, answerOptionID, freeTextValue, numericValue, answeredAt
|
||||
FROM client_answer
|
||||
WHERE clientCode = :cc AND questionID IN (
|
||||
SELECT questionID FROM question WHERE questionnaireID = :qn
|
||||
)'
|
||||
);
|
||||
$copy->execute([':sid' => $submissionID, ':cc' => $clientCode, ':qn' => $questionnaireID]);
|
||||
}
|
||||
|
||||
return $submissionID;
|
||||
}
|
||||
@ -169,6 +299,7 @@ function qdb_export_all_versions_rows(
|
||||
SELECT qs.submissionID, qs.version, qs.submittedAt, qs.submittedByUserID, qs.submittedByRole,
|
||||
qs.completedAt AS submissionCompletedAt, qs.sumPoints AS submissionSumPoints,
|
||||
qs.startedAt AS submissionStartedAt, qs.status AS submissionStatus,
|
||||
qs.structureRevision, qs.structureSnapshotJson,
|
||||
cl.clientCode, cl.coachID,
|
||||
co.username AS coachUsername,
|
||||
sv.username AS supervisorUsername
|
||||
@ -184,16 +315,7 @@ function qdb_export_all_versions_rows(
|
||||
$stmt->execute($params);
|
||||
$submissions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$questionIDs = array_column($questions, 'questionID');
|
||||
$qPlaceholders = !empty($questionIDs)
|
||||
? implode(',', array_fill(0, count($questionIDs), '?'))
|
||||
: "'__none__'";
|
||||
|
||||
$answerStmt = $pdo->prepare("
|
||||
SELECT questionID, answerOptionID, freeTextValue, numericValue
|
||||
FROM client_answer_submission
|
||||
WHERE submissionID = ? AND questionID IN ($qPlaceholders)
|
||||
");
|
||||
$defaultQuestionIDs = array_column($questions, 'questionID');
|
||||
|
||||
$userMap = [];
|
||||
$userStmt = $pdo->query('SELECT userID, username FROM users');
|
||||
@ -203,32 +325,46 @@ function qdb_export_all_versions_rows(
|
||||
|
||||
$rows = [];
|
||||
foreach ($submissions as $s) {
|
||||
$row = [
|
||||
'submissionID' => $s['submissionID'],
|
||||
'version' => (string)(int)$s['version'],
|
||||
'submittedAt' => $s['submittedAt'] ? date('Y-m-d H:i', (int)$s['submittedAt']) : '',
|
||||
'submittedByRole'=> $s['submittedByRole'] ?? '',
|
||||
'submittedBy' => $userMap[$s['submittedByUserID'] ?? ''] ?? ($s['submittedByUserID'] ?? ''),
|
||||
'clientCode' => $s['clientCode'],
|
||||
'coach' => $s['coachUsername'] ?? $s['coachID'],
|
||||
'supervisor' => $s['supervisorUsername'] ?? '',
|
||||
'status' => $s['submissionStatus'] ?? '',
|
||||
'sumPoints' => $s['submissionSumPoints'] ?? '',
|
||||
'startedAt' => $s['submissionStartedAt'] ? date('Y-m-d H:i', (int)$s['submissionStartedAt']) : '',
|
||||
'completedAt' => $s['submissionCompletedAt'] ? date('Y-m-d H:i', (int)$s['submissionCompletedAt']) : '',
|
||||
];
|
||||
|
||||
$bindParams = array_merge([$s['submissionID']], $questionIDs);
|
||||
$answerStmt->execute($bindParams);
|
||||
$answerMap = [];
|
||||
foreach ($answerStmt->fetchAll(PDO::FETCH_ASSOC) as $a) {
|
||||
$answerMap[$a['questionID']] = $a;
|
||||
$snap = json_decode($s['structureSnapshotJson'] ?? '{}', true) ?: [];
|
||||
if (!empty($snap['questions'])) {
|
||||
$snapCtx = qdb_display_context_from_manifest($pdo, $snap);
|
||||
$submissionColumns = $snapCtx['resultColumns'];
|
||||
$submissionOptionMap = $snapCtx['optionTextMap'];
|
||||
$submissionQuestionIDs = array_column($snapCtx['questions'], 'questionID');
|
||||
} else {
|
||||
$submissionColumns = $resultColumns;
|
||||
$submissionOptionMap = $optionTextMap;
|
||||
$submissionQuestionIDs = $defaultQuestionIDs;
|
||||
}
|
||||
|
||||
foreach ($resultColumns as $col) {
|
||||
$row = [
|
||||
'submissionID' => $s['submissionID'],
|
||||
'version' => (string)(int)$s['version'],
|
||||
'structureRevision' => (string)max(1, (int)($s['structureRevision'] ?? 1)),
|
||||
'submittedAt' => $s['submittedAt'] ? date('Y-m-d H:i', (int)$s['submittedAt']) : '',
|
||||
'submittedByRole' => $s['submittedByRole'] ?? '',
|
||||
'submittedBy' => $userMap[$s['submittedByUserID'] ?? ''] ?? ($s['submittedByUserID'] ?? ''),
|
||||
'clientCode' => $s['clientCode'],
|
||||
'coach' => $s['coachUsername'] ?? $s['coachID'],
|
||||
'supervisor' => $s['supervisorUsername'] ?? '',
|
||||
'status' => $s['submissionStatus'] ?? '',
|
||||
'sumPoints' => $s['submissionSumPoints'] ?? '',
|
||||
'startedAt' => $s['submissionStartedAt'] ? date('Y-m-d H:i', (int)$s['submissionStartedAt']) : '',
|
||||
'completedAt' => $s['submissionCompletedAt'] ? date('Y-m-d H:i', (int)$s['submissionCompletedAt']) : '',
|
||||
];
|
||||
|
||||
$answerMap = qdb_load_client_answer_map(
|
||||
$pdo,
|
||||
$s['clientCode'],
|
||||
$submissionQuestionIDs,
|
||||
'client_answer_submission',
|
||||
$s['submissionID']
|
||||
);
|
||||
|
||||
foreach ($submissionColumns as $col) {
|
||||
$qid = $col['questionID'];
|
||||
$a = $answerMap[$qid] ?? null;
|
||||
$row[$col['header']] = qdb_results_column_cell_value($col, $a, $optionTextMap);
|
||||
$row[$col['header']] = qdb_results_column_cell_value($col, $a, $submissionOptionMap);
|
||||
}
|
||||
$rows[] = $row;
|
||||
}
|
||||
@ -671,7 +807,8 @@ function qdb_client_detail(PDO $pdo, array $tokenRec, string $clientCode): array
|
||||
);
|
||||
|
||||
$subStmt = $pdo->prepare(
|
||||
'SELECT submissionID, version, submittedAt, status, sumPoints, completedAt
|
||||
'SELECT submissionID, version, submittedAt, status, sumPoints, completedAt,
|
||||
structureRevision, structureSnapshotJson
|
||||
FROM questionnaire_submission
|
||||
WHERE clientCode = :cc AND questionnaireID = :qn
|
||||
ORDER BY version DESC'
|
||||
@ -679,19 +816,33 @@ function qdb_client_detail(PDO $pdo, array $tokenRec, string $clientCode): array
|
||||
$subStmt->execute([':cc' => $clientCode, ':qn' => $qnID]);
|
||||
$submissions = [];
|
||||
foreach ($subStmt->fetchAll(PDO::FETCH_ASSOC) as $s) {
|
||||
$snap = json_decode($s['structureSnapshotJson'] ?? '{}', true) ?: [];
|
||||
if (!empty($snap['questions'])) {
|
||||
$snapCtx = qdb_display_context_from_manifest($pdo, $snap);
|
||||
$versionColumns = $snapCtx['resultColumns'];
|
||||
$versionOptionMap = $snapCtx['optionTextMap'];
|
||||
$versionStringCache = $snapCtx['stringLabelCache'];
|
||||
$versionQuestionIDs = array_column($snapCtx['questions'], 'questionID');
|
||||
} else {
|
||||
$versionColumns = $resultColumns;
|
||||
$versionOptionMap = $optionTextMap;
|
||||
$versionStringCache = $stringLabelCache;
|
||||
$versionQuestionIDs = $questionIDs;
|
||||
}
|
||||
|
||||
$subMap = qdb_load_client_answer_map(
|
||||
$pdo,
|
||||
$clientCode,
|
||||
$questionIDs,
|
||||
$versionQuestionIDs,
|
||||
'client_answer_submission',
|
||||
$s['submissionID']
|
||||
);
|
||||
$versionAnswers = qdb_answers_display_rows(
|
||||
$pdo,
|
||||
$resultColumns,
|
||||
$versionColumns,
|
||||
$subMap,
|
||||
$optionTextMap,
|
||||
$stringLabelCache,
|
||||
$versionOptionMap,
|
||||
$versionStringCache,
|
||||
$liveMap
|
||||
);
|
||||
$changedCount = count(array_filter(
|
||||
@ -699,14 +850,15 @@ function qdb_client_detail(PDO $pdo, array $tokenRec, string $clientCode): array
|
||||
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,
|
||||
'submissionID' => $s['submissionID'],
|
||||
'version' => (int)$s['version'],
|
||||
'structureRevision' => max(1, (int)($s['structureRevision'] ?? 1)),
|
||||
'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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user