from pydantic import BaseModel, computed_field, Field, field_validator from typing import Optional, List, Any, Dict from datetime import datetime from decimal import Decimal from enum import Enum class Priority(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" URGENT = "urgent" class CustomValueInput(BaseModel): """Input for setting a custom field value.""" field_id: str value: Optional[Any] = None # Can be string, number, date string, or user id class CustomValueResponse(BaseModel): """Response for a custom field value.""" field_id: str field_name: str field_type: str value: Optional[Any] = None display_value: Optional[str] = None # Formatted for display class TaskBase(BaseModel): title: str = Field(..., min_length=1, max_length=500) description: Optional[str] = Field(None, max_length=10000) priority: Priority = Priority.MEDIUM original_estimate: Optional[Decimal] = Field(None, ge=0, le=99999) start_date: Optional[datetime] = None due_date: Optional[datetime] = None class TaskCreate(TaskBase): parent_task_id: Optional[str] = None assignee_id: Optional[str] = None status_id: Optional[str] = None custom_values: Optional[List[CustomValueInput]] = None class TaskUpdate(BaseModel): title: Optional[str] = Field(None, min_length=1, max_length=500) description: Optional[str] = Field(None, max_length=10000) priority: Optional[Priority] = None status_id: Optional[str] = None assignee_id: Optional[str] = None original_estimate: Optional[Decimal] = Field(None, ge=0, le=99999) time_spent: Optional[Decimal] = Field(None, ge=0, le=99999) start_date: Optional[datetime] = None due_date: Optional[datetime] = None position: Optional[int] = Field(None, ge=0) custom_values: Optional[List[CustomValueInput]] = None version: Optional[int] = Field(None, ge=1, description="Version for optimistic locking") class TaskStatusUpdate(BaseModel): status_id: str class TaskAssignUpdate(BaseModel): assignee_id: Optional[str] = None class TaskResponse(TaskBase): id: str project_id: str parent_task_id: Optional[str] = None assignee_id: Optional[str] = None status_id: Optional[str] = None time_spent: Decimal blocker_flag: bool position: int created_by: str created_at: datetime updated_at: datetime version: int = 1 # Optimistic locking version class Config: from_attributes = True # Alias for original_estimate for frontend compatibility @computed_field @property def time_estimate(self) -> Optional[Decimal]: return self.original_estimate class TaskWithDetails(TaskResponse): assignee_name: Optional[str] = None status_name: Optional[str] = None status_color: Optional[str] = None creator_name: Optional[str] = None subtask_count: int = 0 custom_values: Optional[List[CustomValueResponse]] = None class TaskListResponse(BaseModel): tasks: List[TaskWithDetails] total: int class TaskRestoreRequest(BaseModel): """Request body for restoring a soft-deleted task.""" cascade: bool = Field( default=True, description="If True, also restore child tasks deleted at the same time. If False, restore only the parent task." ) class TaskRestoreResponse(BaseModel): """Response for task restore operation.""" restored_task: TaskResponse restored_children_count: int = 0 restored_children_ids: List[str] = [] class TaskDeleteWarningResponse(BaseModel): """Response when task has unresolved blockers and force_delete is False.""" warning: str blocker_count: int message: str = "Task has unresolved blockers. Use force_delete=true to delete anyway." class TaskDeleteResponse(BaseModel): """Response for task delete operation.""" task: TaskResponse blockers_resolved: int = 0 force_deleted: bool = False