/** Dashboard (Home) — system-wide overview, region/village breakdowns, * change comparison, reports feed, and charts. Replaces the old * greeting+3-cards Home page in place. */ function formatHomeDate(iso) { return typeof formatDateIst === 'function' ? formatDateIst(iso) : (iso || '—'); } function dashPct(v) { return v != null ? v.toFixed(2) + '%' : '—'; } function dashDelta(v, suffix) { if (v == null) return '—'; const sign = v > 0 ? '+' : ''; return `${sign}${typeof v === 'number' && suffix === '%' ? v.toFixed(2) : v}${suffix}`; } const dashCharts = {}; function renderDashChart(canvasId, config) { const canvas = document.getElementById(canvasId); if (!canvas || typeof Chart === 'undefined') return; if (dashCharts[canvasId]) { dashCharts[canvasId].destroy(); } dashCharts[canvasId] = new Chart(canvas, config); } async function loadHomeGreeting() { const el = document.getElementById('dda-home-greeting'); if (!el) return; try { const me = await ddaApi('GET', '/api/me'); el.textContent = me.full_name ? `Welcome, ${me.full_name}` : 'Welcome'; } catch (_) { // Not authenticated — the page-load redirect to /login should already // have caught this; leave the default greeting rather than erroring loudly. } } let dashSummaryCache = null; function renderDashKpis(summary) { const el = document.getElementById('dda-dash-kpis'); if (!el) return; const tiles = [ ['Total Detection Runs', summary.totalRuns], ['Total Detected Changes', summary.totalDetectedChanges], ['Total Unclassified', summary.totalUnclassified], ['Avg Change %', dashPct(summary.avgChangePercentage)], ['Regions Covered', summary.totalRegions], ['Villages Covered', summary.totalVillages], ]; el.innerHTML = tiles.map(([label, value]) => ` ${escapeHtml(String(value ?? '—'))} ${escapeHtml(label)} `).join(''); } function renderDashLatest(run) { const el = document.getElementById('dda-dash-latest'); if (!el) return; if (!run) { el.innerHTML = '
No detection runs yet. Start one from the Change Detection page.
'; return; } const thumb = run.afterThumbUrl || run.overlayUrl; el.innerHTML = `${escapeHtml(run.zone)} › ${escapeHtml(run.village)} ·
Could not load overview: ${escapeHtml(err.message || 'error')}
`; return null; } } function roleLabel(role) { const r = String(role || '').trim(); return r ? r.charAt(0).toUpperCase() + r.slice(1) : 'Unknown'; } function renderDashRolesTable(roles) { const el = document.getElementById('dda-dash-roles'); if (!el) return; if (!roles.length) { el.innerHTML = 'No role activity to show yet.
'; return; } el.innerHTML = `| Role | Users | Runs | Detected Changes | Unclassified | Avg Change % | Last Detection |
|---|---|---|---|---|---|---|
| ${escapeHtml(roleLabel(r.role))} | ${r.usersCount} | ${r.runsCount} | ${r.detectedChanges} | ${r.unclassified} | ${dashPct(r.avgChangePercentage)} |
Could not load role breakdown: ${escapeHtml(err.message || 'error')}
`; } } let dashRegionsCache = []; let dashSelectedZone = ''; function renderDashRegionsTable(regions) { const el = document.getElementById('dda-dash-regions'); if (!el) return; if (!regions.length) { el.innerHTML = 'No regions detected yet.
'; return; } el.innerHTML = `| Region | Runs | Detected Changes | Unclassified | Villages | Avg Change % | Last Detection |
|---|---|---|---|---|---|---|
| ${escapeHtml(r.zone)} | ${r.runsCount} | ${r.detectedChanges} | ${r.unclassified} | ${r.villagesCount} | ${dashPct(r.avgChangePercentage)} |
Could not load regions: ${escapeHtml(err.message || 'error')}
`; } } function renderDashVillagesTable(villages) { const el = document.getElementById('dda-dash-villages'); if (!el) return; if (!villages.length) { el.innerHTML = 'No villages match this filter.
'; return; } el.innerHTML = `| Region | Village | Runs | Detected Changes | Unclassified | Avg Change % | Last Detection |
|---|---|---|---|---|---|---|
| ${escapeHtml(v.zone)} | ${escapeHtml(v.village)} | ${v.runsCount} | ${v.detectedChanges} | ${v.unclassified} | ${dashPct(v.avgChangePercentage)} |
Loading villages…
'; try { const qs = zone ? `?zone=${encodeURIComponent(zone)}` : ''; const data = await ddaApi('GET', `/api/dda/dashboard/villages${qs}`); const villages = data.villages || []; renderDashVillagesTable(villages); const top = villages.slice(0, 15); renderDashChart('dda-chart-villages', { type: 'bar', data: { labels: top.map((v) => v.village), datasets: [{ label: 'Detections', data: top.map((v) => v.runsCount), backgroundColor: '#6358d4' }], }, options: { indexAxis: 'y', responsive: true, plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true, ticks: { precision: 0 } } }, }, }); } catch (err) { if (el) el.innerHTML = `Could not load villages: ${escapeHtml(err.message || 'error')}
`; } } function renderDashLocation(data) { const el = document.getElementById('dda-dash-location'); if (!el) return; if (!data.latest) { el.innerHTML = 'No detection history for this location yet.
'; return; } const cmp = data.comparison; el.innerHTML = `${data.runsCount} detection run${data.runsCount === 1 ? '' : 's'} recorded for this location.
${dashPct(data.latest.changePercentage)} change · ${data.latest.regionsCount} regions
${dashPct(data.previous.changePercentage)} change · ${data.previous.regionsCount} regions
No earlier run to compare against.
'}${dashDelta(cmp.changePercentageDelta, '%')} change %
${dashDelta(cmp.regionsCountDelta, '')} regions
` : '—
'}| Date | Title | Change % | Regions | Unclassified | |
|---|---|---|---|---|---|
| ${escapeHtml(r.title)} | ${dashPct(r.changePercentage)} | ${r.regionsCount} | ${r.unclassified} | Report |
Loading location history…
'; try { const data = await ddaApi('GET', `/api/dda/dashboard/location?zone=${encodeURIComponent(zone)}&village=${encodeURIComponent(village)}`); renderDashLocation(data); } catch (err) { if (el) el.innerHTML = `Could not load location history: ${escapeHtml(err.message || 'error')}
`; } } async function loadDashboardTrend() { try { const data = await ddaApi('GET', '/api/dda/dashboard/trend?months=6'); const buckets = data.buckets || []; renderDashChart('dda-chart-trend', { type: 'line', data: { labels: buckets.map((b) => b.label), datasets: [{ label: 'Detection runs', data: buckets.map((b) => b.runsCount), borderColor: '#7a6aed', backgroundColor: 'rgba(122,106,237,0.15)', tension: 0.3, fill: true, }], }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } }, }); } catch (_) { // Chart stays absent — non-critical widget, other sections still work. } } async function loadDashboardReports() { const el = document.getElementById('dda-dash-reports'); if (!el) return; try { const data = await ddaApi('GET', '/api/dda/dashboard/recent-reports?limit=8'); const rows = data.reports || []; if (!rows.length) { el.innerHTML = 'No detection reports yet. Run a comparison on the Change Detection page.
'; return; } el.innerHTML = `| Date (IST) | Title | Area | Change % | Regions | Confirmed | Unclassified | |
|---|---|---|---|---|---|---|---|
| ${escapeHtml(r.title)} | ${escapeHtml(r.zone)} › ${escapeHtml(r.village)} | ${dashPct(r.changePercentage)} | ${r.regionsCount} | ${r.confirmed} | ${r.unclassified} | Report |
Could not load recent reports: ${escapeHtml(err.message || 'error')}
`; } } function markDashUpdated() { const el = document.getElementById('dda-dash-updated'); if (!el) return; el.textContent = `Updated ${formatHomeDate(new Date().toISOString())}`; } async function loadDashboard() { dashSelectedZone = ''; document.getElementById('dda-dash-village-filter-label')?.classList.add('hidden'); document.getElementById('btn-dash-village-clear')?.classList.add('hidden'); document.getElementById('dda-dash-location').innerHTML = 'Select a village above to compare its latest and previous detection results.
'; const summary = await loadDashboardSummary(); if (!summary || !summary.totalRuns) { markDashUpdated(); return; } await Promise.all([ loadDashboardRoles(), loadDashboardRegions(), loadDashboardVillages(''), loadDashboardTrend(), loadDashboardReports(), ]); markDashUpdated(); } document.getElementById('btn-dash-refresh')?.addEventListener('click', loadDashboard); document.getElementById('btn-dash-village-clear')?.addEventListener('click', () => selectDashRegion('')); document.addEventListener('DOMContentLoaded', () => { loadHomeGreeting(); loadDashboard(); });