prepare('SELECT answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId FROM answer_option WHERE questionID = :qid ORDER BY orderIndex'); $stmt->execute([':qid' => $qID]); $options = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($options as &$o) { $o['points'] = (int)$o['points']; $o['orderIndex'] = (int)$o['orderIndex']; $o['optionKey'] = qdb_option_key($o); $o['labelGerman'] = qdb_option_german_label($pdo, $o['answerOptionID'], $o['defaultText']); $tr = $pdo->prepare('SELECT languageCode, text FROM answer_option_translation WHERE answerOptionID = :id'); $tr->execute([':id' => $o['answerOptionID']]); $o['translations'] = $tr->fetchAll(PDO::FETCH_ASSOC); } unset($o); qdb_discard($tmpDb, $lockFp); json_success(['answerOptions' => $options]); } catch (Throwable $e) { qdb_handler_fail($e, 'Load answer options', null, $tmpDb ?? null, $lockFp ?? null); } break; case 'POST': require_role(['admin', 'supervisor'], $tokenRec); $body = read_json_body(); if (empty($body['questionID']) || empty($body['optionKey']) || !isset($body['defaultText'])) { json_error('MISSING_FIELDS', 'questionID, optionKey, and defaultText (German label) required', 400); } $id = bin2hex(random_bytes(16)); $qID = $body['questionID']; $optKey = qdb_validate_stable_key((string)$body['optionKey'], 'Option key'); $text = trim($body['defaultText']); qdb_require_non_empty_german($text, 'German label'); $points = (int)($body['points'] ?? 0); $order = (int)($body['orderIndex'] ?? 0); $nextQ = trim($body['nextQuestionId'] ?? ''); [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $chk = $pdo->prepare('SELECT questionnaireID FROM question WHERE questionID = :id'); $chk->execute([':id' => $qID]); $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]); $order = (int)$max->fetchColumn(); } qdb_assert_unique_option_key($pdo, $qID, $optKey); $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([ 'structureRevision' => $newRev, 'answerOption' => [ 'answerOptionID' => $id, 'questionID' => $qID, 'optionKey' => $optKey, 'defaultText' => $optKey, 'labelGerman' => $text, 'points' => $points, 'orderIndex' => $order, 'nextQuestionId' => $nextQ, 'translations' => [], ]]); } catch (Throwable $e) { qdb_handler_fail($e, 'Answer options', null, $tmpDb ?? null, $lockFp ?? null); } break; case 'PUT': require_role(['admin', 'supervisor'], $tokenRec); $body = read_json_body(); if (empty($body['answerOptionID'])) { json_error('MISSING_FIELDS', 'answerOptionID is required', 400); } $id = $body['answerOptionID']; [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $existing = $pdo->prepare('SELECT * FROM answer_option WHERE answerOptionID = :id'); $existing->execute([':id' => $id]); $row = $existing->fetch(PDO::FETCH_ASSOC); if (!$row) { qdb_discard($tmpDb, $lockFp); json_error('NOT_FOUND', 'Answer option not found', 404); } $optKey = isset($body['optionKey']) ? qdb_validate_stable_key((string)$body['optionKey'], 'Option key') : qdb_option_key($row); if ($optKey === '') { json_error('MISSING_FIELDS', 'optionKey is required', 400); } $labelGerman = trim($body['defaultText'] ?? $body['labelGerman'] ?? ''); if ($labelGerman === '') { $labelGerman = qdb_option_german_label($pdo, $id, $row['defaultText']); } qdb_require_non_empty_german($labelGerman, 'German label'); qdb_assert_unique_option_key($pdo, $row['questionID'], $optKey, $id); $points = (int)($body['points'] ?? $row['points']); $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([ 'structureRevision' => $newRev ?? ($qnID !== null ? qdb_questionnaire_structure_revision($pdo, $qnID) : 1), 'answerOption' => [ 'answerOptionID' => $id, 'questionID' => $row['questionID'], 'optionKey' => $optKey, 'defaultText' => $optKey, 'labelGerman' => $labelGerman, 'points' => $points, 'orderIndex' => $order, 'nextQuestionId' => $nextQ, ]]); } catch (Throwable $e) { qdb_handler_fail($e, 'Answer options', null, $tmpDb ?? null, $lockFp ?? null); } break; case 'DELETE': require_role(['admin', 'supervisor'], $tokenRec); $body = read_json_body(); if (empty($body['answerOptionID'])) { json_error('MISSING_FIELDS', 'answerOptionID is required', 400); } $id = $body['answerOptionID']; [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $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('DELETE FROM answer_option WHERE answerOptionID = :id')->execute([':id' => $id]); $pdo->exec('PRAGMA foreign_keys = ON;'); $pdo->commit(); qdb_save($tmpDb, $lockFp); json_success(['deleted' => true, 'retired' => false, 'structureRevision' => $newRev]); } catch (Throwable $e) { qdb_handler_fail($e, 'Delete answer option', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; case 'PATCH': require_role(['admin', 'supervisor'], $tokenRec); $body = read_json_body(); if (empty($body['questionID']) || !is_array($body['order'] ?? null)) { json_error('MISSING_FIELDS', 'questionID and order[] required', 400); } [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $stmt = $pdo->prepare('UPDATE answer_option SET orderIndex = :o WHERE answerOptionID = :id AND questionID = :qid'); foreach ($body['order'] as $idx => $aoid) { $stmt->execute([':o' => $idx, ':id' => $aoid, ':qid' => $body['questionID']]); } qdb_save($tmpDb, $lockFp); json_success(['reordered' => true]); } catch (Throwable $e) { qdb_handler_fail($e, 'Answer options', null, $tmpDb ?? null, $lockFp ?? null); } break; default: json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405); }