""" TASK Models - For any project type """ from sqlalchemy import Column, String, Boolean, Integer, Text, Date, DateTime, Numeric, ForeignKey, Double, CheckConstraint from sqlalchemy.dialects.postgresql import UUID, JSONB, ARRAY, ENUM from sqlalchemy.orm import relationship from datetime import datetime, date from typing import Optional from app.models.base import BaseModel from app.models.enums import TaskStatus, TicketPriority class Task(BaseModel): """ Tasks (Project Work Items) Tasks represent discrete work items for ANY project type that require: - Field agent assignment - Location-based execution - Expense tracking and reimbursement - Status tracking and completion verification Common Use Cases: 1. **Infrastructure Projects:** - Install fiber cable from pole A to pole B - Maintenance of network equipment - Site surveys for network expansion - Equipment testing and quality checks 2. **Customer Service Projects (FTTH, Fixed Wireless, etc.):** - Deliver ONT devices to warehouse - Pick up faulty equipment from customer sites - Conduct pre-installation site surveys - Customer training and orientation visits - Equipment distribution to field agents 3. **General Operations:** - Any work requiring compensation tracking - Logistics and transportation tasks - Multi-location work assignments Key Features: - Flexible task_type field (no enum constraint, stored as TEXT) - Optional location with GPS coordinates - Links to project regions for team organization - Status tracking: pending → assigned → in_progress → completed - Priority levels for scheduling - Timeline tracking (scheduled, started, completed) Workflow: 1. Manager creates Task for work that needs to be done 2. Task is converted to Ticket (source='task') for field assignment 3. Ticket assigned to field agent(s) 4. Agent executes work and logs expenses via TicketExpense 5. Manager reviews and approves expenses 6. Agent receives reimbursement Business Rules: - Task must belong to a project (any type) - Optional region assignment for geographic organization - Timeline validation (completed_at >= started_at) - Can generate multiple tickets (if work needs to be re-done) Expense Tracking: - Tasks → Tickets → TicketAssignments → TicketExpenses - All expenses linked to ticket assignments for accountability - Expenses require approval before payment - Supports transport, materials, accommodation, meals, etc. """ __tablename__ = "tasks" # Project Link (any project type) project_id = Column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True) # Task Details task_title = Column(Text, nullable=False) task_description = Column(Text, nullable=True) task_type = Column(Text, nullable=True) # 'installation', 'maintenance', 'survey', 'testing' # Location (where work needs to be done) location_name = Column(Text, nullable=True) # e.g., "Pole 45, Ngong Road" project_region_id = Column(UUID(as_uuid=True), ForeignKey("project_regions.id", ondelete="SET NULL"), nullable=True, index=True) task_address_line1 = Column(Text, nullable=True) task_address_line2 = Column(Text, nullable=True) task_maps_link = Column(Text, nullable=True) # Google Maps link for navigation task_latitude = Column(Double, nullable=True) task_longitude = Column(Double, nullable=True) # Status (Assignment handled via tickets) status = Column(ENUM(TaskStatus, name="taskstatus", create_type=False, values_callable=lambda x: [e.value for e in x]), nullable=False, default=TaskStatus.PENDING.value) priority = Column(ENUM(TicketPriority, name="ticket_priority", create_type=False, values_callable=lambda x: [e.value for e in x]), nullable=False, default=TicketPriority.NORMAL.value) # Timeline scheduled_date = Column(Date, nullable=True) started_at = Column(DateTime(timezone=True), nullable=True) completed_at = Column(DateTime(timezone=True), nullable=True) # Metadata notes = Column(Text, nullable=True) additional_metadata = Column(JSONB, default={}, nullable=False) # Created by created_by_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True) # Relationships project = relationship("Project", back_populates="tasks") project_region = relationship("ProjectRegion", foreign_keys=[project_region_id]) created_by = relationship("User", foreign_keys=[created_by_user_id]) # tickets = relationship("Ticket", back_populates="task", lazy="dynamic") # Tickets generated from this task # Constraints __table_args__ = ( CheckConstraint( 'completed_at IS NULL OR started_at IS NULL OR completed_at >= started_at', name='chk_task_dates' ), ) def __repr__(self): return f"" @property def is_completed(self) -> bool: """Check if task is completed""" return self.status == TaskStatus.COMPLETED @property def is_overdue(self) -> bool: """Check if task is overdue (past scheduled date and not completed)""" if not self.scheduled_date or self.is_completed: return False return date.today() > self.scheduled_date @property def has_location(self) -> bool: """Check if task has location coordinates""" return self.task_latitude is not None and self.task_longitude is not None @property def duration_days(self) -> Optional[int]: """Calculate duration in days if both start and completion dates exist""" if self.started_at and self.completed_at: delta = self.completed_at - self.started_at return delta.days return None def can_start(self) -> bool: """Check if task can be started""" return self.status in [TaskStatus.PENDING, TaskStatus.ASSIGNED] def can_complete(self) -> bool: """Check if task can be completed""" return self.status in [TaskStatus.ASSIGNED, TaskStatus.IN_PROGRESS] def can_cancel(self) -> bool: """Check if task can be cancelled""" return self.status not in [TaskStatus.COMPLETED, TaskStatus.CANCELLED]