import uuid from fastapi import APIRouter, Depends from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member from app.models.milestone import Milestone from app.schemas.progress import StudyProgressRead from app.schemas.visit import VisitLostItem from app.crud import visit as visit_crud from app.schemas.dashboard import CenterSummaryItem from app.crud import overview as overview_crud router = APIRouter() @router.get("/progress", response_model=StudyProgressRead, dependencies=[Depends(require_study_member())]) async def get_progress( study_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), ) -> StudyProgressRead: milestone_total_stmt = select(func.count()).select_from(Milestone).where(Milestone.study_id == study_id) milestone_done_stmt = select(func.count()).select_from(Milestone).where( Milestone.study_id == study_id, Milestone.status == "DONE" ) milestone_total = (await db.execute(milestone_total_stmt)).scalar_one() milestone_done = (await db.execute(milestone_done_stmt)).scalar_one() completion_rate = float(milestone_done / milestone_total) if milestone_total else 0.0 return StudyProgressRead( milestones_total=milestone_total, milestones_done=milestone_done, completion_rate=completion_rate, ) @router.get("/lost-visits", response_model=list[VisitLostItem], dependencies=[Depends(require_study_member())]) async def list_lost_visits( study_id: uuid.UUID, limit: int = 20, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> list[VisitLostItem]: cra_scope = await get_cra_site_scope(db, study_id, current_user) site_ids = cra_scope[0] if cra_scope else None rows = await visit_crud.list_lost_visits(db, study_id, site_ids=site_ids, limit=limit) items: list[VisitLostItem] = [] for visit, subject_no, site_id in rows: items.append( VisitLostItem( visit_id=visit.id, subject_id=visit.subject_id, subject_no=subject_no, site_id=site_id, visit_code=visit.visit_code, status=visit.status, updated_at=visit.updated_at, ) ) return items @router.get("/center-summary", response_model=list[CenterSummaryItem], dependencies=[Depends(require_study_member())]) async def get_center_summary( study_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ) -> list[CenterSummaryItem]: role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role if role_value not in {"ADMIN", "PM", "CRA"}: return [] cra_scope = await get_cra_site_scope(db, study_id, current_user) scope_ids = cra_scope[0] if cra_scope else None scope_id_strs = {str(cid) for cid in scope_ids} if scope_ids is not None else None overview = await overview_crud.get_project_overview(db, study_id) centers = overview.get("centers") or [] stage_order = [ ("institution_initiation_status", "机构立项"), ("ethics_status", "伦理审批"), ("contract_sign_status", "合同签署"), ("startup_status", "启动"), ("enrollment_status", "入组"), ("inspection_status", "末次稽查"), ("closeout_status", "关中心"), ] summary_list: list[CenterSummaryItem] = [] for center in centers: center_id = center.get("center_id") if scope_id_strs is not None and center_id not in scope_id_strs: continue stage_label = "未开始" stage_status = "NOT_STARTED" for key, label in stage_order: status = (center.get(key) or "NOT_STARTED").upper() if status in ("IN_PROGRESS", "BLOCKED"): stage_label = label stage_status = status break if status == "COMPLETED": stage_label = label stage_status = status continue summary_list.append( CenterSummaryItem( id=center_id, name=center.get("center_name") or "", stage=stage_label, stage_status=stage_status, actual_enrolled=center.get("enrollment_actual") or 0, planned_enrolled=center.get("enrollment_target") or 0, ) ) return summary_list