import uuid from datetime import date, datetime, timezone from pydantic import BaseModel, ConfigDict, field_validator class MonitoringVisitIssueCreate(BaseModel): issue_no: str | None = None site_id: uuid.UUID | None = None category: str severity: str | None = None mark: str | None = None visit_cycle: str | None = None subject_code: str | None = None monitor_item: str | None = None monitor_type: str | None = None status: str = "OPEN" source: str | None = None creator_name: str | None = None recommendation: str | None = None subject_name: str | None = None description: str | None = None action_taken: str | None = None follow_up_progress: str | None = None center_query: str | None = None center_latest_reply: str | None = None rectification_completed: bool = False found_date: date | None = None due_at: datetime | None = None actual_resolve_date: date | None = None closed_at: datetime | None = None responsible_name: str | None = None @field_validator("category", mode="before") @classmethod def _strip_required(cls, value: str) -> str: text = (value or "").strip() if not text: raise ValueError("不能为空") return text @field_validator("issue_no", mode="before") @classmethod def _strip_issue_no(cls, value: str | None) -> str | None: if value is None: return None text = value.strip() return text or None @field_validator( "subject_code", "severity", "mark", "visit_cycle", "monitor_item", "monitor_type", "source", "creator_name", "recommendation", "subject_name", "description", "action_taken", "follow_up_progress", "center_query", "center_latest_reply", "responsible_name", mode="before", ) @classmethod def _strip_optional(cls, value: str | None) -> str | None: if value is None: return None text = value.strip() return text or None @field_validator("status", mode="before") @classmethod def _normalize_status(cls, value: str | None) -> str: normalized = (value or "OPEN").strip().upper() if normalized not in {"OPEN", "CLOSED"}: raise ValueError("状态必须为 OPEN 或 CLOSED") return normalized @field_validator("due_at", "closed_at") @classmethod def _normalize_due_at(cls, value: datetime | None) -> datetime | None: if value is None: return None return value if value.tzinfo else value.replace(tzinfo=timezone.utc) class MonitoringVisitIssueRead(BaseModel): id: uuid.UUID study_id: uuid.UUID site_id: uuid.UUID | None issue_no: str open_duration: str category: str severity: str | None mark: str | None visit_cycle: str | None subject_code: str | None monitor_item: str | None monitor_type: str | None progress: str status: str source: str | None creator_name: str | None recommendation: str | None subject_name: str | None created_at: datetime description: str | None action_taken: str | None follow_up_progress: str | None center_query: str | None center_latest_reply: str | None rectification_completed: bool found_date: date | None overdue: bool due_at: datetime | None actual_resolve_date: date | None closed_at: datetime | None responsible_name: str | None model_config = ConfigDict(from_attributes=True) class MonitoringVisitIssueImportSummary(BaseModel): created_count: int updated_count: int skipped_count: int skipped_rows: list[str] class MonitoringVisitIssueUpdate(BaseModel): issue_no: str | None = None site_id: uuid.UUID | None = None category: str | None = None severity: str | None = None mark: str | None = None visit_cycle: str | None = None subject_code: str | None = None monitor_item: str | None = None monitor_type: str | None = None status: str | None = None source: str | None = None creator_name: str | None = None recommendation: str | None = None subject_name: str | None = None description: str | None = None action_taken: str | None = None follow_up_progress: str | None = None center_query: str | None = None center_latest_reply: str | None = None rectification_completed: bool | None = None found_date: date | None = None due_at: datetime | None = None actual_resolve_date: date | None = None closed_at: datetime | None = None responsible_name: str | None = None @field_validator("issue_no", "category", mode="before") @classmethod def _strip_required(cls, value: str | None) -> str | None: if value is None: return None text = value.strip() if not text: raise ValueError("不能为空") return text @field_validator( "subject_code", "severity", "mark", "visit_cycle", "monitor_item", "monitor_type", "source", "creator_name", "recommendation", "subject_name", "description", "action_taken", "follow_up_progress", "center_query", "center_latest_reply", "responsible_name", mode="before", ) @classmethod def _strip_optional(cls, value: str | None) -> str | None: if value is None: return None text = value.strip() return text or None @field_validator("status", mode="before") @classmethod def _normalize_status(cls, value: str | None) -> str | None: if value is None: return None normalized = value.strip().upper() if normalized not in {"OPEN", "CLOSED"}: raise ValueError("状态必须为 OPEN 或 CLOSED") return normalized @field_validator("due_at", "closed_at") @classmethod def _normalize_due_at(cls, value: datetime | None) -> datetime | None: if value is None: return None return value if value.tzinfo else value.replace(tzinfo=timezone.utc)