This commit is contained in:
@ -33,6 +33,14 @@ const LAYOUT_TYPES = [
|
||||
|
||||
const OPTION_TYPES = new Set(['radio_question', 'multi_check_box_question']);
|
||||
|
||||
const GLASS_SCORE_LEVELS = [
|
||||
{ key: 'never_glass', label: 'Never' },
|
||||
{ key: 'little_glass', label: 'Little' },
|
||||
{ key: 'moderate_glass', label: 'Moderate' },
|
||||
{ key: 'much_glass', label: 'Much' },
|
||||
{ key: 'extreme_glass', label: 'Extreme' },
|
||||
];
|
||||
|
||||
let questionnaire = null;
|
||||
let questions = [];
|
||||
let isNew = false;
|
||||
@ -194,10 +202,79 @@ function glassSymptomsRowsFromQuestion(q) {
|
||||
return q.glassSymptoms.map(r => ({
|
||||
key: (r.key || '').trim(),
|
||||
labelDe: (r.labelDe || '').trim(),
|
||||
scoreLevels: r.scoreLevels || defaultGlassScoreLevels(),
|
||||
}));
|
||||
}
|
||||
const config = parseConfig(q);
|
||||
return (config.symptoms || []).map(key => ({ key: String(key).trim(), labelDe: '' }));
|
||||
return (config.symptoms || []).map(key => ({
|
||||
key: String(key).trim(),
|
||||
labelDe: '',
|
||||
scoreLevels: defaultGlassScoreLevels(),
|
||||
}));
|
||||
}
|
||||
|
||||
function defaultGlassScoreLevels() {
|
||||
return { never_glass: 0, little_glass: 1, moderate_glass: 2, much_glass: 3, extreme_glass: 4 };
|
||||
}
|
||||
|
||||
function valueScoreRulesSectionHTML(prefix, config, scoreRules = []) {
|
||||
const min = Number(config.range?.min ?? 0);
|
||||
const max = Number(config.range?.max ?? 100);
|
||||
const step = Math.max(1, Number(config.step ?? 1));
|
||||
const lo = Math.min(min, max);
|
||||
const hi = Math.max(min, max);
|
||||
const ruleMap = {};
|
||||
for (const r of scoreRules || []) {
|
||||
ruleMap[r.levelKey] = r.points;
|
||||
}
|
||||
const values = [];
|
||||
for (let v = lo; v <= hi; v += step) values.push(v);
|
||||
if (!values.length) values.push(lo);
|
||||
const rows = values.map(v => `
|
||||
<div class="value-score-row">
|
||||
<span class="value-score-label">${v}</span>
|
||||
<input type="number" class="value-score-pts" data-value="${v}" value="${ruleMap[String(v)] ?? 0}" min="0" step="1">
|
||||
</div>`).join('');
|
||||
return `
|
||||
<div class="value-score-rules" id="${prefix}_value_scores">
|
||||
<label style="font-size:.85rem;font-weight:600;color:var(--text-secondary)">Points per value</label>
|
||||
<p class="field-hint">Set points for each selectable value (used when the questionnaire is scored).</p>
|
||||
<div class="value-score-grid">${rows}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function readValueScoreRulesFromForm(prefix) {
|
||||
const container = document.getElementById(`${prefix}_value_scores`);
|
||||
if (!container) return [];
|
||||
const rules = [];
|
||||
container.querySelectorAll('.value-score-pts').forEach(inp => {
|
||||
rules.push({
|
||||
scopeKey: '',
|
||||
levelKey: String(inp.dataset.value),
|
||||
points: parseInt(inp.value || '0', 10) || 0,
|
||||
});
|
||||
});
|
||||
return rules;
|
||||
}
|
||||
|
||||
function wireValueScoreRules(prefix) {
|
||||
const minInp = document.getElementById(`${prefix}_rangeMin`);
|
||||
const maxInp = document.getElementById(`${prefix}_rangeMax`);
|
||||
const stepInp = document.getElementById(`${prefix}_step`);
|
||||
const rerender = () => {
|
||||
const section = document.getElementById(`${prefix}_value_scores`);
|
||||
if (!section) return;
|
||||
const config = {
|
||||
range: {
|
||||
min: parseInt(minInp?.value || '0', 10),
|
||||
max: parseInt(maxInp?.value || '100', 10),
|
||||
},
|
||||
step: parseInt(stepInp?.value || '1', 10) || 1,
|
||||
};
|
||||
const existing = readValueScoreRulesFromForm(prefix);
|
||||
section.outerHTML = valueScoreRulesSectionHTML(prefix, config, existing);
|
||||
};
|
||||
[minInp, maxInp, stepInp].forEach(el => el?.addEventListener('change', rerender));
|
||||
}
|
||||
|
||||
function suggestSymptomKey(label) {
|
||||
@ -227,6 +304,13 @@ function glassSymptomsSectionHTML(rows, prefix) {
|
||||
}
|
||||
|
||||
function glassSymptomRowHTML(prefix, row, idx) {
|
||||
const levels = row.scoreLevels || defaultGlassScoreLevels();
|
||||
const ptsRow = GLASS_SCORE_LEVELS.map(l => `
|
||||
<div class="glass-score-cell">
|
||||
<label>${esc(l.label)}</label>
|
||||
<input type="number" class="glass-score-pts" data-level="${esc(l.key)}"
|
||||
value="${Number(levels[l.key] ?? 0)}" min="0" step="1">
|
||||
</div>`).join('');
|
||||
return `
|
||||
<li class="glass-symptom-row" data-idx="${idx}">
|
||||
<div class="form-row" style="align-items:flex-end;margin-bottom:0">
|
||||
@ -242,6 +326,7 @@ function glassSymptomRowHTML(prefix, row, idx) {
|
||||
</div>
|
||||
<button type="button" class="btn-icon btn-icon-danger glass-symptom-remove" title="Remove">✖</button>
|
||||
</div>
|
||||
<div class="glass-score-grid">${ptsRow}</div>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
@ -252,7 +337,11 @@ function readGlassSymptomsFromForm(prefix) {
|
||||
container.querySelectorAll('.glass-symptom-row').forEach(rowEl => {
|
||||
const key = rowEl.querySelector('.glass-symptom-key')?.value?.trim() || '';
|
||||
const labelDe = rowEl.querySelector('.glass-symptom-label')?.value?.trim() || '';
|
||||
if (key || labelDe) rows.push({ key, labelDe });
|
||||
const scoreLevels = {};
|
||||
rowEl.querySelectorAll('.glass-score-pts').forEach(inp => {
|
||||
scoreLevels[inp.dataset.level] = parseInt(inp.value || '0', 10) || 0;
|
||||
});
|
||||
if (key || labelDe) rows.push({ key, labelDe, scoreLevels });
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
@ -319,7 +408,7 @@ function wireGlassSymptomsSection(prefix) {
|
||||
|
||||
container.querySelector('[data-action=add-glass-symptom]')?.addEventListener('click', () => {
|
||||
const rows = collectRows();
|
||||
rows.push({ key: '', labelDe: '' });
|
||||
rows.push({ key: '', labelDe: '', scoreLevels: defaultGlassScoreLevels() });
|
||||
renderList(rows);
|
||||
const lastKey = container.querySelector('.glass-symptom-row:last-child .glass-symptom-label');
|
||||
lastKey?.focus();
|
||||
@ -477,7 +566,8 @@ function configFormHTML(layout, config, prefix, opts = {}) {
|
||||
break;
|
||||
}
|
||||
case 'value_spinner':
|
||||
case 'slider_question':
|
||||
case 'slider_question': {
|
||||
const scoreRules = opts.scoreRules || [];
|
||||
html = `
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
@ -497,8 +587,10 @@ function configFormHTML(layout, config, prefix, opts = {}) {
|
||||
<label>Unit label (optional)</label>
|
||||
<input type="text" id="${prefix}_unitLabel" value="${esc(config.unitLabel || '')}" placeholder="e.g. years">
|
||||
</div>` : ''}
|
||||
</div>`;
|
||||
</div>
|
||||
${valueScoreRulesSectionHTML(prefix, config, scoreRules)}`;
|
||||
break;
|
||||
}
|
||||
case 'date_spinner': {
|
||||
const notBefore = config.constraints?.notBefore || '';
|
||||
const notAfter = config.constraints?.notAfter || '';
|
||||
@ -694,11 +786,6 @@ function renderEditor() {
|
||||
<label>Order index</label>
|
||||
<input type="number" id="metaOrder" value="${questionnaire.orderIndex ?? 0}" min="0" ${editable ? '' : 'disabled'}>
|
||||
</div>
|
||||
<div class="form-group" style="display:flex;align-items:flex-end;padding-bottom:16px">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="metaShowPoints" ${questionnaire.showPoints ? 'checked' : ''} ${editable ? '' : 'disabled'}> Show points
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Category key (app grouping)</label>
|
||||
<input type="text" id="metaCategoryKey" value="${esc(questionnaire.categoryKey || '')}" placeholder="e.g. health_interview" ${editable ? '' : 'disabled'}>
|
||||
@ -838,7 +925,7 @@ async function saveMeta() {
|
||||
const version = document.getElementById('metaVersion').value.trim();
|
||||
const state = document.getElementById('metaState').value;
|
||||
const orderIndex = parseInt(document.getElementById('metaOrder').value || '0', 10);
|
||||
const showPoints = document.getElementById('metaShowPoints').checked ? 1 : 0;
|
||||
const showPoints = 0;
|
||||
const categoryKey = (document.getElementById('metaCategoryKey')?.value || '').trim();
|
||||
let conditionJson = '{}';
|
||||
let conditionForm = null;
|
||||
@ -971,6 +1058,7 @@ function showAddQuestionForm() {
|
||||
document.getElementById('aq_config_section').innerHTML = configFormHTML(type, {}, 'aq_cfg');
|
||||
wireStringSpinnerOtherToggle('aq_cfg');
|
||||
if (type === 'glass_scale_question') wireGlassSymptomsSection('aq_cfg');
|
||||
if (type === 'slider_question' || type === 'value_spinner') wireValueScoreRules('aq_cfg');
|
||||
const notesEl = document.getElementById('aq_notes_section');
|
||||
if (notesEl) notesEl.innerHTML = notesSectionHTML({}, 'aq_cfg', qKey);
|
||||
}
|
||||
@ -1060,6 +1148,9 @@ function showAddQuestionForm() {
|
||||
if (glassSymptoms.length && !validateGlassSymptoms(glassSymptoms)) return;
|
||||
postBody.glassSymptoms = glassSymptoms;
|
||||
}
|
||||
if (type === 'slider_question' || type === 'value_spinner') {
|
||||
postBody.scoreRules = readValueScoreRulesFromForm('aq_cfg');
|
||||
}
|
||||
if (!validateStableKey(questionKey, 'Question key')) return;
|
||||
if (!text) { showToast('German text is required', 'error'); return; }
|
||||
if (type === 'string_spinner' && !validateStringSpinnerConfig('aq_cfg')) return;
|
||||
@ -1203,7 +1294,10 @@ function renderQuestionBody(q) {
|
||||
</label>
|
||||
${notesSectionHTML(config, `eq_cfg_${q.questionID}`, qKey)}
|
||||
<div id="eq_config_${q.questionID}">
|
||||
${configFormHTML(layout, config, `eq_cfg_${q.questionID}`, { glassSymptoms: glassSymptomsRowsFromQuestion(q) })}
|
||||
${configFormHTML(layout, config, `eq_cfg_${q.questionID}`, {
|
||||
glassSymptoms: glassSymptomsRowsFromQuestion(q),
|
||||
scoreRules: q.scoreRules || [],
|
||||
})}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:8px">
|
||||
<button class="btn btn-primary btn-sm save-q-btn">Save Question</button>
|
||||
@ -1271,6 +1365,9 @@ function renderQuestionBody(q) {
|
||||
if (glassSymptoms.length && !validateGlassSymptoms(glassSymptoms)) return;
|
||||
payload.glassSymptoms = glassSymptoms;
|
||||
}
|
||||
if (type === 'slider_question' || type === 'value_spinner') {
|
||||
payload.scoreRules = readValueScoreRulesFromForm(cfgPrefix);
|
||||
}
|
||||
if (!validateStableKey(questionKey, 'Question key')) return;
|
||||
if (!text) { showToast('German text is required', 'error'); return; }
|
||||
if (type === 'string_spinner' && !validateStringSpinnerConfig(cfgPrefix)) return;
|
||||
@ -1291,15 +1388,19 @@ function renderQuestionBody(q) {
|
||||
const newLayout = document.getElementById(`eq_type_${q.questionID}`).value;
|
||||
const opts = newLayout === 'glass_scale_question'
|
||||
? { glassSymptoms: glassSymptomsRowsFromQuestion(q) }
|
||||
: {};
|
||||
: (newLayout === 'slider_question' || newLayout === 'value_spinner')
|
||||
? { scoreRules: q.scoreRules || [] }
|
||||
: {};
|
||||
document.getElementById(`eq_config_${q.questionID}`).innerHTML =
|
||||
configFormHTML(newLayout, {}, cfgPrefix, opts);
|
||||
wireStringSpinnerOtherToggle(cfgPrefix);
|
||||
if (newLayout === 'glass_scale_question') wireGlassSymptomsSection(cfgPrefix);
|
||||
if (newLayout === 'slider_question' || newLayout === 'value_spinner') wireValueScoreRules(cfgPrefix);
|
||||
});
|
||||
|
||||
wireStringSpinnerOtherToggle(cfgPrefix);
|
||||
if (layout === 'glass_scale_question') wireGlassSymptomsSection(cfgPrefix);
|
||||
if (layout === 'slider_question' || layout === 'value_spinner') wireValueScoreRules(cfgPrefix);
|
||||
|
||||
body.querySelector('.del-q-btn').addEventListener('click', () => deleteQuestion(q));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user