/** 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 = `
${thumb ? `Latest detection thumbnail` : ''}

${escapeHtml(run.title)}

${escapeHtml(run.zone)} › ${escapeHtml(run.village)} ·

${dashPct(run.changePercentage)} change ${run.regionsCount ?? '—'} regions ${run.unclassified ?? '—'} unclassified
View Report
`; } async function loadDashboardSummary() { try { const summary = await ddaApi('GET', '/api/dda/dashboard/summary'); dashSummaryCache = summary; const empty = document.getElementById('dda-dash-empty'); const content = document.getElementById('dda-dash-content'); if (!summary.totalRuns) { empty?.classList.remove('hidden'); content?.classList.add('hidden'); return summary; } empty?.classList.add('hidden'); content?.classList.remove('hidden'); renderDashKpis(summary); renderDashLatest(summary.latestRun); renderDashChart('dda-chart-status', { type: 'doughnut', data: { labels: ['Confirmed', 'False Positive', 'Submitted', 'Unclassified'], datasets: [{ data: [summary.totalConfirmed, summary.totalFalsePositive, summary.totalSubmitted, summary.totalUnclassified], backgroundColor: ['#16a34a', '#dc2626', '#6358d4', '#ca8a04'], }], }, options: { responsive: true, plugins: { legend: { position: 'bottom' } } }, }); return summary; } catch (err) { document.getElementById('dda-dash-kpis').innerHTML = `

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 = ` ${roles.map((r) => ` `).join('')}
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)}
`; } async function loadDashboardRoles() { const el = document.getElementById('dda-dash-roles'); try { const data = await ddaApi('GET', '/api/dda/dashboard/roles'); const roles = data.roles || []; renderDashRolesTable(roles); renderDashChart('dda-chart-roles', { type: 'bar', data: { labels: roles.map((r) => roleLabel(r.role)), datasets: [{ label: 'Detections', data: roles.map((r) => r.runsCount), backgroundColor: '#16a34a' }], }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } }, }); } catch (err) { if (el) el.innerHTML = `

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 = ` ${regions.map((r) => ` `).join('')}
Region Runs Detected Changes Unclassified Villages Avg Change % Last Detection
${escapeHtml(r.zone)} ${r.runsCount} ${r.detectedChanges} ${r.unclassified} ${r.villagesCount} ${dashPct(r.avgChangePercentage)}
`; el.querySelectorAll('[data-zone]').forEach((row) => { row.addEventListener('click', () => selectDashRegion(row.dataset.zone)); }); } function selectDashRegion(zone) { dashSelectedZone = dashSelectedZone === zone ? '' : zone; renderDashRegionsTable(dashRegionsCache); loadDashboardVillages(dashSelectedZone); const label = document.getElementById('dda-dash-village-filter-label'); const clearBtn = document.getElementById('btn-dash-village-clear'); if (dashSelectedZone) { label.textContent = `Filtered to region: ${dashSelectedZone}`; label.classList.remove('hidden'); clearBtn.classList.remove('hidden'); } else { label.classList.add('hidden'); clearBtn.classList.add('hidden'); } } async function loadDashboardRegions() { try { const data = await ddaApi('GET', '/api/dda/dashboard/regions'); dashRegionsCache = data.regions || []; renderDashRegionsTable(dashRegionsCache); renderDashChart('dda-chart-regions', { type: 'bar', data: { labels: dashRegionsCache.map((r) => r.zone), datasets: [{ label: 'Detections', data: dashRegionsCache.map((r) => r.runsCount), backgroundColor: '#7a6aed' }], }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } }, }); } catch (err) { document.getElementById('dda-dash-regions').innerHTML = `

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 = ` ${villages.map((v) => ` `).join('')}
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)}
`; el.querySelectorAll('[data-village]').forEach((row) => { row.addEventListener('click', () => { loadDashboardLocation(row.dataset.zone, row.dataset.village); document.getElementById('dda-dash-location-card')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }); } async function loadDashboardVillages(zone) { const el = document.getElementById('dda-dash-villages'); if (el) el.innerHTML = '

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 = `

${escapeHtml(data.zone)} › ${escapeHtml(data.village)}

${data.runsCount} detection run${data.runsCount === 1 ? '' : 's'} recorded for this location.

Current

${escapeHtml(data.latest.title)}

${dashPct(data.latest.changePercentage)} change · ${data.latest.regionsCount} regions


View Report
Previous ${data.previous ? `

${escapeHtml(data.previous.title)}

${dashPct(data.previous.changePercentage)} change · ${data.previous.regionsCount} regions


View Report ` : '

No earlier run to compare against.

'}
Change vs previous ${cmp ? `

${dashDelta(cmp.changePercentageDelta, '%')} change %

${dashDelta(cmp.regionsCountDelta, '')} regions

` : '

—

'}

Detection History

${data.history.map((r) => ` `).join('')}
DateTitleChange %RegionsUnclassified
${escapeHtml(r.title)} ${dashPct(r.changePercentage)} ${r.regionsCount} ${r.unclassified} Report
`; } async function loadDashboardLocation(zone, village) { const el = document.getElementById('dda-dash-location'); if (el) el.innerHTML = '

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 = ` ${rows.map((r) => ` `).join('')}
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
`; } catch (err) { el.innerHTML = `

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(); });