diff --git a/website/js/condition-editor.js b/website/js/condition-editor.js
index 4a5be35..17c7921 100644
--- a/website/js/condition-editor.js
+++ b/website/js/condition-editor.js
@@ -7,7 +7,7 @@ const CONDITION_MODES = [
{ value: 'requires', label: 'Requires other questionnaires completed' },
{ value: 'requires_answer', label: 'Requires completions + answer check' },
{ value: 'requires_coach_band', label: 'Requires coach category (band)' },
- { value: 'any_of', label: 'Any of these rules (OR)' },
+ { value: 'any_of', label: 'Any of these rules (OR) — mix prerequisites & bands' },
{ value: 'custom', label: 'Custom JSON (advanced)' },
];
@@ -126,13 +126,45 @@ function sanitizeAnswerRule(rule, selfId) {
};
}
+function emptyCoachBandFields() {
+ return { coachBandProfileID: '', coachBandAllowed: [] };
+}
+
+function parseCoachBandFields(obj) {
+ const band = obj?.requiresCoachBand;
+ if (!band || typeof band !== 'object') return emptyCoachBandFields();
+ const allowed = Array.isArray(band.allowed)
+ ? band.allowed.filter(Boolean).map(v => String(v).toLowerCase())
+ : [];
+ return {
+ coachBandProfileID: (band.profileID || '').trim(),
+ coachBandAllowed: allowed,
+ };
+}
+
+function hasCoachBandFields(rule) {
+ return Boolean(
+ (rule?.coachBandProfileID || '').trim() &&
+ (rule?.coachBandAllowed || []).filter(Boolean).length
+ );
+}
+
function parseRule(obj, selfId = '') {
if (!obj || typeof obj !== 'object') {
- return { type: 'requires', requires: [], questionnaire: '', questionId: '', operator: '==', value: '' };
+ return {
+ type: 'requires',
+ requires: [],
+ questionnaire: '',
+ questionId: '',
+ operator: '==',
+ value: '',
+ ...emptyCoachBandFields(),
+ };
}
if (obj.alwaysAvailable) {
- return { type: 'always' };
+ return { type: 'always', ...emptyCoachBandFields() };
}
+ const coachBand = parseCoachBandFields(obj);
const rule = {
type: hasQuestionCheck(obj) ? 'requires_answer' : 'requires',
requires: asRequiresList(obj),
@@ -140,6 +172,7 @@ function parseRule(obj, selfId = '') {
questionId: obj.questionId || '',
operator: obj.operator === '!=' ? '!=' : '==',
value: obj.value != null ? String(obj.value) : '',
+ ...coachBand,
};
return sanitizeAnswerRule(rule, selfId);
}
@@ -163,16 +196,13 @@ export function parseConditionForm(conditionJson, questionnaireId) {
if (obj.alwaysAvailable === true) {
return { mode: 'always', messageKey: '', germanLabel: '', customJson: '' };
}
- if (obj.requiresCoachBand && typeof obj.requiresCoachBand === 'object') {
- const band = obj.requiresCoachBand;
- const allowed = Array.isArray(band.allowed)
- ? band.allowed.filter(Boolean).map(v => String(v).toLowerCase())
- : [];
+ if (obj.requiresCoachBand && typeof obj.requiresCoachBand === 'object' && !Array.isArray(obj.anyOf)) {
+ const band = parseCoachBandFields(obj);
return {
mode: 'requires_coach_band',
requires: asRequiresList(obj),
- coachBandProfileID: (band.profileID || '').trim(),
- coachBandAllowed: allowed,
+ coachBandProfileID: band.coachBandProfileID,
+ coachBandAllowed: band.coachBandAllowed,
messageKey,
germanLabel: '',
customJson: '',
@@ -216,6 +246,17 @@ function buildRule(rule) {
out.operator = rule.operator === '!=' ? '!=' : '==';
out.value = String(rule.value ?? '').trim();
}
+ const profileID = (rule.coachBandProfileID || '').trim();
+ const allowed = (rule.coachBandAllowed || []).filter(Boolean);
+ if (profileID || allowed.length) {
+ if (!profileID) {
+ throw new Error('Select a scoring profile for the coach category rule (or clear the band checkboxes)');
+ }
+ if (!allowed.length) {
+ throw new Error('Select at least one allowed coach category (band), or clear the scoring profile');
+ }
+ out.requiresCoachBand = { profileID, allowed: [...allowed] };
+ }
return out;
}
@@ -240,6 +281,22 @@ export function buildConditionJson(form, selfQuestionnaireId = '') {
if (!rules.length) {
throw new Error('Add at least one rule for "Any of these rules"');
}
+ for (const rule of form.rules || []) {
+ if (rule.type === 'requires' && !rule.requires?.length && !hasCoachBandFields(rule)) {
+ throw new Error('Each OR rule needs prerequisites and/or a coach category');
+ }
+ if (rule.type === 'requires_answer') {
+ if (!(rule.questionnaire || '').trim()) {
+ throw new Error('Select the questionnaire for each answer-check rule');
+ }
+ if (!(rule.questionId || '').trim()) {
+ throw new Error('Select the question for each answer-check rule');
+ }
+ if (!(rule.value ?? '').toString().trim()) {
+ throw new Error('Enter the expected answer for each answer-check rule');
+ }
+ }
+ }
obj = { anyOf: rules };
} else if (form.mode === 'requires_coach_band') {
obj = {};
@@ -287,10 +344,10 @@ export function buildConditionJson(form, selfQuestionnaireId = '') {
return JSON.stringify(obj);
}
-function bandCheckboxesHTML(selected, editable) {
+function bandCheckboxesHTML(selected, editable, id = 'condCoachBands') {
const dis = editable ? '' : ' disabled';
const sel = new Set((selected || []).map(b => String(b).toLowerCase()));
- return `
+ return `
${SCORING_BANDS.map(b => {
const checked = sel.has(b.value) ? ' checked' : '';
return `
@@ -301,7 +358,7 @@ function bandCheckboxesHTML(selected, editable) {
`;
}
-function scoringProfileSelectHTML(selectedId, scoringProfiles, editable) {
+function scoringProfileSelectHTML(selectedId, scoringProfiles, editable, id = 'condCoachBandProfile') {
const dis = editable ? '' : ' disabled';
const list = (scoringProfiles || []).filter(p => p.profileID);
if (!list.length) {
@@ -309,12 +366,37 @@ function scoringProfileSelectHTML(selectedId, scoringProfiles, editable) {
}
const options = ['
— Select scoring profile — ']
.concat(list.map(p => {
- const id = p.profileID;
- const sel = selectedId === id ? ' selected' : '';
+ const pid = p.profileID;
+ const sel = selectedId === pid ? ' selected' : '';
const label = (p.name || '').trim() || 'Unnamed profile';
- return `
${esc(label)} `;
+ return `
${esc(label)} `;
}));
- return `
${options.join('')} `;
+ return `
${options.join('')} `;
+}
+
+function coachBandFieldsHTML(prefix, rule, scoringProfiles, editable, {
+ optional = false,
+} = {}) {
+ const profileId = `${prefix}CoachBandProfile`;
+ const bandsId = `${prefix}CoachBands`;
+ return `
+
+
${optional
+ ? 'Optional: coach category (leave empty = any band / no category check)'
+ : 'Coach category'}
+ ${optional
+ ? '
Use this when only some paths depend on green/yellow/red. Other OR rules can omit it so the questionnaire unlocks regardless of band.
'
+ : ''}
+
+ Scoring profile
+ ${scoringProfileSelectHTML(rule.coachBandProfileID || '', scoringProfiles, editable, profileId)}
+
+
+
`;
}
function coachBandRuleHTML(form, allQuestionnaires, scoringProfiles, selfId, editable) {
@@ -322,26 +404,23 @@ function coachBandRuleHTML(form, allQuestionnaires, scoringProfiles, selfId, edi
Optional: must also be completed first
${requiresCheckboxes('condRequires', form.requires || [], allQuestionnaires, selfId, editable)}
-
-
- Scoring profile
- ${scoringProfileSelectHTML(form.coachBandProfileID || '', scoringProfiles, editable)}
-
-
-
+ ${coachBandFieldsHTML('cond', form, scoringProfiles, editable, { optional: false })}
`;
}
-function readCoachBandAllowed() {
- const root = document.getElementById('condCoachBands');
+function readCoachBandAllowed(id = 'condCoachBands') {
+ const root = document.getElementById(id);
if (!root) return [];
return [...root.querySelectorAll('input[type=checkbox]:checked')].map(cb => cb.value);
}
+function readCoachBandPrefix(prefix) {
+ return {
+ coachBandProfileID: document.getElementById(`${prefix}CoachBandProfile`)?.value?.trim() || '',
+ coachBandAllowed: readCoachBandAllowed(`${prefix}CoachBands`),
+ };
+}
+
function requiresCheckboxes(id, selected, allQuestionnaires, selfId, editable) {
const list = (allQuestionnaires || []).filter(q => q.questionnaireID !== selfId);
if (!list.length) {
@@ -451,7 +530,10 @@ function answerFieldsHTML(prefix, rule, allQuestionnaires, questionsByQuestionna
`;
}
-function singleRuleHTML(prefix, rule, allQuestionnaires, questionsByQuestionnaire, selfId, editable) {
+function singleRuleHTML(prefix, rule, allQuestionnaires, questionsByQuestionnaire, selfId, editable, {
+ scoringProfiles = [],
+ showOptionalCoachBand = false,
+} = {}) {
const showAnswer = rule.type === 'requires_answer';
return `
@@ -461,6 +543,9 @@ function singleRuleHTML(prefix, rule, allQuestionnaires, questionsByQuestionnair
And answer must match
${answerFieldsHTML(prefix, rule, allQuestionnaires, questionsByQuestionnaire, selfId, editable)}
` : ''}
+ ${showOptionalCoachBand
+ ? coachBandFieldsHTML(prefix, rule, scoringProfiles, editable, { optional: true })
+ : ''}
`;
}
@@ -478,7 +563,13 @@ export function renderConditionEditorHTML(form, {
).join('');
const rules = form.rules?.length ? form.rules : [{
- type: 'requires', requires: [], questionnaire: '', questionId: '', operator: '==', value: '',
+ type: 'requires',
+ requires: [],
+ questionnaire: '',
+ questionId: '',
+ operator: '==',
+ value: '',
+ ...emptyCoachBandFields(),
}];
return `
@@ -509,7 +600,10 @@ export function renderConditionEditorHTML(form, {
-
Unlock if any one rule matches.
+
+ Unlock if any one rule matches. Mix prerequisites and coach categories per rule:
+ e.g. Rule 1 = questionnaires A+B + green band; Rule 2 = questionnaire C only (any band).
+
${rules.map((rule, i) => `
@@ -524,7 +618,10 @@ export function renderConditionEditorHTML(form, {
Requires completions + answer
- ${singleRuleHTML(`anyof${i}`, rule, allQuestionnaires, questionsByQuestionnaire, questionnaireId, editable)}
+ ${singleRuleHTML(`anyof${i}`, rule, allQuestionnaires, questionsByQuestionnaire, questionnaireId, editable, {
+ scoringProfiles,
+ showOptionalCoachBand: true,
+ })}
`).join('')}
@@ -600,8 +697,7 @@ export function readConditionFormFromDOM() {
if (mode === 'requires_coach_band') {
form.requires = readRequires('condRequires');
- form.coachBandProfileID = document.getElementById('condCoachBandProfile')?.value?.trim() || '';
- form.coachBandAllowed = readCoachBandAllowed();
+ Object.assign(form, readCoachBandPrefix('cond'));
return form;
}
@@ -610,7 +706,12 @@ export function readConditionFormFromDOM() {
document.querySelectorAll('#conditionAnyOfRules .condition-anyof-rule').forEach((el, i) => {
const type = el.querySelector('.anyof-rule-type')?.value || 'requires';
const requires = readRequires(`anyof${i}Requires`);
- const rule = { type, requires, ...readAnswerPrefix(`anyof${i}`) };
+ const rule = {
+ type,
+ requires,
+ ...readAnswerPrefix(`anyof${i}`),
+ ...readCoachBandPrefix(`anyof${i}`),
+ };
form.rules.push(rule);
});
}
@@ -716,7 +817,13 @@ export function mountConditionEditor(container, {
form.mode = 'any_of';
form.rules = form.rules || [];
form.rules.push({
- type: 'requires', requires: [], questionnaire: '', questionId: '', operator: '==', value: '',
+ type: 'requires',
+ requires: [],
+ questionnaire: '',
+ questionId: '',
+ operator: '==',
+ value: '',
+ ...emptyCoachBandFields(),
});
label = document.getElementById('conditionGermanLabel')?.value || label;
render();