"""Pydantic schemas for the 工号系统 (employee-number allocation) feature.""" from __future__ import annotations from datetime import date, datetime from uuid import UUID from pydantic import BaseModel, Field, field_validator class EmployeeNumberCreateRequest(BaseModel): """Allocate a single 工号. The number itself is assigned by the server — the caller supplies only the descriptive fields.""" name: str = Field(..., min_length=1, max_length=128) hire_date: date | None = None remark: str | None = Field(default=None, max_length=255) # 一级部门 (BU). Optional; validated against the live BU list server-side. bu: str | None = Field(default=None, max_length=128) @field_validator("name") @classmethod def _strip_name(cls, v: str) -> str: v = (v or "").strip() if not v: raise ValueError("name must not be blank") return v @field_validator("remark", "bu") @classmethod def _strip_optional(cls, v: str | None) -> str | None: if v is None: return None return v.strip() or None class EmployeeNumberUpdateRequest(BaseModel): """Maintain a single 工号's descriptive fields. The number itself (``employee_no`` / ``seq``) is immutable and is NOT part of this payload — only the descriptive fields can change. Same field set and validation as allocation.""" name: str = Field(..., min_length=1, max_length=128) hire_date: date | None = None remark: str | None = Field(default=None, max_length=255) bu: str | None = Field(default=None, max_length=128) @field_validator("name") @classmethod def _strip_name(cls, v: str) -> str: v = (v or "").strip() if not v: raise ValueError("name must not be blank") return v @field_validator("remark", "bu") @classmethod def _strip_optional(cls, v: str | None) -> str | None: if v is None: return None return v.strip() or None class EmployeeNumberResponse(BaseModel): id: UUID employee_no: str seq: int | None name: str | None hire_date: date | None remark: str | None bu: str | None source: str created_by: UUID | None created_by_name: str | None created_at: datetime class NextNumberResponse(BaseModel): """Preview of the number the next allocation will produce.""" next_employee_no: str next_seq: int current_max_no: str | None class SetNextNumberRequest(BaseModel): """Manually set the next 工号 the allocator will hand out. ``next_employee_no`` may be given padded or not (``P5000`` / ``P05000``); it is canonicalised server-side. The target must be strictly greater than the current maximum allocated number (a smaller value would collide with an existing 工号), so a floor only ever moves the sequence forward. """ next_employee_no: str = Field(..., min_length=1, max_length=16) reason: str = Field(..., min_length=1, max_length=500) @field_validator("next_employee_no", "reason") @classmethod def _strip(cls, v: str) -> str: v = (v or "").strip() if not v: raise ValueError("must not be blank") return v class NextNumberOverrideResponse(BaseModel): """One 工号-维护 (next-number override) record.""" id: UUID next_employee_no: str next_seq: int previous_next_employee_no: str | None previous_next_seq: int | None reason: str created_by: UUID | None created_by_name: str | None created_at: datetime class ImportErrorRow(BaseModel): row: int # 1-based row number in the uploaded file (data rows, header = row 1) employee_no: str | None reason: str class ImportResultResponse(BaseModel): total: int created: int skipped: int errors: list[ImportErrorRow] class KpiSummary(BaseModel): total: int next_employee_no: str max_employee_no: str | None created_this_month: int class TrendBucket(BaseModel): month: str # 'YYYY-MM' count: int class SourceBucket(BaseModel): source: str count: int class HireYearBucket(BaseModel): year: str # 'YYYY' or 'unknown' count: int class EmployeeNumberReportResponse(BaseModel): kpi: KpiSummary monthly_trend: list[TrendBucket] source_distribution: list[SourceBucket] hire_year_distribution: list[HireYearBucket]