import { apiGet, apiPost, apiDelete } from '../api.js'; import { pageHeaderHTML, showToast } from '../app.js'; import { isDevTestClientCode, testDataRowClassForClient } from '../test-data.js'; const PAGE_SIZE = 50; let clientsList = []; let coachesList = []; let filterSearch = ''; let filterTestData = ''; let page = 0; let expandedClientCode = null; let expandedQnKey = null; let expandedVersionKey = null; let detailByClient = {}; export async function clientsPage() { const app = document.getElementById('app'); app.innerHTML = ` ${pageHeaderHTML('Clients', '')}
`; document.getElementById('showCreateFormBtn').addEventListener('click', () => { const wrapper = document.getElementById('createClientWrapper'); if (wrapper.style.display === 'none') { showCreateForm(); } else { hideCreateForm(); } }); await loadClients(); } async function loadClients() { try { const [clientsData, assignData] = await Promise.all([ apiGet('clients.php'), apiGet('assignments.php'), ]); clientsList = clientsData.clients || []; coachesList = assignData.coaches || []; renderClients(); } catch (e) { showToast(e.message, 'error'); document.getElementById('clientsContent').innerHTML = `

${esc(e.message)}

`; } } function renderClients() { const container = document.getElementById('clientsContent'); if (!clientsList.length) { container.innerHTML = `

No clients found

Create the first client above.

`; return; } container.innerHTML = `

Expand a row to view questionnaire responses and upload version history.

Client Code Current Coach Actions
`; document.getElementById('clientListSearch').addEventListener('input', (e) => { filterSearch = e.target.value.trim(); page = 0; clearExpandIfHidden(); renderClientTableBody(); }); document.getElementById('clientTestFilter').addEventListener('change', (e) => { filterTestData = e.target.value; page = 0; clearExpandIfHidden(); renderClientTableBody(); }); renderClientTableBody(); } function getFilteredClientsList() { let list = clientsList; if (filterSearch) { const q = filterSearch.toLowerCase(); list = list.filter(c => { const hay = `${c.clientCode || ''} ${c.coachUsername || ''}`.toLowerCase(); return hay.includes(q); }); } if (filterTestData === 'test') { list = list.filter(c => isDevTestClientCode(c.clientCode)); } else if (filterTestData === 'hide-test') { list = list.filter(c => !isDevTestClientCode(c.clientCode)); } return list; } function renderClientTableBody() { const body = document.getElementById('clientsTableBody'); const countEl = document.getElementById('clientListCount'); const filtered = getFilteredClientsList(); const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); if (page >= totalPages) page = totalPages - 1; const slice = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); if (countEl) { countEl.textContent = `${filtered.length} client${filtered.length === 1 ? '' : 's'}`; } if (!slice.length) { body.innerHTML = `No clients match`; } else { body.innerHTML = slice.map(c => clientRowHTML(c)).join(''); body.querySelectorAll('.client-expand-btn').forEach(btn => { btn.addEventListener('click', () => toggleClient(btn.dataset.code)); }); body.querySelectorAll('.client-qn-expand-btn').forEach(btn => { btn.addEventListener('click', () => toggleQn(btn.dataset.client, btn.dataset.qn)); }); body.querySelectorAll('.client-version-expand-btn').forEach(btn => { btn.addEventListener('click', () => toggleVersion( btn.dataset.client, btn.dataset.qn, btn.dataset.submission )); }); body.querySelectorAll('.delete-client-btn').forEach(btn => { btn.addEventListener('click', () => deleteClient(btn.dataset.code)); }); } const pag = document.getElementById('clientsPagination'); if (!pag) return; if (filtered.length <= PAGE_SIZE) { pag.innerHTML = ''; return; } const from = page * PAGE_SIZE + 1; const to = Math.min((page + 1) * PAGE_SIZE, filtered.length); pag.innerHTML = ` Rows ${from}–${to} of ${filtered.length} `; document.getElementById('clientsPagePrev')?.addEventListener('click', () => { page--; clearExpandIfHidden(); renderClientTableBody(); }); document.getElementById('clientsPageNext')?.addEventListener('click', () => { page++; clearExpandIfHidden(); renderClientTableBody(); }); } function qnKey(clientCode, questionnaireID) { return `${clientCode}|${questionnaireID}`; } function versionKey(clientCode, questionnaireID, submissionID) { return `${clientCode}|${questionnaireID}|${submissionID}`; } function clearExpandIfHidden() { const filtered = getFilteredClientsList(); const slice = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); if (expandedClientCode && !slice.some(c => c.clientCode === expandedClientCode)) { expandedClientCode = null; expandedQnKey = null; expandedVersionKey = null; } } function answerTableHTML(rows, { highlightLiveDiff = false } = {}) { if (!rows?.length) { return '

No answers recorded

'; } return ` ${rows.map(r => ` `).join('')}
QuestionAnswer
${esc(r.label)} ${esc(r.value || '—')}
`; } function clientDetailPanelHTML(clientCode) { const detail = detailByClient[clientCode]; if (!detail) { return '

Loading…

'; } const cl = detail.client || {}; const questionnaires = detail.questionnaires || []; if (!questionnaires.length) { return `

Coach: ${esc(cl.coachUsername || '—')} ${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''}

No questionnaire data for this client yet.

`; } return `

Coach: ${esc(cl.coachUsername || '—')} ${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''}

${questionnaires.map(q => clientQnBlockHTML(clientCode, q)).join('')}`; } function clientQnBlockHTML(clientCode, q) { const key = qnKey(clientCode, q.questionnaireID); const qnExpanded = expandedQnKey === key; const meta = [ q.status ? `Status: ${q.status}` : '', q.sumPoints != null ? `Points: ${q.sumPoints}` : '', q.completedAt ? `Completed: ${q.completedAt}` : '', q.submissionCount ? `${q.submissionCount} upload(s)` : '', ].filter(Boolean).join(' · '); const versionsHTML = (q.submissions || []).map(s => { const vKey = versionKey(clientCode, q.questionnaireID, s.submissionID); const vExpanded = expandedVersionKey === vKey; const vMeta = [ s.submittedAt ? `Uploaded: ${s.submittedAt}` : '', s.status ? `Status: ${s.status}` : '', s.sumPoints != null ? `Points: ${s.sumPoints}` : '', ].filter(Boolean).join(' · '); return `
Version ${s.version} ${s.changedCount > 0 ? `${s.changedCount} diff vs live` : 'Same as live'} ${vMeta ? `${esc(vMeta)}` : ''}
${vExpanded ? `
${s.changedCount > 0 ? '

Highlighted rows differ from current live data.

' : ''} ${answerTableHTML(s.answers, { highlightLiveDiff: true })}
` : ''}
`; }).join(''); return `
${esc(q.name)} ${q.questionnaireVersion ? `v${esc(q.questionnaireVersion)}` : ''} ${meta ? `${esc(meta)}` : ''}
${qnExpanded ? `

Current data (live)

${answerTableHTML(q.currentAnswers)} ${(q.submissions || []).length ? `

Upload versions

${versionsHTML} ` : '

No archived upload versions.

'}
` : ''}
`; } function clientHasResponseData(c) { return Number(c.hasResponseData) === 1; } function clientRowHTML(c) { const coach = c.coachUsername ? `${esc(c.coachUsername)}` : `Unassigned`; const canExpand = clientHasResponseData(c); const expanded = canExpand && expandedClientCode === c.clientCode; const expandControl = canExpand ? ` ` : ''; return ` ${expandControl}${esc(c.clientCode)} ${coach} ${expanded ? `
${clientDetailPanelHTML(c.clientCode)}
` : ''}`; } async function toggleClient(clientCode) { const row = clientsList.find(c => c.clientCode === clientCode); if (!row || !clientHasResponseData(row)) { return; } if (expandedClientCode === clientCode) { expandedClientCode = null; expandedQnKey = null; expandedVersionKey = null; renderClientTableBody(); return; } expandedClientCode = clientCode; expandedQnKey = null; expandedVersionKey = null; renderClientTableBody(); if (!detailByClient[clientCode]) { try { const data = await apiGet( `clients.php?clientCode=${encodeURIComponent(clientCode)}&detail=1` ); detailByClient[clientCode] = data; } catch (e) { showToast(e.message, 'error'); detailByClient[clientCode] = { client: {}, questionnaires: [] }; } renderClientTableBody(); } } function toggleQn(clientCode, questionnaireID) { const key = qnKey(clientCode, questionnaireID); expandedQnKey = expandedQnKey === key ? null : key; expandedVersionKey = null; renderClientTableBody(); } function toggleVersion(clientCode, questionnaireID, submissionID) { const key = versionKey(clientCode, questionnaireID, submissionID); expandedVersionKey = expandedVersionKey === key ? null : key; renderClientTableBody(); } async function deleteClient(clientCode) { if (!confirm(`Delete client "${clientCode}"? This will also remove all their answers and results. This cannot be undone.`)) return; try { await apiDelete('clients.php', { clientCode }); clientsList = clientsList.filter(c => c.clientCode !== clientCode); delete detailByClient[clientCode]; if (expandedClientCode === clientCode) { expandedClientCode = null; expandedQnKey = null; expandedVersionKey = null; } showToast(`Client "${clientCode}" deleted`, 'success'); if (!clientsList.length) renderClients(); else renderClientTableBody(); } catch (e) { showToast(e.message, 'error'); } } function showCreateForm() { if (!coachesList.length) { showToast('No coaches available. Create a coach first.', 'error'); return; } const wrapper = document.getElementById('createClientWrapper'); wrapper.style.display = ''; wrapper.innerHTML = `

Create New Client

`; const input = document.getElementById('cc_code'); input.focus(); wrapper.querySelectorAll('input').forEach(inp => { inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') submitCreateClient(); if (e.key === 'Escape') hideCreateForm(); }); }); document.getElementById('cc_submit').addEventListener('click', submitCreateClient); document.getElementById('cc_cancel').addEventListener('click', hideCreateForm); } function hideCreateForm() { const wrapper = document.getElementById('createClientWrapper'); if (wrapper) { wrapper.style.display = 'none'; wrapper.innerHTML = ''; } } async function submitCreateClient() { const errEl = document.getElementById('cc_error'); errEl.style.display = 'none'; const clientCode = document.getElementById('cc_code').value.trim(); const coachID = document.getElementById('cc_coach').value; if (!clientCode) { showError(errEl, 'Client code is required'); return; } if (!coachID) { showError(errEl, 'Please select a coach'); return; } const btn = document.getElementById('cc_submit'); btn.disabled = true; btn.textContent = 'Creating...'; try { const data = await apiPost('clients.php', { clientCode, coachID }); const coach = coachesList.find(c => c.coachID === coachID); clientsList.push({ clientCode: data.clientCode, coachID: coachID, coachUsername: coach?.username ?? null, }); showToast(`Client "${data.clientCode}" created`, 'success'); hideCreateForm(); renderClients(); } catch (e) { showError(errEl, e.message); btn.disabled = false; btn.textContent = 'Create Client'; } } function showError(el, msg) { el.textContent = msg; el.style.display = ''; } function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }