"""Request/response schemas for the attendance domain. Responses are emitted as plain dicts by ``serializers.py`` (wrapped in the standard envelope by the router), so the Pydantic models here are mostly request validation + OpenAPI documentation of the response shape. """ from __future__ import annotations from typing import Literal from uuid import UUID from pydantic import BaseModel, Field class RosterOverrideRequest(BaseModel): """HR add/switch a person on the intern-roster overlay. ``include`` pulls the person into the report even if they are not in the Feishu department; ``exclude`` keeps a Feishu member out. """ person_id: UUID mode: Literal["include", "exclude"] note: str | None = Field(default=None, max_length=500) class RosterOverrideOut(BaseModel): person_id: str mode: str note: str | None = None person_name: str | None = None person_email: str | None = None person_employee_no: str | None = None person_employment_status: str | None = None created_at: str | None = None class GenerateCycleRequest(BaseModel): """Generate (or refresh) all intern reports for the cycle whose end date falls in ``year``-``month`` (e.g. 2026/6 → the 5.21–6.20 cycle). """ year: int = Field(ge=2000, le=2100) month: int = Field(ge=1, le=12) class ReturnReportRequest(BaseModel): """Intern or leader returning a report to HR with a reason.""" reason: str = Field(min_length=1, max_length=1000) class CycleOut(BaseModel): id: str cycle_key: str start_date: str end_date: str intern_department: str | None = None report_count: int = 0 created_at: str | None = None class ReportListItem(BaseModel): id: str cycle_id: str cycle_key: str | None = None status: str version: int person_id: str person_name: str | None = None person_email: str | None = None person_employee_no: str | None = None person_employment_status: str | None = None leader_person_id: str | None = None leader_name: str | None = None work_days: float = 0 # duration-weighted 出勤天数 (fractional) abnormal_days: int = 0 lack_count: int = 0 late_count: int = 0 early_count: int = 0 remedied_count: int = 0 data_quality: dict | None = None generated_at: str | None = None last_sent_at: str | None = None intern_confirmed_at: str | None = None leader_confirmed_at: str | None = None returned_at: str | None = None return_reason: str | None = None returned_by: str | None = None class ReportEventOut(BaseModel): event_type: str actor_person_id: str | None = None actor_name: str | None = None from_status: str | None = None to_status: str | None = None reason: str | None = None created_at: str | None = None class ReportDetailOut(ReportListItem): snapshot: dict | None = None events: list[ReportEventOut] = Field(default_factory=list)