20ce6bccef
后端: - 删除 StudyRolePermission 模型和 project_permissions API 文件 - 重写 project_permissions.py core,移除所有模块级权限函数 - 重写 permission_cache.py,移除模块级权限缓存逻辑 - 新增权限模板功能(PermissionTemplate 模型、API、服务层) - 新增 permission_templates 数据库迁移 - 迁移 8 个模块(attachments、audit_logs、dashboard、faqs、 faq_categories、fees_attachments、knowledge_notes、 material_equipments、overview、subject_histories、subject_pds) 至接口级权限检查 - 删除所有模块级权限相关测试文件,新增权限模板测试 前端: - 删除 ProjectPermissions.vue 和 ProjectPermissionsModule.vue - 重写 projectRoutePermissions.ts,改为基于接口级权限格式 - 更新 store/study.ts、router/index.ts、AuditLogs.vue、Projects.vue 中的权限 API 调用,从 fetchProjectRolePermissions 改为 fetchApiEndpointPermissions - 清理 types/api.ts 中的旧模块级权限类型定义 - 新增 PermissionTemplateSelector.vue 组件 - 更新权限管理页面,移除模块级权限 tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
133 lines
4.9 KiB
Python
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("dashboard: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
|