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

@@ -172,7 +172,7 @@ interface MonitoringState {
error: string | null;
fetchGridFeatures: (date: string) => Promise<void>;
fetchAggregatedData: (startDate: string, endDate: string, district?: string) => Promise<void>;
fetchDistrictCases: (diagnosis?: string) => Promise<void>;
fetchDistrictCases: (diagnosis?: string, startDate?: string, endDate?: string) => Promise<void>;
clearError: () => void;
}
@@ -219,10 +219,14 @@ export const useMonitoringStore = create<MonitoringState>((set) => ({
}
},
fetchDistrictCases: async (diagnosis) => {
fetchDistrictCases: async (diagnosis?: string, startDate?: string, endDate?: string) => {
set({ isLoading: true, error: null });
try {
const data = await caseApi.getDistricts(diagnosis ? { diagnosis } : undefined);
const params: Record<string, string> = {};
if (diagnosis) params.diagnosis = diagnosis;
if (startDate) params.start_date = startDate;
if (endDate) params.end_date = endDate;
const data = await caseApi.getDistricts(Object.keys(params).length > 0 ? params : undefined);
const districts = Array.isArray(data) ? data : (data as any).districts || [];
set({ districtCases: districts, isLoading: false });
} catch (e) {