File size: 6,566 Bytes
38ac151
dad7dc2
38ac151
0a3426b
 
38ac151
 
 
 
 
 
 
 
 
dad7dc2
38ac151
dad7dc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38ac151
 
dad7dc2
 
 
 
38ac151
dad7dc2
 
 
 
 
 
 
 
 
38ac151
 
dad7dc2
38ac151
 
dad7dc2
 
 
 
 
 
 
38ac151
 
 
 
dad7dc2
38ac151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a3426b
 
38ac151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""
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"<Task {self.task_title} ({self.status})>"
    
    @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]