Add support for app string translations in questionnaire imports, enhance UI strings, and improve question navigation in editor
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
20
common.php
20
common.php
@ -2094,6 +2094,26 @@ function qdb_import_questionnaires_bundle(PDO $pdo, array $bundle, bool $replace
|
||||
if (!is_array($items) || !$items) {
|
||||
json_error('MISSING_FIELDS', 'bundle.questionnaires array is required', 400);
|
||||
}
|
||||
foreach ($bundle['appStringTranslations'] ?? [] as $st) {
|
||||
if (!is_array($st)) {
|
||||
continue;
|
||||
}
|
||||
$sk = trim($st['stringKey'] ?? '');
|
||||
if ($sk === '') {
|
||||
continue;
|
||||
}
|
||||
$map = qdb_normalize_bundle_translations($st['translations'] ?? []);
|
||||
if (!$map) {
|
||||
continue;
|
||||
}
|
||||
if (isset($map[QDB_SOURCE_LANGUAGE])) {
|
||||
qdb_set_app_string_german_label($pdo, $sk, $map[QDB_SOURCE_LANGUAGE]);
|
||||
unset($map[QDB_SOURCE_LANGUAGE]);
|
||||
}
|
||||
if ($map) {
|
||||
qdb_apply_translation_map($pdo, 'app_string', $sk, $map);
|
||||
}
|
||||
}
|
||||
$results = [];
|
||||
foreach ($items as $item) {
|
||||
$results[] = qdb_import_questionnaire_bundle($pdo, $item, $replaceIfExists);
|
||||
|
||||
@ -40,6 +40,10 @@
|
||||
"june",
|
||||
"lay",
|
||||
"consultation_unlock_requires_rhs",
|
||||
"enter_ages_ranges_hint",
|
||||
"questionnaire_unlock_2_rhs",
|
||||
"questionnaire_unlock_3_integration_index",
|
||||
"questionnaire_unlock_4_consultation_results",
|
||||
"locked",
|
||||
"questionnaire_chip_edit",
|
||||
"questionnaire_chip_edit_pending",
|
||||
@ -131,6 +135,10 @@
|
||||
"june": "Juni",
|
||||
"lay": "liegen.",
|
||||
"consultation_unlock_requires_rhs": "Freischaltung nach Abschluss von Demografie und RHS",
|
||||
"enter_ages_ranges_hint": "Bitte Alter als Bereich (z. B. 8–12) oder einzelne Jahre (z. B. 6, 9, 13) eingeben",
|
||||
"questionnaire_unlock_2_rhs": "Verfügbar nach Abschluss von Demografie",
|
||||
"questionnaire_unlock_3_integration_index": "Verfügbar nach Abschluss von Demografie und RHS",
|
||||
"questionnaire_unlock_4_consultation_results": "Verfügbar nach Abschluss von Demografie (Einwilligung unterschrieben), RHS und IPL",
|
||||
"locked": "Gesperrt",
|
||||
"questionnaire_chip_edit": "Bearbeiten",
|
||||
"questionnaire_chip_edit_pending": "Bearbeiten · Upload ausstehend",
|
||||
|
||||
@ -193,19 +193,44 @@ function stringSpinnerOtherEnabled(config) {
|
||||
return Boolean(config.otherNextQuestionId || config.otherOptionKey);
|
||||
}
|
||||
|
||||
function stringSpinnerNextSectionHTML(config, prefix) {
|
||||
/** Question types that use config.nextQuestionId (not per-option branching). */
|
||||
const QUESTION_LEVEL_NEXT_LAYOUTS = new Set([
|
||||
'value_spinner',
|
||||
'slider_question',
|
||||
'free_text',
|
||||
'date_spinner',
|
||||
'multi_check_box_question',
|
||||
'glass_scale_question',
|
||||
'client_coach_code_question',
|
||||
'client_not_signed',
|
||||
'last_page',
|
||||
]);
|
||||
|
||||
function questionNextSectionHTML(config, prefix, { label, hint } = {}) {
|
||||
const next = config.nextQuestionId || '';
|
||||
return `
|
||||
<div class="form-group" style="margin-top:12px">
|
||||
<label>Next question (list selection)</label>
|
||||
<div class="form-group question-next-field" style="margin-top:12px">
|
||||
<label>${label || 'Next question'}</label>
|
||||
<select id="${prefix}_nextQuestionId">
|
||||
<option value="">(next in list order)</option>
|
||||
${branchTargetOptionsHTML(next)}
|
||||
</select>
|
||||
<span class="field-hint">Where to go after a normal spinner choice. <strong>Required</strong> when “Other” targets a question that sits next in order (so list picks can skip it).</span>
|
||||
<span class="field-hint">${hint || 'Where to go after this question. Leave empty to continue in list order.'}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function stringSpinnerNextSectionHTML(config, prefix) {
|
||||
return questionNextSectionHTML(config, prefix, {
|
||||
label: 'Next question (list selection)',
|
||||
hint: 'Where to go after a normal spinner choice. <strong>Required</strong> when “Other” targets a question that sits next in order (so list picks can skip it).',
|
||||
});
|
||||
}
|
||||
|
||||
function readQuestionNextFromForm(prefix, config) {
|
||||
const next = document.getElementById(`${prefix}_nextQuestionId`)?.value?.trim() || '';
|
||||
if (next) config.nextQuestionId = next;
|
||||
}
|
||||
|
||||
function stringSpinnerOtherSectionHTML(config, prefix) {
|
||||
const enabled = stringSpinnerOtherEnabled(config);
|
||||
const otherKey = config.otherOptionKey || 'other_option';
|
||||
@ -288,7 +313,8 @@ function configFormHTML(layout, config, prefix) {
|
||||
<div class="form-group">
|
||||
<label>Extra text key (optional)</label>
|
||||
<input type="text" id="${prefix}_textKey" value="${esc(config.textKey || '')}" placeholder="e.g. resilience_reflection_prompt">
|
||||
</div>`;
|
||||
</div>
|
||||
<p class="field-hint" style="margin-top:8px">Branching: set <strong>Next question</strong> on each answer option below.</p>`;
|
||||
break;
|
||||
case 'multi_check_box_question':
|
||||
html = `
|
||||
@ -429,6 +455,9 @@ function configFormHTML(layout, config, prefix) {
|
||||
</div>`;
|
||||
break;
|
||||
}
|
||||
if (QUESTION_LEVEL_NEXT_LAYOUTS.has(layout)) {
|
||||
html += questionNextSectionHTML(config, prefix);
|
||||
}
|
||||
return html ? `<div class="config-section">${html}</div>` : '';
|
||||
}
|
||||
|
||||
@ -517,6 +546,9 @@ function readConfigFromForm(layout, prefix) {
|
||||
if (val('textKey')) config.textKey = val('textKey');
|
||||
break;
|
||||
}
|
||||
if (QUESTION_LEVEL_NEXT_LAYOUTS.has(layout) || layout === 'string_spinner') {
|
||||
readQuestionNextFromForm(prefix, config);
|
||||
}
|
||||
return JSON.stringify(config);
|
||||
}
|
||||
|
||||
@ -790,6 +822,7 @@ function showAddQuestionForm() {
|
||||
<input type="number" id="aq_opt_pts" value="0" min="0" placeholder="Pts">
|
||||
</div>
|
||||
<div class="form-group" style="width:160px;margin-bottom:0">
|
||||
<label style="font-size:.75rem;color:var(--text-secondary)">Next question</label>
|
||||
<select id="aq_opt_next">
|
||||
<option value="">(next in order)</option>
|
||||
${branchTargetOptionsHTML()}
|
||||
@ -1052,10 +1085,10 @@ function renderQuestionBody(q) {
|
||||
<p><strong>Key:</strong> <code>${esc(qKey || '—')}</code> · <strong>ID:</strong> <code>${esc(localId)}</code></p>
|
||||
<p><strong>German:</strong> ${esc(q.defaultText)}</p>
|
||||
<p><strong>Layout:</strong> ${esc(layoutLabel(layout))}</p>
|
||||
${layout === 'string_spinner' && (config.nextQuestionId || config.otherNextQuestionId)
|
||||
${config.nextQuestionId || config.otherNextQuestionId
|
||||
? `<p><strong>Branches:</strong>
|
||||
${config.nextQuestionId ? `list → <code>${esc(config.nextQuestionId)}</code>` : 'list → (order)'}
|
||||
${config.otherNextQuestionId ? ` · Other (<code>${esc(config.otherOptionKey || 'other_option')}</code>) → <code>${esc(config.otherNextQuestionId)}</code>` : ''}
|
||||
${config.nextQuestionId ? `→ <code>${esc(config.nextQuestionId)}</code>` : ''}
|
||||
${config.otherNextQuestionId ? `${config.nextQuestionId ? ' · ' : ''}Other (<code>${esc(config.otherOptionKey || 'other_option')}</code>) → <code>${esc(config.otherNextQuestionId)}</code>` : ''}
|
||||
</p>`
|
||||
: ''}
|
||||
${Object.keys(config).length ? `<p><strong>Config:</strong> <code>${esc(JSON.stringify(config))}</code></p>` : ''}
|
||||
@ -1265,6 +1298,7 @@ function showEditOptionForm(q, opt) {
|
||||
<input type="number" id="eo_pts_${opt.answerOptionID}" value="${opt.points}" min="0">
|
||||
</div>
|
||||
<div class="form-group" style="flex:1;min-width:160px;margin-bottom:8px">
|
||||
<label style="font-size:.8rem">Next question</label>
|
||||
<select id="eo_next_${opt.answerOptionID}">
|
||||
<option value="">(next in order)</option>
|
||||
${qIds.map(qi => `<option value="${esc(qi.localId)}" ${opt.nextQuestionId === qi.localId ? 'selected' : ''}>${esc(branchTargetLabel(qi))}</option>`).join('')}
|
||||
|
||||
Reference in New Issue
Block a user