227 lines
8.7 KiB
Python
227 lines
8.7 KiB
Python
"""
|
|
Timer and time tracking models for integrated time tracking system
|
|
"""
|
|
from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, ForeignKey, Enum
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
from app.models.base import BaseModel
|
|
import enum
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
class TimerStatus(str, enum.Enum):
|
|
"""Timer status enumeration"""
|
|
STOPPED = "stopped"
|
|
RUNNING = "running"
|
|
PAUSED = "paused"
|
|
|
|
|
|
class TimerType(str, enum.Enum):
|
|
"""Timer type enumeration"""
|
|
BILLABLE = "billable"
|
|
NON_BILLABLE = "non_billable"
|
|
ADMINISTRATIVE = "administrative"
|
|
|
|
|
|
class Timer(BaseModel):
|
|
"""
|
|
Active timer sessions for time tracking
|
|
Represents a running, paused, or stopped timer
|
|
"""
|
|
__tablename__ = "timers"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
# User and assignment
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
file_no = Column(String(45), ForeignKey("files.file_no")) # Optional file assignment
|
|
customer_id = Column(String(45), ForeignKey("rolodex.id")) # Optional customer assignment
|
|
|
|
# Timer details
|
|
title = Column(String(200), nullable=False) # Brief description of task
|
|
description = Column(Text) # Detailed description of work
|
|
timer_type = Column(Enum(TimerType), default=TimerType.BILLABLE)
|
|
|
|
# Time tracking
|
|
status = Column(Enum(TimerStatus), default=TimerStatus.STOPPED)
|
|
total_seconds = Column(Integer, default=0) # Total accumulated time in seconds
|
|
|
|
# Session timing
|
|
started_at = Column(DateTime(timezone=True)) # When timer was first started
|
|
last_started_at = Column(DateTime(timezone=True)) # When current session started
|
|
last_paused_at = Column(DateTime(timezone=True)) # When timer was last paused
|
|
stopped_at = Column(DateTime(timezone=True)) # When timer was stopped
|
|
|
|
# Billing information
|
|
hourly_rate = Column(Float) # Override rate for this timer
|
|
is_billable = Column(Boolean, default=True)
|
|
|
|
# Metadata
|
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
|
|
|
# Task categorization
|
|
task_category = Column(String(50)) # research, drafting, client_call, court_appearance, etc.
|
|
notes = Column(Text) # Additional notes
|
|
|
|
# Relationships
|
|
user = relationship("User", back_populates="timers")
|
|
file = relationship("File", back_populates="timers")
|
|
customer = relationship("Rolodex", back_populates="timers")
|
|
time_entries = relationship("TimeEntry", back_populates="timer", cascade="all, delete-orphan")
|
|
|
|
def __repr__(self):
|
|
return f"<Timer(id={self.id}, title='{self.title}', status='{self.status}', total_seconds={self.total_seconds})>"
|
|
|
|
@property
|
|
def total_hours(self) -> float:
|
|
"""Get total time in hours"""
|
|
return self.total_seconds / 3600.0 if self.total_seconds else 0.0
|
|
|
|
@property
|
|
def is_active(self) -> bool:
|
|
"""Check if timer is currently running or paused"""
|
|
return self.status in [TimerStatus.RUNNING, TimerStatus.PAUSED]
|
|
|
|
def get_current_session_seconds(self) -> int:
|
|
"""Get seconds for current running session"""
|
|
if self.status == TimerStatus.RUNNING and self.last_started_at:
|
|
now = datetime.now(timezone.utc)
|
|
session_duration = (now - self.last_started_at).total_seconds()
|
|
return int(session_duration)
|
|
return 0
|
|
|
|
def get_total_current_seconds(self) -> int:
|
|
"""Get total seconds including current running session"""
|
|
return self.total_seconds + self.get_current_session_seconds()
|
|
|
|
|
|
class TimeEntry(BaseModel):
|
|
"""
|
|
Completed time entries that can be converted to billing transactions
|
|
Represents finalized time that has been logged
|
|
"""
|
|
__tablename__ = "time_entries"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
# Source timer (optional - entries can be created manually)
|
|
timer_id = Column(Integer, ForeignKey("timers.id"))
|
|
|
|
# User and assignment
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
file_no = Column(String(45), ForeignKey("files.file_no")) # Optional file assignment
|
|
customer_id = Column(String(45), ForeignKey("rolodex.id")) # Optional customer assignment
|
|
|
|
# Time entry details
|
|
title = Column(String(200), nullable=False)
|
|
description = Column(Text)
|
|
entry_type = Column(Enum(TimerType), default=TimerType.BILLABLE)
|
|
|
|
# Time information
|
|
hours = Column(Float, nullable=False) # Time in decimal hours (e.g., 1.5 = 1 hour 30 minutes)
|
|
entry_date = Column(DateTime(timezone=True), nullable=False) # Date/time when work was performed
|
|
|
|
# Billing information
|
|
hourly_rate = Column(Float) # Rate for this entry
|
|
is_billable = Column(Boolean, default=True)
|
|
billed = Column(Boolean, default=False) # Whether this has been converted to a ledger entry
|
|
|
|
# Related ledger entry (after billing)
|
|
ledger_id = Column(Integer, ForeignKey("ledger.id")) # Link to created billing transaction
|
|
|
|
# Metadata
|
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
|
created_by = Column(String(50)) # Who created this entry
|
|
|
|
# Task categorization
|
|
task_category = Column(String(50)) # research, drafting, client_call, court_appearance, etc.
|
|
notes = Column(Text) # Additional notes
|
|
|
|
# Approval workflow
|
|
approved = Column(Boolean, default=False)
|
|
approved_by = Column(String(50))
|
|
approved_at = Column(DateTime(timezone=True))
|
|
|
|
# Relationships
|
|
timer = relationship("Timer", back_populates="time_entries")
|
|
user = relationship("User", back_populates="time_entries")
|
|
file = relationship("File", back_populates="time_entries")
|
|
customer = relationship("Rolodex", back_populates="time_entries")
|
|
ledger_entry = relationship("Ledger")
|
|
|
|
def __repr__(self):
|
|
return f"<TimeEntry(id={self.id}, title='{self.title}', hours={self.hours}, billed={self.billed})>"
|
|
|
|
@property
|
|
def calculated_amount(self) -> float:
|
|
"""Calculate billable amount based on hours and rate"""
|
|
if not self.is_billable or not self.hourly_rate:
|
|
return 0.0
|
|
return self.hours * self.hourly_rate
|
|
|
|
|
|
class TimerSession(BaseModel):
|
|
"""
|
|
Individual timer sessions for detailed tracking
|
|
Each start/stop cycle creates a session record
|
|
"""
|
|
__tablename__ = "timer_sessions"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
timer_id = Column(Integer, ForeignKey("timers.id"), nullable=False)
|
|
|
|
# Session timing
|
|
started_at = Column(DateTime(timezone=True), nullable=False)
|
|
ended_at = Column(DateTime(timezone=True))
|
|
duration_seconds = Column(Integer, default=0)
|
|
|
|
# Session notes
|
|
notes = Column(Text) # Notes for this specific session
|
|
|
|
# Pause tracking
|
|
pause_count = Column(Integer, default=0) # Number of times paused during session
|
|
total_pause_seconds = Column(Integer, default=0) # Total time spent paused
|
|
|
|
# Relationships
|
|
timer = relationship("Timer")
|
|
|
|
def __repr__(self):
|
|
return f"<TimerSession(id={self.id}, timer_id={self.timer_id}, duration_seconds={self.duration_seconds})>"
|
|
|
|
@property
|
|
def duration_hours(self) -> float:
|
|
"""Get session duration in hours"""
|
|
return self.duration_seconds / 3600.0 if self.duration_seconds else 0.0
|
|
|
|
|
|
class TimerTemplate(BaseModel):
|
|
"""
|
|
Predefined timer templates for common tasks
|
|
Allows quick creation of timers with pre-filled information
|
|
"""
|
|
__tablename__ = "timer_templates"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
# Template details
|
|
name = Column(String(100), nullable=False)
|
|
title_template = Column(String(200), nullable=False) # Template for timer title
|
|
description_template = Column(Text) # Template for timer description
|
|
|
|
# Default settings
|
|
timer_type = Column(Enum(TimerType), default=TimerType.BILLABLE)
|
|
task_category = Column(String(50))
|
|
default_rate = Column(Float)
|
|
is_billable = Column(Boolean, default=True)
|
|
|
|
# Template metadata
|
|
created_by = Column(String(50))
|
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
|
is_active = Column(Boolean, default=True)
|
|
usage_count = Column(Integer, default=0) # Track how often template is used
|
|
|
|
def __repr__(self):
|
|
return f"<TimerTemplate(id={self.id}, name='{self.name}', usage_count={self.usage_count})>" |