This commit is contained in:
@ -232,6 +232,7 @@ Question fields:
|
|||||||
- `options[].nextQuestionId`: optional conditional navigation target.
|
- `options[].nextQuestionId`: optional conditional navigation target.
|
||||||
- `pointsMap`: optional map from answer option key to point value.
|
- `pointsMap`: optional map from answer option key to point value.
|
||||||
- `textKey`, `textKey1`, `textKey2`, `hint`, `hint1`, `hint2`, `symptoms`, `range`, `constraints`, `precision`, `dateRange`, `minSelection`: optional layout-specific config fields.
|
- `textKey`, `textKey1`, `textKey2`, `hint`, `hint1`, `hint2`, `symptoms`, `range`, `constraints`, `precision`, `dateRange`, `minSelection`: optional layout-specific config fields.
|
||||||
|
- `noteBeforeKey`, `noteAfterKey`: optional translation keys for text shown above / below the question title (editor “note before/after”). Present only when notes are configured; resolve via the questionnaire `translations` map.
|
||||||
- `dateRange` (date_spinner): `"past"` (default) or `"future"` — which years/dates the app offers.
|
- `dateRange` (date_spinner): `"past"` (default) or `"future"` — which years/dates the app offers.
|
||||||
|
|
||||||
Important ID rule: the fetch response returns short question IDs such as `q1`. The server resolves these back to full database IDs during upload, so the Android app should upload the same short IDs it received.
|
Important ID rule: the fetch response returns short question IDs such as `q1`. The server resolves these back to full database IDs during upload, so the Android app should upload the same short IDs it received.
|
||||||
|
|||||||
@ -197,10 +197,17 @@ case 'PUT':
|
|||||||
$oldKey = qdb_question_key($oldCfg, $row['defaultText']);
|
$oldKey = qdb_question_key($oldCfg, $row['defaultText']);
|
||||||
$oldSymptoms = json_encode($oldCfg['symptoms'] ?? [], JSON_UNESCAPED_UNICODE);
|
$oldSymptoms = json_encode($oldCfg['symptoms'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||||
$newSymptoms = json_encode($config['symptoms'] ?? [], JSON_UNESCAPED_UNICODE);
|
$newSymptoms = json_encode($config['symptoms'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||||
|
// Notes live in configJson and are exported as noteBeforeKey/noteAfterKey + translations.
|
||||||
|
// App sync skips detail re-download when structureRevision is unchanged, so note-only
|
||||||
|
// (and other app-visible config) edits must bump the revision.
|
||||||
|
$oldNotes = trim((string)($oldCfg['noteBefore'] ?? '')) . "\0" . trim((string)($oldCfg['noteAfter'] ?? ''));
|
||||||
|
$newNotes = trim((string)($config['noteBefore'] ?? '')) . "\0" . trim((string)($config['noteAfter'] ?? ''));
|
||||||
$structuralChange = ($type !== (string)$row['type'])
|
$structuralChange = ($type !== (string)$row['type'])
|
||||||
|| ($req !== (int)$row['isRequired'])
|
|| ($req !== (int)$row['isRequired'])
|
||||||
|| ($questionKey !== $oldKey)
|
|| ($questionKey !== $oldKey)
|
||||||
|| ($newSymptoms !== $oldSymptoms);
|
|| ($newSymptoms !== $oldSymptoms)
|
||||||
|
|| ($oldNotes !== $newNotes)
|
||||||
|
|| (json_encode($config, JSON_UNESCAPED_UNICODE) !== json_encode($oldCfg, JSON_UNESCAPED_UNICODE));
|
||||||
if ((int)($row['retiredAt'] ?? 0) > 0) {
|
if ((int)($row['retiredAt'] ?? 0) > 0) {
|
||||||
qdb_discard($tmpDb, $lockFp);
|
qdb_discard($tmpDb, $lockFp);
|
||||||
json_error('INVALID_FIELD', 'Cannot edit a retired question', 400);
|
json_error('INVALID_FIELD', 'Cannot edit a retired question', 400);
|
||||||
|
|||||||
@ -54,4 +54,58 @@ final class QuestionnaireEditingTest extends QdbTestCase
|
|||||||
$res = $this->api()->withToken($token, 'GET', 'answer_options');
|
$res = $this->api()->withToken($token, 'GET', 'answer_options');
|
||||||
$this->assertApiError($res, 'MISSING_PARAM', 400);
|
$this->assertApiError($res, 'MISSING_PARAM', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSavingNoteBeforeBumpsStructureRevisionAndExportsKey(): void
|
||||||
|
{
|
||||||
|
$adminToken = $this->api()->loginWebAndGetToken(
|
||||||
|
DatabaseSeeder::ADMIN_USERNAME,
|
||||||
|
DatabaseSeeder::PASSWORD
|
||||||
|
)['token'];
|
||||||
|
$coachLogin = $this->api()->loginMobileAndGetToken(
|
||||||
|
DatabaseSeeder::COACH_USERNAME,
|
||||||
|
DatabaseSeeder::PASSWORD
|
||||||
|
);
|
||||||
|
$f = $this->fixture();
|
||||||
|
|
||||||
|
$before = $this->api()->withMobileToken($coachLogin['token'], 'GET', 'app_questionnaires', null, [
|
||||||
|
'id' => $f->questionnaireId,
|
||||||
|
]);
|
||||||
|
$this->assertApiOk($before);
|
||||||
|
$revBefore = (int)($before['data']['structureRevision'] ?? 1);
|
||||||
|
|
||||||
|
$put = $this->api()->withToken($adminToken, 'PUT', 'questions', [
|
||||||
|
'questionID' => $f->questionId,
|
||||||
|
'questionKey' => 'consent_q',
|
||||||
|
'defaultText' => 'Consent?',
|
||||||
|
'type' => 'single_choice_question',
|
||||||
|
'isRequired' => 1,
|
||||||
|
'configJson' => json_encode(['questionKey' => 'consent_q'], JSON_UNESCAPED_UNICODE),
|
||||||
|
'noteBefore' => 'Bitte dem Klienten deutlich machen: Ohne Einwilligung keine Teilnahme.',
|
||||||
|
'noteAfter' => '',
|
||||||
|
]);
|
||||||
|
$this->assertApiOk($put);
|
||||||
|
$revAfter = (int)($put['data']['structureRevision'] ?? 0);
|
||||||
|
$this->assertGreaterThan($revBefore, $revAfter, 'note-only edit must bump structureRevision for app sync');
|
||||||
|
|
||||||
|
$detail = $this->api()->withMobileToken($coachLogin['token'], 'GET', 'app_questionnaires', null, [
|
||||||
|
'id' => $f->questionnaireId,
|
||||||
|
]);
|
||||||
|
$this->assertApiOk($detail);
|
||||||
|
$questions = $detail['data']['questions'] ?? [];
|
||||||
|
$this->assertIsArray($questions);
|
||||||
|
$matched = null;
|
||||||
|
foreach ($questions as $q) {
|
||||||
|
if (($q['question'] ?? '') === 'consent_q' || ($q['id'] ?? '') === 'q1') {
|
||||||
|
$matched = $q;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertNotNull($matched, 'expected consent question in app export');
|
||||||
|
$this->assertSame('consent_q_note_before', $matched['noteBeforeKey'] ?? null);
|
||||||
|
$de = $detail['data']['translations']['de'] ?? [];
|
||||||
|
$this->assertSame(
|
||||||
|
'Bitte dem Klienten deutlich machen: Ohne Einwilligung keine Teilnahme.',
|
||||||
|
$de['consent_q_note_before'] ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user