adding process completion logic
Some checks failed
PHPUnit / test (push) Has been cancelled

This commit is contained in:
2026-07-01 12:07:41 +02:00
parent 9bd9d9653c
commit 339091e13f
10 changed files with 353 additions and 10 deletions

View File

@ -6,10 +6,17 @@ const CONDITION_MODES = [
{ value: 'always', label: 'Always available' },
{ 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: 'custom', label: 'Custom JSON (advanced)' },
];
const SCORING_BANDS = [
{ value: 'green', label: 'Green' },
{ value: 'yellow', label: 'Yellow' },
{ value: 'red', label: 'Red' },
];
const CHOICE_LAYOUT_TYPES = new Set([
'radio_question',
'multi_check_box_question',
@ -156,6 +163,21 @@ 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())
: [];
return {
mode: 'requires_coach_band',
requires: asRequiresList(obj),
coachBandProfileID: (band.profileID || '').trim(),
coachBandAllowed: allowed,
messageKey,
germanLabel: '',
customJson: '',
};
}
if (Array.isArray(obj.anyOf) && obj.anyOf.length) {
return {
mode: 'any_of',
@ -219,6 +241,20 @@ export function buildConditionJson(form, selfQuestionnaireId = '') {
throw new Error('Add at least one rule for "Any of these rules"');
}
obj = { anyOf: rules };
} else if (form.mode === 'requires_coach_band') {
obj = {};
if (form.requires?.length) {
obj.requiresCompleted = [...form.requires];
}
const profileID = (form.coachBandProfileID || '').trim();
const allowed = (form.coachBandAllowed || []).filter(Boolean);
if (!profileID) {
throw new Error('Select a scoring profile for the coach category rule');
}
if (!allowed.length) {
throw new Error('Select at least one allowed coach category (band)');
}
obj.requiresCoachBand = { profileID, allowed: [...allowed] };
} else {
obj = buildRule({
type: form.mode,
@ -251,6 +287,61 @@ export function buildConditionJson(form, selfQuestionnaireId = '') {
return JSON.stringify(obj);
}
function bandCheckboxesHTML(selected, editable) {
const dis = editable ? '' : ' disabled';
const sel = new Set((selected || []).map(b => String(b).toLowerCase()));
return `<div class="scoring-band-picker" id="condCoachBands">
${SCORING_BANDS.map(b => {
const checked = sel.has(b.value) ? ' checked' : '';
return `<label class="scoring-band-picker-item">
<input type="checkbox" value="${b.value}"${checked}${dis}>
<span class="band-badge band-${b.value}">${esc(b.label)}</span>
</label>`;
}).join('')}
</div>`;
}
function scoringProfileSelectHTML(selectedId, scoringProfiles, editable) {
const dis = editable ? '' : ' disabled';
const list = (scoringProfiles || []).filter(p => p.profileID);
if (!list.length) {
return '<p class="data-toolbar-hint">No active scoring profiles. Create one under Questionnaires → Scoring profiles.</p>';
}
const options = ['<option value="">— Select scoring profile —</option>']
.concat(list.map(p => {
const id = p.profileID;
const sel = selectedId === id ? ' selected' : '';
const label = (p.name || '').trim() || 'Unnamed profile';
return `<option value="${esc(id)}"${sel}>${esc(label)}</option>`;
}));
return `<select id="condCoachBandProfile" class="condition-qn-select scoring-profile-select"${dis}>${options.join('')}</select>`;
}
function coachBandRuleHTML(form, allQuestionnaires, scoringProfiles, selfId, editable) {
return `
<div class="scoring-coach-band-panel condition-rule-block">
<label class="form-label">Optional: must also be completed first</label>
${requiresCheckboxes('condRequires', form.requires || [], allQuestionnaires, selfId, editable)}
<div class="scoring-coach-band-fields">
<div class="form-group">
<label class="form-label">Scoring profile</label>
${scoringProfileSelectHTML(form.coachBandProfileID || '', scoringProfiles, editable)}
</div>
<div class="form-group">
<label class="form-label">Allowed coach categories</label>
${bandCheckboxesHTML(form.coachBandAllowed || [], editable)}
<p class="field-hint">Unlocks only after the coach sets one of these categories during score review.</p>
</div>
</div>
</div>`;
}
function readCoachBandAllowed() {
const root = document.getElementById('condCoachBands');
if (!root) return [];
return [...root.querySelectorAll('input[type=checkbox]:checked')].map(cb => cb.value);
}
function requiresCheckboxes(id, selected, allQuestionnaires, selfId, editable) {
const list = (allQuestionnaires || []).filter(q => q.questionnaireID !== selfId);
if (!list.length) {
@ -377,6 +468,7 @@ export function renderConditionEditorHTML(form, {
questionnaireId,
allQuestionnaires,
questionsByQuestionnaire,
scoringProfiles = [],
germanLabel,
editable,
}) {
@ -412,6 +504,10 @@ export function renderConditionEditorHTML(form, {
}, allQuestionnaires, questionsByQuestionnaire, questionnaireId, editable)}
</div>
<div id="conditionPanelRequiresCoachBand" class="condition-mode-panel" style="display:${form.mode === 'requires_coach_band' ? '' : 'none'}">
${coachBandRuleHTML(form, allQuestionnaires, scoringProfiles, questionnaireId, editable)}
</div>
<div id="conditionPanelAnyOf" class="condition-mode-panel" style="display:${form.mode === 'any_of' ? '' : 'none'}">
<p class="data-toolbar-hint" style="margin:0 0 10px">Unlock if any one rule matches.</p>
<div id="conditionAnyOfRules">
@ -502,6 +598,13 @@ export function readConditionFormFromDOM() {
return form;
}
if (mode === 'requires_coach_band') {
form.requires = readRequires('condRequires');
form.coachBandProfileID = document.getElementById('condCoachBandProfile')?.value?.trim() || '';
form.coachBandAllowed = readCoachBandAllowed();
return form;
}
if (mode === 'any_of') {
form.rules = [];
document.querySelectorAll('#conditionAnyOfRules .condition-anyof-rule').forEach((el, i) => {
@ -572,6 +675,7 @@ export function mountConditionEditor(container, {
questionnaireId,
allQuestionnaires,
questionsByQuestionnaire = {},
scoringProfiles = [],
conditionJson,
germanLabel,
editable,
@ -586,6 +690,7 @@ export function mountConditionEditor(container, {
questionnaireId,
allQuestionnaires,
questionsByQuestionnaire,
scoringProfiles,
germanLabel: label,
editable,
}