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:
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/questionnaire_structure.php';
|
||||
|
||||
$tokenRec = require_valid_token_web();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
@ -50,12 +52,14 @@ case 'POST':
|
||||
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
||||
try {
|
||||
$chk = $pdo->prepare('SELECT 1 FROM question WHERE questionID = :id');
|
||||
$chk = $pdo->prepare('SELECT questionnaireID FROM question WHERE questionID = :id');
|
||||
$chk->execute([':id' => $qID]);
|
||||
if (!$chk->fetch()) {
|
||||
$qnID = $chk->fetchColumn();
|
||||
if (!$qnID) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('NOT_FOUND', 'Question not found', 404);
|
||||
}
|
||||
$qnID = (string)$qnID;
|
||||
if ($order === 0) {
|
||||
$max = $pdo->prepare('SELECT COALESCE(MAX(orderIndex),0)+1 FROM answer_option WHERE questionID = :id');
|
||||
$max->execute([':id' => $qID]);
|
||||
@ -65,8 +69,11 @@ case 'POST':
|
||||
$pdo->prepare('INSERT INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId) VALUES (:id, :qid, :t, :p, :o, :nq)')
|
||||
->execute([':id' => $id, ':qid' => $qID, ':t' => $optKey, ':p' => $points, ':o' => $order, ':nq' => $nextQ]);
|
||||
qdb_upsert_source_translation($pdo, 'answer_option', $id, $text);
|
||||
$newRev = qdb_bump_structure_revision($pdo, $qnID, 'option_added');
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['answerOption' => [
|
||||
json_success([
|
||||
'structureRevision' => $newRev,
|
||||
'answerOption' => [
|
||||
'answerOptionID' => $id,
|
||||
'questionID' => $qID,
|
||||
'optionKey' => $optKey,
|
||||
@ -115,11 +122,19 @@ case 'PUT':
|
||||
$order = (int)($body['orderIndex'] ?? $row['orderIndex']);
|
||||
$nextQ = trim($body['nextQuestionId'] ?? $row['nextQuestionId']);
|
||||
|
||||
$keyChanged = $optKey !== qdb_option_key($row);
|
||||
$pdo->prepare('UPDATE answer_option SET defaultText = :t, points = :p, orderIndex = :o, nextQuestionId = :nq WHERE answerOptionID = :id')
|
||||
->execute([':t' => $optKey, ':p' => $points, ':o' => $order, ':nq' => $nextQ, ':id' => $id]);
|
||||
qdb_upsert_source_translation($pdo, 'answer_option', $id, $labelGerman);
|
||||
$qnID = qdb_questionnaire_id_for_question($pdo, (string)$row['questionID']);
|
||||
$newRev = null;
|
||||
if ($keyChanged && $qnID !== null) {
|
||||
$newRev = qdb_bump_structure_revision($pdo, $qnID, 'option_key_changed');
|
||||
}
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['answerOption' => [
|
||||
json_success([
|
||||
'structureRevision' => $newRev ?? ($qnID !== null ? qdb_questionnaire_structure_revision($pdo, $qnID) : 1),
|
||||
'answerOption' => [
|
||||
'answerOptionID' => $id,
|
||||
'questionID' => $row['questionID'],
|
||||
'optionKey' => $optKey,
|
||||
@ -144,15 +159,35 @@ case 'DELETE':
|
||||
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
||||
try {
|
||||
$pdo->exec('PRAGMA foreign_keys = OFF;');
|
||||
$existing = $pdo->prepare('SELECT questionID FROM answer_option WHERE answerOptionID = :id');
|
||||
$existing->execute([':id' => $id]);
|
||||
$qid = $existing->fetchColumn();
|
||||
if (!$qid) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('NOT_FOUND', 'Answer option not found', 404);
|
||||
}
|
||||
$qnID = qdb_questionnaire_id_for_question($pdo, (string)$qid);
|
||||
if ($qnID === null) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('NOT_FOUND', 'Question not found', 404);
|
||||
}
|
||||
$pdo->beginTransaction();
|
||||
$newRev = qdb_bump_structure_revision($pdo, $qnID, 'option_removed');
|
||||
if (qdb_option_has_client_data($pdo, $id)) {
|
||||
$pdo->prepare('UPDATE answer_option SET retiredAt = :ts WHERE answerOptionID = :id')
|
||||
->execute([':ts' => time(), ':id' => $id]);
|
||||
$pdo->commit();
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['deleted' => false, 'retired' => true, 'structureRevision' => $newRev]);
|
||||
break;
|
||||
}
|
||||
$pdo->exec('PRAGMA foreign_keys = OFF;');
|
||||
$pdo->prepare('DELETE FROM answer_option_translation WHERE answerOptionID = :id')->execute([':id' => $id]);
|
||||
$pdo->prepare('UPDATE client_answer SET answerOptionID = NULL WHERE answerOptionID = :id')->execute([':id' => $id]);
|
||||
$pdo->prepare('DELETE FROM answer_option WHERE answerOptionID = :id')->execute([':id' => $id]);
|
||||
$pdo->commit();
|
||||
$pdo->exec('PRAGMA foreign_keys = ON;');
|
||||
$pdo->commit();
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['deleted' => true]);
|
||||
json_success(['deleted' => true, 'retired' => false, 'structureRevision' => $newRev]);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Delete answer option', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
|
||||
@ -99,13 +99,24 @@ if ($method === 'POST') {
|
||||
$opened = qdb_open_write_or_fail();
|
||||
[$pdo, $tmpDb, $lockFp] = $opened;
|
||||
|
||||
// Verify questionnaire exists
|
||||
$qnStmt = $pdo->prepare("SELECT questionnaireID FROM questionnaire WHERE questionnaireID = :id");
|
||||
require_once __DIR__ . '/../lib/questionnaire_structure.php';
|
||||
|
||||
$qnStmt = $pdo->prepare(
|
||||
'SELECT questionnaireID, COALESCE(structureRevision, 1) AS structureRevision
|
||||
FROM questionnaire WHERE questionnaireID = :id'
|
||||
);
|
||||
$qnStmt->execute([':id' => $qnID]);
|
||||
if (!$qnStmt->fetch()) {
|
||||
$qnRow = $qnStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$qnRow) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('NOT_FOUND', 'Questionnaire not found', 404);
|
||||
}
|
||||
$currentRev = max(1, (int)$qnRow['structureRevision']);
|
||||
$submitRev = max(1, (int)($body['structureRevision'] ?? 1));
|
||||
if ($submitRev > $currentRev) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('STRUCTURE_REVISION_NEWER', 'App structure revision is newer than server', 400);
|
||||
}
|
||||
|
||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||
$clStmt = $pdo->prepare(
|
||||
@ -122,42 +133,24 @@ if ($method === 'POST') {
|
||||
? ($tokenRec['entityID'] ?? '')
|
||||
: ($clientRow['coachID'] ?? '');
|
||||
|
||||
// Load all questions for this questionnaire: full questionID keyed by short ID
|
||||
$qRows = $pdo->prepare(
|
||||
"SELECT questionID, type, configJson FROM question WHERE questionnaireID = :qn"
|
||||
);
|
||||
$qRows->execute([':qn' => $qnID]);
|
||||
$shortIdMap = []; // shortId -> fullQuestionID
|
||||
$shortIdToType = []; // shortId -> layout type
|
||||
$freeTextMaxLen = []; // fullQuestionID -> maxLength
|
||||
$symptomParentMap = qdb_glass_symptom_parent_map($pdo, $qnID);
|
||||
foreach ($qRows->fetchAll(PDO::FETCH_ASSOC) as $qRow) {
|
||||
$fullId = $qRow['questionID'];
|
||||
$parts = explode('__', $fullId);
|
||||
$short = end($parts);
|
||||
$shortIdMap[$short] = $fullId;
|
||||
$shortIdToType[$short] = $qRow['type'] ?? '';
|
||||
if (($qRow['type'] ?? '') === 'free_text') {
|
||||
$cfg = json_decode($qRow['configJson'] ?? '{}', true) ?: [];
|
||||
$freeTextMaxLen[$fullId] = max(1, min((int)($cfg['maxLength'] ?? 500), 10000));
|
||||
$isLegacySubmit = $submitRev < $currentRev;
|
||||
if ($isLegacySubmit) {
|
||||
$submitManifest = qdb_load_structure_manifest($pdo, $qnID, $submitRev);
|
||||
if ($submitManifest === null) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('STRUCTURE_REVISION_UNKNOWN', 'Unknown structure revision', 400);
|
||||
}
|
||||
} else {
|
||||
$submitManifest = qdb_build_structure_manifest($pdo, $qnID, false);
|
||||
$submitManifest['structureRevision'] = $currentRev;
|
||||
}
|
||||
|
||||
// Load all answer options for this questionnaire: keyed by [questionID][defaultText]
|
||||
$aoRows = $pdo->prepare("
|
||||
SELECT ao.answerOptionID, ao.questionID, ao.defaultText, ao.points
|
||||
FROM answer_option ao
|
||||
JOIN question q ON q.questionID = ao.questionID
|
||||
WHERE q.questionnaireID = :qn
|
||||
");
|
||||
$aoRows->execute([':qn' => $qnID]);
|
||||
$optionMap = []; // fullQuestionID -> [defaultText -> {answerOptionID, points}]
|
||||
foreach ($aoRows->fetchAll(PDO::FETCH_ASSOC) as $ao) {
|
||||
$optionMap[$ao['questionID']][$ao['defaultText']] = [
|
||||
'answerOptionID' => $ao['answerOptionID'],
|
||||
'points' => (int)$ao['points'],
|
||||
];
|
||||
}
|
||||
$maps = qdb_submit_maps_from_manifest($pdo, $qnID, $submitManifest);
|
||||
$shortIdMap = $maps['shortIdMap'];
|
||||
$shortIdToType = $maps['shortIdToType'];
|
||||
$optionMap = $maps['optionMap'];
|
||||
$symptomParentMap = $maps['symptomParentMap'];
|
||||
$freeTextMaxLen = $maps['freeTextMaxLen'];
|
||||
|
||||
require_once __DIR__ . '/../lib/app_submit_validate.php';
|
||||
$validationErrors = qdb_validate_app_submit_payload(
|
||||
@ -167,7 +160,8 @@ if ($method === 'POST') {
|
||||
$shortIdMap,
|
||||
$shortIdToType,
|
||||
$symptomParentMap,
|
||||
$optionMap
|
||||
$optionMap,
|
||||
$submitManifest
|
||||
);
|
||||
if ($validationErrors !== []) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
@ -281,20 +275,25 @@ if ($method === 'POST') {
|
||||
$submittedQuestionIds[$parentQID] = true;
|
||||
}
|
||||
|
||||
// Re-edit uploads omit answers pruned on the device (e.g. branching changes).
|
||||
// Drop live rows for questions not present in this payload.
|
||||
$staleQuestionIds = array_diff(array_values($shortIdMap), array_keys($submittedQuestionIds));
|
||||
if ($staleQuestionIds !== []) {
|
||||
$placeholders = implode(',', array_fill(0, count($staleQuestionIds), '?'));
|
||||
$staleDelete = $pdo->prepare(
|
||||
"DELETE FROM client_answer WHERE clientCode = ? AND questionID IN ($placeholders)"
|
||||
if (!$isLegacySubmit) {
|
||||
$activeMaps = qdb_submit_maps_from_active_questions($pdo, $qnID);
|
||||
$staleQuestionIds = array_diff(
|
||||
array_values($activeMaps['shortIdMap']),
|
||||
array_keys($submittedQuestionIds)
|
||||
);
|
||||
$staleDelete->execute(array_merge([$clientCode], array_values($staleQuestionIds)));
|
||||
if ($staleQuestionIds !== []) {
|
||||
$placeholders = implode(',', array_fill(0, count($staleQuestionIds), '?'));
|
||||
$staleDelete = $pdo->prepare(
|
||||
"DELETE FROM client_answer WHERE clientCode = ? AND questionID IN ($placeholders)"
|
||||
);
|
||||
$staleDelete->execute(array_merge([$clientCode], array_values($staleQuestionIds)));
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert completed_questionnaire record
|
||||
require_once __DIR__ . '/../lib/scoring.php';
|
||||
$sumPoints = qdb_compute_questionnaire_score($pdo, $clientCode, $qnID);
|
||||
$sumPoints = $isLegacySubmit
|
||||
? qdb_compute_questionnaire_score_from_manifest($pdo, $clientCode, $submitManifest)
|
||||
: qdb_compute_questionnaire_score($pdo, $clientCode, $qnID);
|
||||
|
||||
$pdo->prepare("
|
||||
INSERT INTO completed_questionnaire (clientCode, questionnaireID, assignedByCoach, status, startedAt, completedAt, sumPoints)
|
||||
@ -323,7 +322,10 @@ if ($method === 'POST') {
|
||||
$startedAt,
|
||||
$completedAt,
|
||||
$sumPoints,
|
||||
$assignedByCoach
|
||||
$assignedByCoach,
|
||||
$submitRev,
|
||||
$submitManifest,
|
||||
array_keys($submittedQuestionIds)
|
||||
);
|
||||
|
||||
qdb_recompute_profile_scores_for_client($pdo, $clientCode, $qnID);
|
||||
@ -331,7 +333,12 @@ if ($method === 'POST') {
|
||||
$pdo->commit();
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
|
||||
json_success(['submitted' => true, 'sumPoints' => $sumPoints]);
|
||||
json_success([
|
||||
'submitted' => true,
|
||||
'sumPoints' => $sumPoints,
|
||||
'structureRevision' => $submitRev,
|
||||
'legacySubmit' => $isLegacySubmit,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Submit questionnaire', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
@ -473,6 +480,7 @@ if ($fetchClients) {
|
||||
}
|
||||
|
||||
if ($qnID) {
|
||||
require_once __DIR__ . '/../lib/questionnaire_structure.php';
|
||||
$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
|
||||
$qn->execute([':id' => $qnID]);
|
||||
$qnRow = $qn->fetch(PDO::FETCH_ASSOC);
|
||||
@ -481,10 +489,12 @@ if ($qnID) {
|
||||
json_error('NOT_FOUND', 'Questionnaire not found', 404);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT questionID, defaultText, type, orderIndex, configJson
|
||||
FROM question WHERE questionnaireID = :id ORDER BY orderIndex
|
||||
");
|
||||
$structureRevision = qdb_questionnaire_structure_revision($pdo, $qnID);
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT questionID, defaultText, type, orderIndex, configJson
|
||||
FROM question WHERE questionnaireID = :id AND ' . qdb_active_questions_clause('question') . '
|
||||
ORDER BY orderIndex'
|
||||
);
|
||||
$stmt->execute([':id' => $qnID]);
|
||||
$dbQuestions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@ -547,10 +557,11 @@ if ($qnID) {
|
||||
}
|
||||
}
|
||||
|
||||
$ao = $pdo->prepare("
|
||||
SELECT defaultText, points, nextQuestionId
|
||||
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex
|
||||
");
|
||||
$ao = $pdo->prepare(
|
||||
'SELECT defaultText, points, nextQuestionId
|
||||
FROM answer_option WHERE questionID = :qid AND ' . qdb_active_options_clause('answer_option') . '
|
||||
ORDER BY orderIndex'
|
||||
);
|
||||
$ao->execute([':qid' => $dbQ['questionID']]);
|
||||
$opts = $ao->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@ -577,15 +588,19 @@ if ($qnID) {
|
||||
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_success([
|
||||
'meta' => ['id' => $qnID],
|
||||
'questions' => $questions,
|
||||
'translations' => qdb_build_questionnaire_translations_map($pdo, $qnID),
|
||||
'meta' => ['id' => $qnID],
|
||||
'structureRevision' => $structureRevision,
|
||||
'questions' => $questions,
|
||||
'translations' => qdb_build_questionnaire_translations_map($pdo, $qnID),
|
||||
]);
|
||||
}
|
||||
|
||||
// Default: ordered questionnaire list
|
||||
require_once __DIR__ . '/../lib/questionnaire_structure.php';
|
||||
|
||||
$rows = $pdo->query("
|
||||
SELECT questionnaireID, name, showPoints, conditionJson, categoryKey
|
||||
SELECT questionnaireID, name, showPoints, conditionJson, categoryKey,
|
||||
COALESCE(structureRevision, 1) AS structureRevision
|
||||
FROM questionnaire
|
||||
WHERE state = 'active'
|
||||
ORDER BY orderIndex
|
||||
@ -594,10 +609,11 @@ $rows = $pdo->query("
|
||||
$list = [];
|
||||
foreach ($rows as $r) {
|
||||
$item = [
|
||||
'id' => $r['questionnaireID'],
|
||||
'name' => $r['name'],
|
||||
'showPoints' => (bool)(int)$r['showPoints'],
|
||||
'condition' => json_decode($r['conditionJson'], true) ?: new stdClass(),
|
||||
'id' => $r['questionnaireID'],
|
||||
'name' => $r['name'],
|
||||
'showPoints' => (bool)(int)$r['showPoints'],
|
||||
'structureRevision' => max(1, (int)$r['structureRevision']),
|
||||
'condition' => json_decode($r['conditionJson'], true) ?: new stdClass(),
|
||||
];
|
||||
$cat = trim($r['categoryKey'] ?? '');
|
||||
if ($cat !== '') {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/scoring.php';
|
||||
require_once __DIR__ . '/../lib/questionnaire_structure.php';
|
||||
|
||||
$tokenRec = require_valid_token_web();
|
||||
|
||||
@ -14,23 +15,34 @@ case 'GET':
|
||||
try {
|
||||
$opened = qdb_open_read_or_fail();
|
||||
[$pdo, $tmpDb, $lockFp] = $opened;
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson
|
||||
FROM question WHERE questionnaireID = :qid ORDER BY orderIndex
|
||||
");
|
||||
$includeRetired = !empty($_GET['includeRetired']);
|
||||
$qWhere = 'questionnaireID = :qid';
|
||||
if (!$includeRetired) {
|
||||
$qWhere .= ' AND ' . qdb_active_questions_clause('question');
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson,
|
||||
retiredAt, retiredInRevision
|
||||
FROM question WHERE $qWhere ORDER BY orderIndex"
|
||||
);
|
||||
$stmt->execute([':qid' => $qnID]);
|
||||
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$structureRevision = qdb_questionnaire_structure_revision($pdo, $qnID);
|
||||
foreach ($questions as &$q) {
|
||||
$q['isRequired'] = (int)$q['isRequired'];
|
||||
$q['orderIndex'] = (int)$q['orderIndex'];
|
||||
$q['retiredAt'] = (int)($q['retiredAt'] ?? 0);
|
||||
$q['retiredInRevision'] = (int)($q['retiredInRevision'] ?? 0);
|
||||
$q['retired'] = $q['retiredAt'] > 0;
|
||||
$cfg = qdb_parse_config_json($q['configJson']);
|
||||
$q['configJson'] = json_encode($cfg, JSON_UNESCAPED_UNICODE);
|
||||
$q['questionKey'] = qdb_question_key($cfg, $q['defaultText']);
|
||||
$q['localId'] = qdb_question_local_id($q['questionID'], $qnID);
|
||||
$ao = $pdo->prepare("
|
||||
SELECT answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId
|
||||
FROM answer_option WHERE questionID = :qid ORDER BY orderIndex
|
||||
");
|
||||
$ao = $pdo->prepare(
|
||||
"SELECT answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId, retiredAt
|
||||
FROM answer_option WHERE questionID = :qid AND " . qdb_active_options_clause('answer_option') . '
|
||||
ORDER BY orderIndex'
|
||||
);
|
||||
$ao->execute([':qid' => $q['questionID']]);
|
||||
$q['answerOptions'] = $ao->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($q['answerOptions'] as &$opt) {
|
||||
@ -51,7 +63,10 @@ case 'GET':
|
||||
}
|
||||
unset($q);
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_success(['questions' => $questions]);
|
||||
json_success([
|
||||
'questions' => $questions,
|
||||
'structureRevision' => $structureRevision,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Load questions', null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
@ -119,8 +134,11 @@ case 'POST':
|
||||
if ($scoreRules !== [] || in_array($type, ['slider_question', 'value_spinner', 'glass_scale_question'], true)) {
|
||||
qdb_sync_question_score_rules($pdo, $id, $scoreRules);
|
||||
}
|
||||
$newRev = qdb_bump_structure_revision($pdo, $qnID, 'question_added');
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['question' => [
|
||||
json_success([
|
||||
'structureRevision' => $newRev,
|
||||
'question' => [
|
||||
'questionID' => $id, 'questionnaireID' => $qnID, 'defaultText' => $text,
|
||||
'questionKey' => $questionKey, 'localId' => $localId,
|
||||
'type' => $type, 'orderIndex' => $order, 'isRequired' => $req,
|
||||
@ -129,7 +147,8 @@ case 'POST':
|
||||
? qdb_glass_symptoms_with_score_rules($pdo, $id, $config) : [],
|
||||
'scoreRules' => in_array($type, ['slider_question', 'value_spinner'], true)
|
||||
? qdb_get_score_rules_for_question($pdo, $id) : [],
|
||||
]]);
|
||||
],
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Questions', null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
@ -175,6 +194,17 @@ case 'PUT':
|
||||
$body['noteAfter'] ?? ($cfgIn['noteAfter'] ?? null)
|
||||
);
|
||||
$configJson = json_encode($config, JSON_UNESCAPED_UNICODE);
|
||||
$oldKey = qdb_question_key($oldCfg, $row['defaultText']);
|
||||
$oldSymptoms = json_encode($oldCfg['symptoms'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||
$newSymptoms = json_encode($config['symptoms'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||
$structuralChange = ($type !== (string)$row['type'])
|
||||
|| ($req !== (int)$row['isRequired'])
|
||||
|| ($questionKey !== $oldKey)
|
||||
|| ($newSymptoms !== $oldSymptoms);
|
||||
if ((int)($row['retiredAt'] ?? 0) > 0) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('INVALID_FIELD', 'Cannot edit a retired question', 400);
|
||||
}
|
||||
$pdo->prepare("UPDATE question SET defaultText = :t, type = :ty, orderIndex = :o, isRequired = :r, configJson = :cj WHERE questionID = :id")
|
||||
->execute([':t' => $text, ':ty' => $type, ':o' => $order, ':r' => $req, ':cj' => $configJson, ':id' => $id]);
|
||||
qdb_upsert_source_translation($pdo, 'question', $id, $text);
|
||||
@ -186,8 +216,14 @@ case 'PUT':
|
||||
if ($scoreRules !== [] || in_array($type, ['slider_question', 'value_spinner', 'glass_scale_question'], true)) {
|
||||
qdb_sync_question_score_rules($pdo, $id, $scoreRules);
|
||||
}
|
||||
$newRev = null;
|
||||
if ($structuralChange) {
|
||||
$newRev = qdb_bump_structure_revision($pdo, (string)$row['questionnaireID'], 'question_updated');
|
||||
}
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['question' => [
|
||||
json_success([
|
||||
'structureRevision' => $newRev ?? qdb_questionnaire_structure_revision($pdo, (string)$row['questionnaireID']),
|
||||
'question' => [
|
||||
'questionID' => $id, 'questionnaireID' => $row['questionnaireID'],
|
||||
'defaultText' => $text, 'questionKey' => $questionKey,
|
||||
'type' => $type, 'orderIndex' => $order, 'isRequired' => $req,
|
||||
@ -196,7 +232,8 @@ case 'PUT':
|
||||
? qdb_glass_symptoms_with_score_rules($pdo, $id, $config) : [],
|
||||
'scoreRules' => in_array($type, ['slider_question', 'value_spinner'], true)
|
||||
? qdb_get_score_rules_for_question($pdo, $id) : [],
|
||||
]]);
|
||||
],
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Questions', null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
@ -211,22 +248,53 @@ case 'DELETE':
|
||||
$id = $body['questionID'];
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
||||
try {
|
||||
$pdo->exec("PRAGMA foreign_keys = OFF;");
|
||||
$existing = $pdo->prepare('SELECT questionnaireID, retiredAt FROM question WHERE questionID = :id');
|
||||
$existing->execute([':id' => $id]);
|
||||
$row = $existing->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('NOT_FOUND', 'Question not found', 404);
|
||||
}
|
||||
$qnID = (string)$row['questionnaireID'];
|
||||
if ((int)($row['retiredAt'] ?? 0) > 0) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_success(['deleted' => false, 'retired' => true, 'structureRevision' => qdb_questionnaire_structure_revision($pdo, $qnID)]);
|
||||
break;
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
$aoIds = $pdo->prepare("SELECT answerOptionID FROM answer_option WHERE questionID = :id");
|
||||
$newRev = qdb_bump_structure_revision($pdo, $qnID, 'question_removed');
|
||||
|
||||
if (qdb_question_has_client_data($pdo, $id)) {
|
||||
qdb_retire_question($pdo, $id, $newRev);
|
||||
$pdo->commit();
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success([
|
||||
'deleted' => false,
|
||||
'retired' => true,
|
||||
'structureRevision' => $newRev,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
$pdo->exec('PRAGMA foreign_keys = OFF;');
|
||||
$aoIds = $pdo->prepare('SELECT answerOptionID FROM answer_option WHERE questionID = :id');
|
||||
$aoIds->execute([':id' => $id]);
|
||||
foreach ($aoIds->fetchAll(PDO::FETCH_COLUMN) as $aoid) {
|
||||
$pdo->prepare("DELETE FROM answer_option_translation WHERE answerOptionID = :id")->execute([':id' => $aoid]);
|
||||
$pdo->prepare('DELETE FROM answer_option_translation WHERE answerOptionID = :id')->execute([':id' => $aoid]);
|
||||
}
|
||||
$pdo->prepare("DELETE FROM answer_option WHERE questionID = :id")->execute([':id' => $id]);
|
||||
$pdo->prepare("DELETE FROM question_score_rule WHERE questionID = :id")->execute([':id' => $id]);
|
||||
$pdo->prepare("DELETE FROM question_translation WHERE questionID = :id")->execute([':id' => $id]);
|
||||
$pdo->prepare("DELETE FROM client_answer WHERE questionID = :id")->execute([':id' => $id]);
|
||||
$pdo->prepare("DELETE FROM question WHERE questionID = :id")->execute([':id' => $id]);
|
||||
$pdo->prepare('DELETE FROM answer_option WHERE questionID = :id')->execute([':id' => $id]);
|
||||
$pdo->prepare('DELETE FROM question_score_rule WHERE questionID = :id')->execute([':id' => $id]);
|
||||
$pdo->prepare('DELETE FROM question_translation WHERE questionID = :id')->execute([':id' => $id]);
|
||||
$pdo->prepare('DELETE FROM question WHERE questionID = :id')->execute([':id' => $id]);
|
||||
$pdo->exec('PRAGMA foreign_keys = ON;');
|
||||
$pdo->commit();
|
||||
$pdo->exec("PRAGMA foreign_keys = ON;");
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['deleted' => true]);
|
||||
json_success([
|
||||
'deleted' => true,
|
||||
'retired' => false,
|
||||
'structureRevision' => $newRev,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Delete question', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user