""" 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"" 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, }