import { apiGet } from '../api.js'; import { pageHeaderHTML, showToast } from '../app.js'; const FETCH_LIMIT = 100; const CLIENT_GROUP_PAGE = 10; const UPLOADS_VISIBLE = 5; let coachesList = []; let expandedCoachId = null; /** @type {Record} */ let recentByCoach = {}; /** @type {Record, showAllUploads: Set, filter: string }>} */ let coachDetailUi = {}; let filterSearch = ''; export async function coachesPage() { const app = document.getElementById('app'); app.innerHTML = ` ${pageHeaderHTML('Counselor activity')}
`; try { const data = await apiGet('coaches.php'); coachesList = data.coaches || []; renderCoaches(); } catch (e) { showToast(e.message, 'error'); document.getElementById('coachesContent').innerHTML = `

${esc(e.message)}

`; } } function coachSearchText(c) { return [c.username, c.coachID, c.supervisorUsername].filter(Boolean).join(' '); } function coachHasUploads(c) { return (c.totalSubmissions ?? 0) > 0; } function filteredCoaches() { const q = filterSearch.trim().toLowerCase(); if (!q) return coachesList; return coachesList.filter(c => coachSearchText(c).toLowerCase().includes(q)); } function getCoachUi(coachID) { if (!coachDetailUi[coachID]) { coachDetailUi[coachID] = { clientLimit: CLIENT_GROUP_PAGE, expandedClients: new Set(), showAllUploads: new Set(), filter: '', }; } return coachDetailUi[coachID]; } function groupSubmissionsByClient(submissions) { const map = new Map(); for (const s of submissions) { const code = s.clientCode || ''; if (!map.has(code)) { map.set(code, []); } map.get(code).push(s); } const groups = []; for (const [clientCode, uploads] of map) { uploads.sort((a, b) => String(b.submittedAt).localeCompare(String(a.submittedAt))); groups.push({ clientCode, uploads, count: uploads.length, lastAt: uploads[0]?.submittedAt || '', }); } groups.sort((a, b) => String(b.lastAt).localeCompare(String(a.lastAt))); return groups; } function filterClientGroups(groups, q) { if (!q) return groups; const needle = q.toLowerCase(); return groups.filter(g => { if (g.clientCode.toLowerCase().includes(needle)) return true; return g.uploads.some(u => (u.questionnaireName || '').toLowerCase().includes(needle) || (u.questionnaireID || '').toLowerCase().includes(needle) ); }); } function uploadsTableHTML(uploads) { if (!uploads.length) { return '

No uploads

'; } return ` ${uploads.map(s => ` `).join('')}
WhenQuestionnaireVer.
${esc(s.submittedAt)} ${esc(s.questionnaireName)} ${s.version}
`; } function coachRecentPanelHTML(coachID) { const data = recentByCoach[coachID]; if (!data) { return '

Loading…

'; } const ui = getCoachUi(coachID); const allGroups = groupSubmissionsByClient(data.submissions || []); const groups = filterClientGroups(allGroups, ui.filter.trim()); const visibleGroups = groups.slice(0, ui.clientLimit); const hasMoreClients = groups.length > ui.clientLimit; const hasMoreUploads = (data.submissions?.length ?? 0) < (data.total ?? 0); const summary = data.total ? `${data.total} upload${data.total === 1 ? '' : 's'} · ${allGroups.length} client${allGroups.length === 1 ? '' : 's'}` : 'No uploads'; let groupsHTML = ''; if (!groups.length) { groupsHTML = `

${ui.filter.trim() ? 'No matches' : 'No uploads'}

`; } else { groupsHTML = visibleGroups.map(g => { const expanded = ui.expandedClients.has(g.clientCode); const showAll = ui.showAllUploads.has(g.clientCode); const visibleUploads = showAll ? g.uploads : g.uploads.slice(0, UPLOADS_VISIBLE); const hiddenCount = g.uploads.length - UPLOADS_VISIBLE; return `
${expanded ? `
${uploadsTableHTML(visibleUploads)} ${!showAll && hiddenCount > 0 ? ` ` : ''}
` : ''}
`; }).join(''); } return `
${esc(summary)}${ui.filter.trim() && groups.length !== allGroups.length ? ` · ${groups.length} shown` : ''}
${groupsHTML}
${hasMoreClients ? ` ` : ''} ${hasMoreUploads ? ` ` : ''}
`; } function renderCoaches() { const el = document.getElementById('coachesContent'); if (!coachesList.length) { el.innerHTML = `

No counselors

`; return; } el.innerHTML = `

Track uploads and client load per counselor. Expand a row to browse uploads by client.

CounselorSupervisorClients Total uploads7d30dLast upload
`; document.getElementById('coachListSearch').addEventListener('input', (e) => { filterSearch = e.target.value; const filtered = filteredCoaches(); if ( expandedCoachId && !filtered.some(c => c.coachID === expandedCoachId && coachHasUploads(c)) ) { expandedCoachId = null; } renderCoachTableBody(); }); renderCoachTableBody(); } function coachRowHTML(c) { const canExpand = coachHasUploads(c); const expanded = canExpand && expandedCoachId === c.coachID; const expandControl = canExpand ? ` ` : ''; return ` ${expandControl}${esc(c.username)} ${esc(c.supervisorUsername || '—')} ${c.clientCount ?? 0} ${c.totalSubmissions ?? 0} ${c.submissionsLast7d ?? 0} ${c.submissionsLast30d ?? 0} ${esc(c.lastSubmissionAt || '—')} ${expanded ? ` ${coachRecentPanelHTML(c.coachID)} ` : ''}`; } function wireCoachDetailPanel(coachID) { const panel = document.querySelector(`[data-coach-panel="${CSS.escape(coachID)}"]`); if (!panel) return; panel.querySelector('.coach-detail-search')?.addEventListener('input', (e) => { getCoachUi(coachID).filter = e.target.value; getCoachUi(coachID).clientLimit = CLIENT_GROUP_PAGE; renderCoachTableBody(); }); panel.querySelectorAll('.coach-client-toggle').forEach(btn => { btn.addEventListener('click', () => { const ui = getCoachUi(coachID); const client = btn.dataset.client; if (ui.expandedClients.has(client)) { ui.expandedClients.delete(client); } else { ui.expandedClients.add(client); } renderCoachTableBody(); }); }); panel.querySelectorAll('.coach-show-all-uploads').forEach(btn => { btn.addEventListener('click', () => { getCoachUi(coachID).showAllUploads.add(btn.dataset.client); renderCoachTableBody(); }); }); panel.querySelector('.coach-more-clients')?.addEventListener('click', () => { getCoachUi(coachID).clientLimit += CLIENT_GROUP_PAGE; renderCoachTableBody(); }); panel.querySelector('.coach-more-uploads')?.addEventListener('click', () => { loadMoreCoachUploads(coachID); }); } function renderCoachTableBody() { const body = document.getElementById('coachTableBody'); const countEl = document.getElementById('coachListCount'); if (!body) return; const filtered = filteredCoaches(); if (!filtered.length) { body.innerHTML = ` No counselors match `; if (countEl) { countEl.textContent = filterSearch.trim() ? `0 of ${coachesList.length} counselors` : ''; } return; } body.innerHTML = filtered.map(c => coachRowHTML(c)).join(''); if (countEl) { const q = filterSearch.trim(); countEl.textContent = q ? `${filtered.length} of ${coachesList.length} counselors` : `${coachesList.length} counselors${coachesList.length === 1 ? '' : ''}`; } body.querySelectorAll('.coach-expand-btn').forEach(btn => { btn.addEventListener('click', () => toggleCoach(btn.dataset.coach)); }); if (expandedCoachId) { wireCoachDetailPanel(expandedCoachId); } } async function fetchCoachRecent(coachID, offset = 0, append = false) { const data = await apiGet( `coaches.php?coachID=${encodeURIComponent(coachID)}&recent=1` + `&limit=${FETCH_LIMIT}&offset=${offset}` ); const submissions = data.submissions || []; if (append && recentByCoach[coachID]) { const existing = recentByCoach[coachID].submissions || []; const seen = new Set(existing.map(s => s.submissionID)); for (const s of submissions) { if (!seen.has(s.submissionID)) { existing.push(s); } } recentByCoach[coachID] = { submissions: existing, total: data.total ?? existing.length, }; } else { recentByCoach[coachID] = { submissions, total: data.total ?? submissions.length, }; } } async function loadMoreCoachUploads(coachID) { const data = recentByCoach[coachID]; if (!data) return; const btn = document.querySelector( `[data-coach-panel="${CSS.escape(coachID)}"] .coach-more-uploads` ); if (btn) { btn.disabled = true; btn.textContent = 'Loading…'; } try { await fetchCoachRecent(coachID, data.submissions.length, true); renderCoachTableBody(); } catch (e) { showToast(e.message, 'error'); if (btn) { btn.disabled = false; btn.textContent = 'Load more uploads'; } } } async function toggleCoach(coachID) { const coach = coachesList.find(c => c.coachID === coachID); if (!coach || !coachHasUploads(coach)) { return; } if (expandedCoachId === coachID) { expandedCoachId = null; renderCoachTableBody(); return; } expandedCoachId = coachID; if (!coachDetailUi[coachID]) { coachDetailUi[coachID] = { clientLimit: CLIENT_GROUP_PAGE, expandedClients: new Set(), showAllUploads: new Set(), filter: '', }; } renderCoachTableBody(); if (!recentByCoach[coachID]) { try { await fetchCoachRecent(coachID, 0, false); } catch (e) { showToast(e.message, 'error'); recentByCoach[coachID] = { submissions: [], total: 0 }; } renderCoachTableBody(); } } function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }