feat: add analysis pages and raster risk map

Ship a new app version with broader analytics, restructured
dashboards, and a server-rendered risk map.

Frontend:
- Add Overview, Demographic, Disease, and Environmental Health
  analysis pages
- Add AnomalyMarkers, CalendarHeatmap, and MetricHeatmapTable
  components
- Rebuild Alerts map onto server-rendered raster risk tiles;
  expand Monitoring, Trend, and District Comparison views
- Extend API client, stores, and TypeScript types

Backend:
- Add environment router (pollutants, lag correlations)
- Add risk_raster util serving XYZ 100m risk tiles
- Expand cases endpoints (demographics, seasonality, diagnoses)
  and insights; harden auth and file-based loaders

Data & tooling:
- Add processed outpatient/inpatient/combined case parquet (LFS)
- Add nested CLAUDE.md guides, pyrightconfig, and test updates
This commit is contained in:
2026-06-21 17:35:03 +08:00
parent f092c3c550
commit e95e2f1338
63 changed files with 8534 additions and 988 deletions

View File

@@ -148,7 +148,12 @@ function ReportDetail({ reportId, onBack }: { reportId: string; onBack: () => vo
const fetchReport = useReportsStore((s) => s.fetchReport);
const { selectedDiagnoses } = useDiseaseStore();
useEffect(() => { fetchReport(reportId); }, [reportId]);
useEffect(() => {
// Skip refetch if the report is already loaded (e.g. just generated) to avoid clobbering it.
if (useReportsStore.getState().currentReport?.metadata?.report_id !== reportId) {
fetchReport(reportId);
}
}, [reportId]);
if (error) return <ErrorBanner error={error} onRetry={() => { clearError(); fetchReport(reportId); }} onDismiss={clearError} />;
if (isLoading || !currentReport) return <div className="text-center py-8 text-sm text-gray-500">...</div>;
@@ -176,12 +181,29 @@ function ReportDetail({ reportId, onBack }: { reportId: string; onBack: () => vo
<span>: {metadata.generated_at?.slice(0, 10)}</span>
</div>
</div>
<button
onClick={() => downloadCSV(currentReport)}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
>
<Download className="w-3.5 h-3.5" /> CSV
</button>
<div className="flex items-center gap-2">
<button
onClick={() => downloadCSV(currentReport)}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
>
<Download className="w-3.5 h-3.5" /> CSV
</button>
<button
onClick={() => {
const json = JSON.stringify(currentReport, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${reportId}.json`;
link.click();
URL.revokeObjectURL(url);
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
>
<Download className="w-3.5 h-3.5" /> JSON
</button>
</div>
</div>
{/* Summary cards */}
@@ -285,7 +307,11 @@ export function ReportsCenter() {
setIsGenerating(true);
try {
await generateReport(genType);
setView('detail');
const generated = useReportsStore.getState().currentReport;
if (generated) {
setSelectedReportId(generated.metadata.report_id);
setView('detail');
}
} finally {
setIsGenerating(false);
}