diff --git a/backend/app/crud/subject.py b/backend/app/crud/subject.py index 923ccf00..a972bf45 100644 --- a/backend/app/crud/subject.py +++ b/backend/app/crud/subject.py @@ -34,6 +34,9 @@ def _standard_visit_codes(visit_schedule: list[dict] | None) -> set[str]: def derive_subject_status(subject, visits: Sequence[Visit], visit_schedule: list[dict] | None = None) -> str: + if not getattr(subject, "screening_date", None): + return "" + if any(_visit_code(visit) == "提前终止" and getattr(visit, "actual_date", None) for visit in visits): return "DROPPED" @@ -116,7 +119,7 @@ async def create_subject(db: AsyncSession, study_id: uuid.UUID, subject_in: Subj study_id=study_id, site_id=subject_in.site_id, subject_no=subject_in.subject_no, - status="SCREENING", + status="SCREENING" if subject_in.screening_date else "", screening_date=subject_in.screening_date, consent_date=subject_in.consent_date, enrollment_date=subject_in.enrollment_date, @@ -194,8 +197,7 @@ async def generate_default_visits(db: AsyncSession, subject: Subject) -> None: async def update_subject(db: AsyncSession, subject: Subject, subject_in: SubjectUpdate) -> Subject: update_data = subject_in.model_dump(exclude_unset=True) - update_data.pop("status", None) - next_screening_date = subject.screening_date + next_screening_date = update_data.get("screening_date", subject.screening_date) next_consent_date = update_data.get("consent_date", subject.consent_date) next_enrollment_date = update_data.get("enrollment_date", subject.enrollment_date) next_completion_date = update_data.get("completion_date", subject.completion_date) diff --git a/backend/app/schemas/subject.py b/backend/app/schemas/subject.py index 0fed17cf..669e24e1 100644 --- a/backend/app/schemas/subject.py +++ b/backend/app/schemas/subject.py @@ -15,7 +15,7 @@ class SubjectCreate(BaseModel): class SubjectUpdate(BaseModel): - status: Optional[str] = None + screening_date: Optional[date] = None consent_date: Optional[date] = None enrollment_date: Optional[date] = None baseline_date: Optional[date] = None diff --git a/backend/tests/test_subject_medication_adherence.py b/backend/tests/test_subject_medication_adherence.py index b134f6e2..bb03083e 100644 --- a/backend/tests/test_subject_medication_adherence.py +++ b/backend/tests/test_subject_medication_adherence.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import date, datetime from types import SimpleNamespace import uuid @@ -12,6 +12,16 @@ def test_subject_update_accepts_actual_medication_count(): assert payload.actual_medication_count == 12 +def test_subject_update_accepts_screening_date(): + payload = SubjectUpdate(screening_date=date(2026, 5, 25)) + + assert payload.screening_date == date(2026, 5, 25) + + +def test_subject_update_does_not_expose_status_input(): + assert "status" not in SubjectUpdate.model_fields + + def test_subject_read_includes_actual_medication_count(): subject = SimpleNamespace( id=uuid.UUID("00000000-0000-0000-0000-000000000001"), diff --git a/backend/tests/test_visit_schedule.py b/backend/tests/test_visit_schedule.py index 9666c0c1..33341c73 100644 --- a/backend/tests/test_visit_schedule.py +++ b/backend/tests/test_visit_schedule.py @@ -275,3 +275,9 @@ def test_derive_subject_status_marks_screening_until_enrollment_date_is_filled() subject = SimpleNamespace(screening_date=date(2026, 5, 1), enrollment_date=None) assert derive_subject_status(subject, [], []) == "SCREENING" + + +def test_derive_subject_status_is_empty_before_screening_date_is_filled(): + subject = SimpleNamespace(screening_date=None, enrollment_date=None) + + assert derive_subject_status(subject, [], []) == "" diff --git a/frontend/src/views/ia/SubjectManagement.vue b/frontend/src/views/ia/SubjectManagement.vue index f097d7f2..4318785d 100644 --- a/frontend/src/views/ia/SubjectManagement.vue +++ b/frontend/src/views/ia/SubjectManagement.vue @@ -85,7 +85,7 @@ - + @@ -227,6 +227,10 @@ const getAeBadges = (row: any) => { } return badges; }; +const displaySubjectStatus = (subject: any) => { + if (!subject?.screening_date) return TEXT.common.fallback; + return displayEnum(TEXT.enums.subjectStatus, subject.status); +}; const filteredItems = computed(() => { const keyword = filters.value.keyword.trim().toLowerCase(); return items.value.filter((item) => { diff --git a/frontend/src/views/subjects/SubjectDetail.vue b/frontend/src/views/subjects/SubjectDetail.vue index f507caa1..fb884f81 100644 --- a/frontend/src/views/subjects/SubjectDetail.vue +++ b/frontend/src/views/subjects/SubjectDetail.vue @@ -30,9 +30,18 @@ {{ detail.subject_no || TEXT.common.fallback }} {{ siteMap[detail.site_id] || TEXT.common.fallback }} - {{ displayEnum(TEXT.enums.subjectStatus, detail.status) }} + {{ displaySubjectStatus(detail) }} + + + + {{ displayDate(detail.screening_date) }} - {{ displayDate(detail.screening_date) }} ({ const subjectEditing = ref(false); const subjectSaving = ref(false); const subjectForm = reactive({ + screening_date: "", enrollment_date: "", baseline_date: "", consent_date: "", @@ -665,6 +675,11 @@ const getErrorMessage = (error: any, fallback: string) => { return fallback; }; +const displaySubjectStatus = (subject: any) => { + if (!subject?.screening_date) return TEXT.common.fallback; + return displayEnum(TEXT.enums.subjectStatus, subject.status); +}; + const loadSites = async () => { if (!studyId) return; try { @@ -702,6 +717,7 @@ const startSubjectEdit = () => { ElMessage.warning("中心已停用"); return; } + subjectForm.screening_date = detail.screening_date || ""; subjectForm.consent_date = detail.consent_date || ""; subjectForm.enrollment_date = detail.enrollment_date || ""; subjectForm.baseline_date = detail.baseline_date || ""; @@ -723,6 +739,7 @@ const saveSubjectEdit = async () => { subjectSaving.value = true; try { const payload = { + screening_date: subjectForm.screening_date || null, consent_date: subjectForm.consent_date || null, enrollment_date: subjectForm.enrollment_date || null, baseline_date: subjectForm.baseline_date || null,