feat: refine subject visits and project workflows

Add early termination visit workflow with ordering, non-applicable visit handling, visit window display, and medication adherence support.

Extend monitoring visit issue template fields, site scoping, setup draft project info handling, login security UI, attachment behavior, and related tests/migrations.
This commit is contained in:
Cheng Zhou
2026-05-09 17:10:34 +08:00
parent 74feca4467
commit 917ab7ccf1
41 changed files with 3463 additions and 701 deletions
+56 -1
View File
@@ -9,7 +9,8 @@ from app.crud import site as site_crud
from app.crud import subject as subject_crud
from app.crud import study as study_crud
from app.crud import visit as visit_crud
from app.schemas.visit import VisitCreate, VisitRead, VisitUpdate
from app.schemas.subject import SubjectUpdate
from app.schemas.visit import EarlyTerminationCreate, VisitCreate, VisitRead, VisitUpdate
router = APIRouter()
@@ -110,6 +111,60 @@ async def create_visit(
return visit
@router.post(
"/early-termination",
response_model=VisitRead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_study_roles(["PM", "CRA"])), Depends(require_study_not_locked())],
)
async def create_early_termination(
study_id: uuid.UUID,
subject_id: uuid.UUID,
termination_in: EarlyTerminationCreate,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> VisitRead:
subject = await _ensure_subject(db, study_id, subject_id)
await _ensure_subject_active(db, subject)
reason = termination_in.reason.strip()
if not reason:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="提前终止原因不能为空")
if subject.baseline_date and termination_in.termination_date < subject.baseline_date:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="提前终止日期不能早于基线/治疗日期")
try:
visit = await visit_crud.create_early_termination_visit(
db,
study_id=study_id,
subject=subject,
termination_date=termination_in.termination_date,
reason=reason,
notes=termination_in.notes,
)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
await subject_crud.update_subject(
db,
subject,
SubjectUpdate(
status="DROPPED",
completion_date=termination_in.termination_date,
drop_reason=reason,
),
)
await audit_crud.log_action(
db,
study_id=study_id,
entity_type="visit",
entity_id=visit.id,
action="CREATE_EARLY_TERMINATION",
detail=f"参与者 {subject.subject_no} 已提前终止",
operator_id=current_user.id,
operator_role=current_user.role,
)
return visit
@router.patch(
"/{visit_id}",
response_model=VisitRead,