128 lines
4.7 KiB
Python
128 lines
4.7 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, is_system_admin, 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()
|
|
|
|
|
|
@router.get("/progress", response_model=StudyProgressRead)
|
|
async def get_progress(
|
|
study_id: uuid.UUID,
|
|
_=Depends(require_api_permission("project_overview:read")),
|
|
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])
|
|
async def list_lost_visits(
|
|
study_id: uuid.UUID,
|
|
limit: int = 20,
|
|
_=Depends(require_api_permission("project_overview:read")),
|
|
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])
|
|
async def get_center_summary(
|
|
study_id: uuid.UUID,
|
|
_=Depends(require_api_permission("project_overview:read")),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
current_user=Depends(get_current_user),
|
|
) -> list[CenterSummaryItem]:
|
|
membership = None
|
|
if not is_system_admin(current_user):
|
|
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
|