swiftops-backend / src /app /models /ticket_progress_report.py
kamau1's picture
feat: ticket progress reports, tickets incidents
dad7dc2
"""
Ticket Progress Report Model - Progress tracking for task tickets
Supervisors document work progress with narrative descriptions and photo evidence.
No subjective percentage - focus on what's done, what's left, and blockers.
"""
from sqlalchemy import Column, String, ForeignKey, Boolean, DECIMAL, TIMESTAMP, Integer, Text, Date, CheckConstraint
from sqlalchemy.dialects.postgresql import UUID, ARRAY
from sqlalchemy.orm import relationship
from datetime import datetime
import uuid
from app.core.database import Base
class TicketProgressReport(Base):
"""
Ticket Progress Report Model
Tracks work progress on task tickets with:
- Narrative description of completed work
- Issues encountered and resolved
- Team size and hours worked
- Location verification
- Photo evidence (via ticket_images with polymorphic linking)
Links to:
- tickets (required) - which ticket this report is for
- users (reported_by) - supervisor who created report
- ticket_images (via polymorphic link) - progress photos
"""
__tablename__ = "ticket_progress_reports"
# Primary Key
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
# Foreign Keys
ticket_id = Column(
UUID(as_uuid=True),
ForeignKey("tickets.id", ondelete="CASCADE"),
nullable=False,
index=True
)
reported_by_user_id = Column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="RESTRICT"),
nullable=False,
index=True
)
# Progress Narrative
work_completed_description = Column(Text, nullable=False) # What was done (required)
work_remaining_description = Column(Text, nullable=True) # What's left
issues_encountered = Column(Text, nullable=True) # Blockers/problems
issues_resolved = Column(Text, nullable=True) # What we fixed
next_steps = Column(Text, nullable=True) # What needs to happen next
estimated_completion_date = Column(Date, nullable=True) # When will this be done?
# Team and Effort Tracking
team_size_on_site = Column(Integer, nullable=True) # Number of workers present
hours_worked = Column(DECIMAL(5, 2), nullable=True) # Total man-hours
# Location Verification
report_latitude = Column(DECIMAL(10, 7), nullable=True)
report_longitude = Column(DECIMAL(10, 7), nullable=True)
location_verified = Column(Boolean, nullable=False, default=False)
# Environmental Context
weather_conditions = Column(Text, nullable=True) # Weather affecting work
notes = Column(Text, nullable=True) # Additional notes
# Timestamps
created_at = Column(
TIMESTAMP(timezone=True),
nullable=False,
default=datetime.utcnow,
server_default="timezone('utc'::text, now())"
)
updated_at = Column(
TIMESTAMP(timezone=True),
nullable=False,
default=datetime.utcnow,
onupdate=datetime.utcnow,
server_default="timezone('utc'::text, now())"
)
deleted_at = Column(TIMESTAMP(timezone=True), nullable=True)
# Relationships
ticket = relationship("Ticket", back_populates="progress_reports")
reported_by_user = relationship("User", foreign_keys=[reported_by_user_id])
# Images linked via polymorphic relationship
# Query: SELECT * FROM ticket_images WHERE linked_entity_type = 'progress_report' AND linked_entity_id = self.id
# Constraints
__table_args__ = (
CheckConstraint('team_size_on_site IS NULL OR team_size_on_site > 0', name='chk_progress_positive_team_size'),
CheckConstraint('hours_worked IS NULL OR hours_worked >= 0', name='chk_progress_positive_hours'),
)
def __repr__(self):
return f"<TicketProgressReport(id={self.id}, ticket_id={self.ticket_id}, created_at={self.created_at})>"
def to_dict(self):
"""Convert progress report to dictionary"""
return {
"id": str(self.id),
"ticket_id": str(self.ticket_id),
"reported_by_user_id": str(self.reported_by_user_id),
"work_completed_description": self.work_completed_description,
"work_remaining_description": self.work_remaining_description,
"issues_encountered": self.issues_encountered,
"issues_resolved": self.issues_resolved,
"next_steps": self.next_steps,
"estimated_completion_date": self.estimated_completion_date.isoformat() if self.estimated_completion_date else None,
"team_size_on_site": self.team_size_on_site,
"hours_worked": float(self.hours_worked) if self.hours_worked else None,
"report_latitude": float(self.report_latitude) if self.report_latitude else None,
"report_longitude": float(self.report_longitude) if self.report_longitude else None,
"location_verified": self.location_verified,
"weather_conditions": self.weather_conditions,
"notes": self.notes,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}