This commit is contained in:
HotSwapp
2025-08-16 10:05:42 -05:00
parent 0347284556
commit ae4484381f
15 changed files with 3966 additions and 77 deletions

227
app/models/timers.py Normal file
View File

@@ -0,0 +1,227 @@
"""
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})>"