import uuid from datetime import date, datetime from decimal import Decimal from pydantic import BaseModel, Field, model_validator from app.models.absence import AbsenceStatus from app.models.absence_type import AbsenceCategory # ── AbsenceType ─────────────────────────────────────────────────────────────── class AbsenceTypeOut(BaseModel): model_config = {"from_attributes": True} id: uuid.UUID company_id: uuid.UUID name: str color: str category: AbsenceCategory requires_approval: bool deducts_vacation: bool affects_overtime_balance: bool requires_certificate: bool certificate_after_days: int is_paid: bool max_days_per_year: int | None is_active: bool class AbsenceTypeCreate(BaseModel): name: str = Field(min_length=1, max_length=255) color: str = Field("#3B82F6", pattern=r"^#[0-9A-Fa-f]{6}$") category: AbsenceCategory = AbsenceCategory.OTHER requires_approval: bool = True deducts_vacation: bool = False affects_overtime_balance: bool = False requires_certificate: bool = False certificate_after_days: int = Field(3, ge=0, le=365) is_paid: bool = True max_days_per_year: int | None = Field(None, ge=1) class AbsenceTypeUpdate(BaseModel): name: str | None = Field(None, min_length=1, max_length=255) color: str | None = Field(None, pattern=r"^#[0-9A-Fa-f]{6}$") category: AbsenceCategory | None = None requires_approval: bool | None = None deducts_vacation: bool | None = None affects_overtime_balance: bool | None = None requires_certificate: bool | None = None certificate_after_days: int | None = Field(None, ge=0, le=365) is_paid: bool | None = None max_days_per_year: int | None = Field(None, ge=1) is_active: bool | None = None # ── Absence ─────────────────────────────────────────────────────────────────── class AbsenceOut(BaseModel): model_config = {"from_attributes": True} id: uuid.UUID user_id: uuid.UUID type_id: uuid.UUID start_date: date end_date: date half_day_start: bool half_day_end: bool working_days: float fza_hours: Decimal | None = None status: AbsenceStatus approved_by: uuid.UUID | None substitute_id: uuid.UUID | None note: str | None correction_note: str | None rejection_reason: str | None certificate_required_by: date | None = None certificate_received_at: date | None = None created_at: datetime class AbsenceCreate(BaseModel): type_id: uuid.UUID start_date: date end_date: date half_day_start: bool = False half_day_end: bool = False substitute_id: uuid.UUID | None = None note: str | None = None for_user_id: uuid.UUID | None = None # HR/Admin: Abwesenheit für anderen Mitarbeiter anlegen fza_hours: Decimal | None = Field( None, ge=Decimal("0.25"), le=Decimal("24"), description="FZA in Stunden (statt Tagen); nur bei eintägigem Zeitraum erlaubt.", ) def model_post_init(self, __context) -> None: if self.end_date < self.start_date: raise ValueError("end_date must be >= start_date") if self.fza_hours is not None and self.start_date != self.end_date: raise ValueError("fza_hours ist nur erlaubt wenn start_date == end_date (eintägiger FZA).") class AbsenceUpdate(BaseModel): type_id: uuid.UUID | None = None start_date: date | None = None end_date: date | None = None half_day_start: bool | None = None half_day_end: bool | None = None substitute_id: uuid.UUID | None = None note: str | None = None correction_note: str | None = None # Pflicht bei Änderung genehmigter Anträge (Mitarbeiter) def model_post_init(self, __context) -> None: if self.start_date and self.end_date and self.end_date < self.start_date: raise ValueError("end_date must be >= start_date") class AbsenceReject(BaseModel): rejection_reason: str = Field(min_length=1) class AbsenceListResponse(BaseModel): total: int items: list[AbsenceOut] # ── Krankmeldung ────────────────────────────────────────────────────────────── class QuickSickIn(BaseModel): start_date: date end_date: date def model_post_init(self, __context) -> None: if self.end_date < self.start_date: raise ValueError("end_date must be >= start_date") class CertificateMarkIn(BaseModel): received_at: date | None = None # default = heute class SickStatsOut(BaseModel): user_id: uuid.UUID user_name: str personnel_number: str | None = None episodes: int total_days: float bradford_factor: float certificates_overdue: int # ── VacationBalance ─────────────────────────────────────────────────────────── class VacationBalanceOut(BaseModel): model_config = {"from_attributes": True} id: uuid.UUID user_id: uuid.UUID year: int entitled_days: int special_days: int = 0 carried_over: int used_days: int total_days: int remaining_days: int pending_days: float = 0 # Resturlaub-Verfall (wird zur Laufzeit befüllt, nicht in DB) carried_over_expires_at: date | None = None carried_over_expired: bool = False class VacationBalanceUpdate(BaseModel): entitled_days: int | None = Field(None, ge=0, le=365) special_days: int | None = Field(None, ge=0, le=365) carried_over: int | None = Field(None, ge=0, le=365) # ── PublicHoliday ───────────────────────────────────────────────────────────── class PublicHolidayOut(BaseModel): model_config = {"from_attributes": True} id: uuid.UUID country: str state: str | None date: date name: str year: int class PublicHolidayCreate(BaseModel): country: str = Field("DE", min_length=2, max_length=10) state: str | None = Field(None, max_length=10) date: date name: str = Field(min_length=1, max_length=255) # ── OvertimeBalance ─────────────────────────────────────────────────────────── class OvertimeBalanceOut(BaseModel): total_hours: float taken_hours: float available_hours: float # ── Calendar ────────────────────────────────────────────────────────────────── class CalendarEntry(BaseModel): user_id: uuid.UUID user_name: str absence_id: uuid.UUID type_name: str type_color: str start_date: date end_date: date status: AbsenceStatus working_days: float