import { apiGet, apiPost, apiPut, apiDelete, apiPatch } from '../api.js'; import { canEdit, pageHeaderHTML, showToast } from '../app.js'; import { confirmAction } from '../confirm-modal.js'; export async function sessionsPage() { const app = document.getElementById('app'); app.innerHTML = ` ${pageHeaderHTML('Sessions', '', { subtitle: 'Define which session measures belong to each program session', })}
`; if (canEdit()) { document.getElementById('sessionHeaderActions').innerHTML = ''; } try { const [sessionsData, questionnairesData] = await Promise.all([ apiGet('program_sessions'), apiGet('questionnaires'), ]); renderSessions( sessionsData.sessions || [], questionnairesData.questionnaires || [], sessionsData.settings || {}, ); } catch (e) { showToast(e.message, 'error'); document.getElementById('sessionsContent').innerHTML = `

${esc(e.message)}

`; } } function renderSessions(sessions, questionnaires, settings) { const el = document.getElementById('sessionsContent'); const sorted = [...sessions].sort((a, b) => effectiveSessionNumber(a) - effectiveSessionNumber(b) || String(a.name).localeCompare(String(b.name)), ); const autoRepeat = !!settings.autoRepeatSessions; el.innerHTML = `

Cycle behavior

When enabled, completing every active session starts a new cycle at session 1. Counselors re-complete the assigned session measures for the next cycle.

${canEdit() ? ` ` : `

Auto-repeat is ${autoRepeat ? 'enabled' : 'disabled'}.

`}

Program sessions

Counselors see these sessions in the app. Assign one or more session measures to each visit slot.

${sorted.length ? sorted.map(s => sessionCardHTML(s, questionnaires)).join('') : '

No sessions defined yet. Create one to group session measures by visit.

'}
`; if (!canEdit()) return; document.getElementById('autoRepeatSessionsToggle')?.addEventListener('change', async (e) => { const enabled = e.target.checked; try { await apiPatch('program_sessions', { autoRepeatSessions: enabled }); showToast(enabled ? 'Auto-repeat enabled' : 'Auto-repeat disabled', 'success'); } catch (err) { e.target.checked = !enabled; showToast(err.message, 'error'); } }); document.getElementById('newSessionBtn')?.addEventListener('click', () => { openSessionModal(null, questionnaires, sorted); }); el.querySelectorAll('.edit-session-btn').forEach(btn => { btn.addEventListener('click', () => { const session = sorted.find(x => x.sessionID === btn.dataset.id); openSessionModal(session, questionnaires, sorted); }); }); el.querySelectorAll('.delete-session-btn').forEach(btn => { btn.addEventListener('click', async () => { if (!(await confirmAction({ title: 'Delete session', message: `Delete session "${btn.dataset.name}"?`, confirmLabel: 'Delete', variant: 'danger', }))) return; try { await apiDelete('program_sessions', { sessionID: btn.dataset.id }); showToast('Session deleted', 'success'); await sessionsPage(); } catch (err) { showToast(err.message, 'error'); } }); }); } function sessionCardHTML(session, questionnaires) { const qnById = Object.fromEntries((questionnaires || []).map(q => [q.questionnaireID, q])); const members = session.questionnaires || []; const activeBadge = session.isActive ? 'Active' : 'Inactive'; const memberRows = members.map(m => { const q = qnById[m.questionnaireID]; const name = q?.name || m.name || m.questionnaireID; return `
${esc(name)}
`; }).join(''); return `
${esc(session.name)} ${activeBadge}

Session ${effectiveSessionNumber(session)}

${session.description ? `

${esc(session.description)}

` : ''}
${canEdit() ? `
` : ''}

${members.length} session measure(s)

${memberRows || '

No session measures assigned

'}
`; } function openSessionModal(session, questionnaires, allSessions) { const isEdit = !!session; const members = session?.questionnaires?.length ? session.questionnaires.map(m => ({ questionnaireID: m.questionnaireID, orderIndex: m.orderIndex ?? 0, })) : []; const selectedIds = new Set(members.map(m => m.questionnaireID)); const defaultSessionNumber = (allSessions?.length ?? 0) + 1; const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = ` `; document.body.appendChild(overlay); document.body.classList.add('modal-open'); const picker = overlay.querySelector('#sessQnPicker'); const sortedQn = [...(questionnaires || [])].sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0), ); function renderPicker() { const ordered = members.map(m => ({ ...m, q: sortedQn.find(q => q.questionnaireID === m.questionnaireID), })); const available = sortedQn.filter(q => !selectedIds.has(q.questionnaireID)); picker.innerHTML = ` ${ordered.map((m, idx) => `
${esc(m.q?.name || m.questionnaireID)}
`).join('')} ${available.length ? `
` : ''}`; picker.querySelectorAll('.sess-remove').forEach(btn => { btn.addEventListener('click', () => { const row = btn.closest('.scoring-profile-q-row'); const id = row?.dataset.qn; if (!id) return; selectedIds.delete(id); const i = members.findIndex(m => m.questionnaireID === id); if (i >= 0) members.splice(i, 1); renderPicker(); }); }); picker.querySelectorAll('.sess-move-up').forEach(btn => { btn.addEventListener('click', () => { const row = btn.closest('.scoring-profile-q-row'); const id = row?.dataset.qn; const i = members.findIndex(m => m.questionnaireID === id); if (i > 0) { [members[i - 1], members[i]] = [members[i], members[i - 1]]; renderPicker(); } }); }); picker.querySelectorAll('.sess-move-down').forEach(btn => { btn.addEventListener('click', () => { const row = btn.closest('.scoring-profile-q-row'); const id = row?.dataset.qn; const i = members.findIndex(m => m.questionnaireID === id); if (i >= 0 && i < members.length - 1) { [members[i], members[i + 1]] = [members[i + 1], members[i]]; renderPicker(); } }); }); const addBtn = overlay.querySelector('#sessAddBtn'); if (addBtn) { const freshAddBtn = addBtn.cloneNode(true); addBtn.replaceWith(freshAddBtn); freshAddBtn.addEventListener('click', () => { const sel = overlay.querySelector('#sessAddSelect'); const id = sel?.value; if (!id || selectedIds.has(id)) return; selectedIds.add(id); members.push({ questionnaireID: id, orderIndex: members.length }); renderPicker(); }); } } renderPicker(); const close = () => { overlay.remove(); document.body.classList.remove('modal-open'); }; overlay.querySelector('#sessCancel')?.addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); overlay.querySelector('#sessSave')?.addEventListener('click', async () => { const name = overlay.querySelector('#sessName')?.value.trim(); const description = overlay.querySelector('#sessDesc')?.value.trim() || ''; const sessionNumber = parseInt(overlay.querySelector('#sessNumber')?.value || '1', 10); const isActive = overlay.querySelector('#sessActive')?.checked; if (!name) { showToast('Session name is required', 'error'); return; } const payload = { name, description, sessionNumber: Number.isFinite(sessionNumber) ? Math.max(1, sessionNumber) : 1, isActive, questionnaires: members.map((m, idx) => ({ questionnaireID: m.questionnaireID, orderIndex: idx, })), }; try { if (isEdit) { await apiPut('program_sessions', { sessionID: session.sessionID, ...payload }); showToast('Session saved', 'success'); } else { await apiPost('program_sessions', payload); showToast('Session created', 'success'); } close(); await sessionsPage(); } catch (err) { showToast(err.message, 'error'); } }); } function effectiveSessionNumber(session) { if (!session) return 1; return Math.max(session.sessionNumber ?? 1, (session.orderIndex ?? 0) + 1); } function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }