feat: add reports center, admin drill-down, disease filter + bug fixes + perf optimization
Frontend features:
- 报表中心 (ReportsCenter): list/detail views, diagnosis breakdown chart, CSV export
- 多级行政下钻 (AdminBreadcrumb): 湖北省→武汉市→区→街道 hierarchical drill-down
- 按病种筛选 (DiseaseFilter): multi-select diagnosis filter on monitoring + reports pages
Backend:
- Add /forecast/{days} endpoint, diagnosis filter params on cases endpoints
- Add /streets aggregation endpoint, enrich reports with real case data
- Extract shared case_loader module
Bug fixes (14):
- Fix missing /risk/forecast route (404), historyApi pointing to non-existent router
- Fix min_risk filter silently ignored in insights/hotspots
- Fix type mismatches: CaseTrendResponse, CaseStatsResponse shapes
- Fix silent .catch(() => {}) swallowing errors, fetchAlerts not clearing stale state
- Fix lru_cache caching exceptions, generateReport used cachedGet for write op
- Fix missing useEffect deps in Insights, DistrictComparison, ReportsCenter
Performance (9):
- Zustand selectors across 9 components (eliminate re-render cascades)
- Fix districtCases.sort() mutating store state, inline IIFE → memo'd component
- CaseLocationMap: React.memo, race protection, correct deps
- AlertCard: stable callbacks, TimelinePlayer: useMemo, TopNav: clock isolation
- SideNav: modules array to module scope, DiseaseFilter: memoized filter
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Router for geocoded case data and grid aggregated data
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
@@ -53,6 +53,18 @@ class GeocodedResponse(BaseModel):
|
||||
cases: List[GeocodedCaseData]
|
||||
total_count: int
|
||||
|
||||
|
||||
class StreetData(BaseModel):
|
||||
"""Street-level aggregated case data"""
|
||||
name: str
|
||||
total_cases: int
|
||||
outpatient: int
|
||||
inpatient: int
|
||||
|
||||
|
||||
class StreetsResponse(BaseModel):
|
||||
streets: List[StreetData]
|
||||
|
||||
@router.get("/grid", response_model=GridCaseResponse, summary="Get aggregated grid case data")
|
||||
async def get_grid_cases():
|
||||
"""
|
||||
@@ -176,3 +188,38 @@ async def get_geocoded_count():
|
||||
except Exception as e:
|
||||
logger.exception("Error counting geocoded cases")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/streets", response_model=StreetsResponse, summary="Get street-level aggregation")
|
||||
async def get_streets(district: str = Query(..., description="District name")):
|
||||
"""Get street-level aggregated case data for a district."""
|
||||
cases_file = DATA_DIR / "geocoded_all_cases.csv"
|
||||
if not cases_file.exists():
|
||||
raise HTTPException(status_code=404, detail="Geocoded data not found")
|
||||
|
||||
try:
|
||||
df = _load_csv(cases_file)
|
||||
df = df.dropna(subset=['latitude', 'longitude'])
|
||||
df = df[df['district'] == district]
|
||||
|
||||
# Group by street
|
||||
streets = []
|
||||
if 'street' in df.columns:
|
||||
street_groups = df.groupby('street')
|
||||
for street, group in street_groups:
|
||||
if pd.isna(street) or str(street).strip() == '':
|
||||
continue
|
||||
out_count = len(group[group['case_type'] == 'outpatient'])
|
||||
in_count = len(group[group['case_type'] == 'inpatient'])
|
||||
streets.append(StreetData(
|
||||
name=str(street),
|
||||
total_cases=len(group),
|
||||
outpatient=out_count,
|
||||
inpatient=in_count
|
||||
))
|
||||
streets.sort(key=lambda s: s.total_cases, reverse=True)
|
||||
|
||||
return StreetsResponse(streets=streets)
|
||||
except Exception as e:
|
||||
logger.exception("Error loading street data")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
Reference in New Issue
Block a user