Spaces:
Sleeping
Sleeping
| """ | |
| 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, | |
| } | |