Files
ctms/backend/app/api/v1/dashboard.py
T
2026-05-28 10:56:28 +08:00

133 lines
4.9 KiB
Python

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, require_api_permission
from app.core.project_permissions import role_has_api_permission
from app.crud import member as member_crud
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()
def _role_value(user) -> str:
return user.role.value if hasattr(user.role, "value") else str(user.role)
@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()),
Depends(require_api_permission("project_overview:read"))
])
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 = _role_value(current_user)
membership = None
if role_value != "ADMIN":
membership = await member_crud.get_member(db, study_id, current_user.id)
if not membership or not membership.is_active:
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