This commit is contained in:
@ -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,
|
||||
}
|
||||
|
||||
@ -938,6 +938,13 @@ async function initConditionEditor(condJson) {
|
||||
if (!mount) return;
|
||||
mount.innerHTML = '<div class="spinner"></div>';
|
||||
await loadQuestionsByQuestionnaire();
|
||||
let scoringProfiles = [];
|
||||
try {
|
||||
const data = await apiGet('scoring_profiles.php');
|
||||
scoringProfiles = (data.profiles || []).filter(p => p.isActive !== 0 && p.isActive !== false);
|
||||
} catch {
|
||||
scoringProfiles = [];
|
||||
}
|
||||
const qid = questionnaire?.questionnaireID || 'new';
|
||||
const parsed = parseConditionForm(condJson, qid);
|
||||
const germanLabel = parsed.messageKey
|
||||
@ -947,6 +954,7 @@ async function initConditionEditor(condJson) {
|
||||
questionnaireId: qid,
|
||||
allQuestionnaires,
|
||||
questionsByQuestionnaire,
|
||||
scoringProfiles,
|
||||
conditionJson: condJson,
|
||||
germanLabel,
|
||||
editable: canEdit(),
|
||||
|
||||
@ -463,6 +463,54 @@ function bandRangesEditorHTML(bands) {
|
||||
<div id="spBandPreview" class="scoring-band-labels"></div>`;
|
||||
}
|
||||
|
||||
const SCORING_BAND_OPTIONS = [
|
||||
{ value: 'green', label: 'Green' },
|
||||
{ value: 'yellow', label: 'Yellow' },
|
||||
{ value: 'red', label: 'Red' },
|
||||
];
|
||||
|
||||
function processCompleteBandsSummaryHTML(profile) {
|
||||
const bands = (profile?.processCompleteBands || [])
|
||||
.map(b => String(b).toLowerCase())
|
||||
.filter(Boolean);
|
||||
if (!bands.length) return '';
|
||||
const chips = bands.map(b => {
|
||||
const label = SCORING_BAND_OPTIONS.find(o => o.value === b)?.label || b;
|
||||
return `<span class="band-badge band-${esc(b)}">${esc(label)}</span>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="scoring-profile-complete-bands">
|
||||
<span class="scoring-profile-complete-label">Process complete when coach sets</span>
|
||||
<span class="scoring-profile-complete-chips">${chips}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function processCompleteBandsEditorHTML(selected, editable) {
|
||||
const dis = editable ? '' : ' disabled';
|
||||
const sel = new Set((selected || []).map(b => String(b).toLowerCase()));
|
||||
return `
|
||||
<div class="scoring-process-complete-panel">
|
||||
<h4 class="scoring-modal-section-title">Process complete</h4>
|
||||
<p class="scoring-process-complete-hint">
|
||||
When the coach sets one of these categories during score review, the app shows the process-complete dialog for that client.
|
||||
</p>
|
||||
<div class="scoring-band-picker" id="spProcessCompleteBands">
|
||||
${SCORING_BAND_OPTIONS.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>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function readProcessCompleteBandsFromModal(overlay) {
|
||||
return [...(overlay.querySelectorAll('#spProcessCompleteBands input[type=checkbox]:checked') || [])]
|
||||
.map(cb => cb.value);
|
||||
}
|
||||
|
||||
function renderScoringProfilesSection(profiles, questionnaires) {
|
||||
const panel = document.getElementById('scoringProfilesPanel');
|
||||
if (!panel) return;
|
||||
@ -542,6 +590,7 @@ function scoringProfileCardHTML(profile, questionnaires) {
|
||||
</div>
|
||||
${profile.description ? `<p class="scoring-profile-desc">${esc(profile.description)}</p>` : ''}
|
||||
<p class="scoring-profile-bands">${bandSummary(profile)}</p>
|
||||
${processCompleteBandsSummaryHTML(profile)}
|
||||
</div>
|
||||
${canEdit() ? `
|
||||
<div class="scoring-profile-card-actions">
|
||||
@ -584,6 +633,7 @@ function openScoringProfileModal(profile, questionnaires, allProfiles) {
|
||||
</label>
|
||||
</div>
|
||||
${bandRangesEditorHTML(initialBands)}
|
||||
${processCompleteBandsEditorHTML(profile?.processCompleteBands || [], canEdit())}
|
||||
<h4 class="scoring-modal-section-title">Questionnaires</h4>
|
||||
<p class="data-toolbar-hint" style="margin:0 0 10px">Add questionnaires and set weight. List order is the display order.</p>
|
||||
<div id="spQnPicker" class="scoring-profile-picker"></div>
|
||||
@ -724,6 +774,7 @@ function openScoringProfileModal(profile, questionnaires, allProfiles) {
|
||||
description,
|
||||
isActive,
|
||||
...bands,
|
||||
processCompleteBands: readProcessCompleteBandsFromModal(overlay),
|
||||
questionnaires: members.map((m, idx) => ({
|
||||
questionnaireID: m.questionnaireID,
|
||||
weight: parseFloat(picker.querySelector(`[data-qn="${m.questionnaireID}"] .sp-weight`)?.value) || m.weight || 1,
|
||||
|
||||
Reference in New Issue
Block a user