progress
This commit is contained in:
227
app/models/timers.py
Normal file
227
app/models/timers.py
Normal 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})>"
|
||||
Reference in New Issue
Block a user