import { apiGet, apiPut } from '../api.js'; import { pageHeaderHTML, showToast } from '../app.js'; let overview = null; let staleClients = []; let followupSearch = ''; export async function insightsPage() { const app = document.getElementById('app'); app.innerHTML = ` ${pageHeaderHTML('Insights')}
`; try { const [ov, stale] = await Promise.all([ apiGet('analytics.php?overview=1'), apiGet('analytics.php?staleClients=1'), ]); overview = ov; staleClients = stale.clients || []; renderInsights(); } catch (e) { showToast(e.message, 'error'); document.getElementById('insightsContent').innerHTML = `

${esc(e.message)}

`; } } function renderInsights() { const el = document.getElementById('insightsContent'); const ov = overview || {}; const kpiCards = `
${ov.clientCount ?? 0}
Clients
${ov.clientsWithCompletions ?? 0}
With completions
${ov.submissionsLast7d ?? 0}
Uploads (7 days)
${ov.submissionsLast30d ?? 0}
Uploads (30 days)
`; const uploadChart = verticalBarChart( (ov.submissionsByDay || []).map(d => ({ label: formatChartDay(d.date), value: d.count, title: `${d.date}: ${d.count} upload(s)`, })), { emptyLabel: 'No uploads in this period' } ); const withoutCompletions = Math.max(0, (ov.clientCount ?? 0) - (ov.clientsWithCompletions ?? 0)); const engagementChart = horizontalBarChart([ { label: 'With completions', value: ov.clientsWithCompletions ?? 0 }, { label: 'No completions yet', value: withoutCompletions }, ], { suffix: '' }); const idleChart = horizontalBarChart(idleBucketItems(staleClients), { suffix: '', variant: 'warn', }); const completionChart = horizontalBarChart( (ov.questionnaires || []).map(q => ({ label: q.name, value: q.completionRatio ?? 0, title: `${q.name}: ${q.completionRatio ?? 0}%`, })), { suffix: '%', max: 100 } ); const scoringProfiles = ov.scoringProfiles || []; const scoringCharts = scoringProfiles.map(sp => { const computed = sp.computedBands || sp.bands || {}; const coach = sp.coachBands || {}; const computedChart = horizontalBarChart([ { label: 'Green', value: computed.green ?? 0 }, { label: 'Yellow', value: computed.yellow ?? 0 }, { label: 'Red', value: computed.red ?? 0 }, ], { suffix: '' }); const coachChart = horizontalBarChart([ { label: 'Green', value: coach.green ?? 0 }, { label: 'Yellow', value: coach.yellow ?? 0 }, { label: 'Red', value: coach.red ?? 0 }, ], { suffix: '' }); return `

${esc(sp.name)}

${sp.clientCount ?? 0} client(s) with complete profile ${sp.averageTotal != null ? ` · avg ${sp.averageTotal}` : ''} ${sp.pendingReview ? ` · ${sp.pendingReview} pending coach review` : ''}

Calculated bands

${computedChart}

Coach decisions

${coachChart}
`; }).join(''); const qRows = (ov.questionnaires || []).map(q => ` ${esc(q.name)} ${q.completedCount ?? 0} / ${q.eligibleCount ?? 0} ${q.completionRatio ?? 0}% `).join(''); el.innerHTML = ` ${kpiCards}

Uploads per day

Last 14 days

${uploadChart}

Client engagement

Among clients in your scope

${engagementChart}

Days since last activity

Clients with at least one completion

${idleChart}
${scoringProfiles.length ? `

Scoring profiles

Calculated vs coach band distribution among clients with a complete weighted score

${scoringCharts}
` : ''}

Completion by questionnaire

Share of clients who completed each active questionnaire

${completionChart}
${qRows || ''}
QuestionnaireCompletedRate
No active questionnaires

Needs follow-up

Clients who completed at least one questionnaire, sorted by longest time since any upload activity.

ClientCoachFirst completedFirst at Last activityDays idleNote
`; document.getElementById('followupListSearch').addEventListener('input', (e) => { followupSearch = e.target.value; renderFollowupTableBody(); }); renderFollowupTableBody(); } function followupSearchText(c) { return [ c.clientCode, c.coachUsername, c.firstQuestionnaireName, c.firstQuestionnaireID, c.note, c.firstCompletedAt, c.lastActivityAt, c.daysSinceLastActivity != null ? String(c.daysSinceLastActivity) : '', ].filter(Boolean).join(' '); } function filteredFollowupClients() { const q = followupSearch.trim().toLowerCase(); if (!q) return staleClients; return staleClients.filter(c => followupSearchText(c).toLowerCase().includes(q)); } function followupRowHTML(c) { return ` ${esc(c.clientCode)} ${esc(c.coachUsername)} ${esc(c.firstQuestionnaireName || '—')} ${esc(c.firstCompletedAt || '—')} ${esc(c.lastActivityAt || '—')} ${c.daysSinceLastActivity != null ? c.daysSinceLastActivity : '—'} `; } function renderFollowupTableBody() { const body = document.getElementById('followupTableBody'); const countEl = document.getElementById('followupListCount'); if (!body) return; const filtered = filteredFollowupClients(); if (!filtered.length) { body.innerHTML = ` ${staleClients.length ? 'No clients match' : 'No clients'} `; if (countEl) { countEl.textContent = followupSearch.trim() && staleClients.length ? `0 of ${staleClients.length} clients` : ''; } return; } body.innerHTML = filtered.map(c => followupRowHTML(c)).join(''); if (countEl) { const q = followupSearch.trim(); countEl.textContent = q ? `${filtered.length} of ${staleClients.length} clients` : `${staleClients.length} client${staleClients.length === 1 ? '' : 's'}`; } body.querySelectorAll('.save-note-btn').forEach(btn => { btn.addEventListener('click', () => { const tr = btn.closest('tr'); const ta = tr?.querySelector('.followup-note-input'); saveNote(btn.dataset.client, ta); }); }); } async function saveNote(clientCode, ta) { if (!ta) return; try { await apiPut('analytics.php', { clientCode, note: ta.value.trim() }); const row = staleClients.find(c => c.clientCode === clientCode); if (row) row.note = ta.value.trim(); showToast('Note saved', 'success'); } catch (e) { showToast(e.message, 'error'); } } function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; } function formatChartDay(isoDate) { if (!isoDate) return ''; const d = new Date(isoDate + 'T12:00:00'); if (Number.isNaN(d.getTime())) return isoDate.slice(5); return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } function chartMax(values, fallbackMax = 1) { const m = Math.max(0, ...values); return m > 0 ? m : fallbackMax; } function verticalBarChart(items, { emptyLabel = 'No data' } = {}) { if (!items.length) { return `

${esc(emptyLabel)}

`; } const max = chartMax(items.map(i => i.value)); return ` `; } function horizontalBarChart(items, { suffix = '%', max: fixedMax, variant = '' } = {}) { if (!items.length) { return '

No data

'; } const max = fixedMax ?? chartMax(items.map(i => i.value)); const fillClass = variant === 'warn' ? 'insights-hchart-fill--warn' : variant === 'muted' ? 'insights-hchart-fill--muted' : ''; return ` `; } function idleBucketItems(clients) { const buckets = [ { label: '0–7 days', value: 0 }, { label: '8–30 days', value: 0 }, { label: '31–90 days', value: 0 }, { label: '90+ days', value: 0 }, { label: 'No uploads yet', value: 0 }, ]; for (const c of clients) { const d = c.daysSinceLastActivity; if (d == null) buckets[4].value += 1; else if (d <= 7) buckets[0].value += 1; else if (d <= 30) buckets[1].value += 1; else if (d <= 90) buckets[2].value += 1; else buckets[3].value += 1; } return buckets.filter(b => b.value > 0); }