Files
TimeTracker/app/models/recurring_block.py
Dries Peeters b6c0a79ffc feat: Focus mode, estimates/burndown+budget alerts, recurring blocks, saved filters, and rate overrides
Add Pomodoro focus mode with session summaries
Model: FocusSession; API: /api/focus-sessions/; UI: Focus modal on timer page
Add estimates vs actuals with burndown and budget alerts
Project fields: estimated_hours, budget_amount, budget_threshold_percent
API: /api/projects/<id>/burndown; Charts in project view and project report
Implement recurring time blocks/templates
Model: RecurringBlock; API CRUD: /api/recurring-blocks; CLI: flask generate_recurring
Add tagging and saved filters across views
Model: SavedFilter; /api/entries supports tag and saved_filter_id
Support billable rate overrides per project/member
Model: RateOverride; invoicing uses effective rate resolution
Also:
Migration: 016_add_focus_recurring_rates_filters_and_project_budget.py
Integrations and UI updates in projects view, timer page, and reports
Docs updated (startup, invoice, task mgmt) and README feature list
Added basic tests for new features
2025-10-06 13:34:56 +02:00

70 lines
2.9 KiB
Python

from datetime import datetime
from app import db
class RecurringBlock(db.Model):
"""Recurring time block template to generate time entries on a schedule.
Supports weekly recurrences with selected weekdays, start/end times, and optional
end date. Generation logic will live in a scheduler/route that expands these
templates into concrete `TimeEntry` rows.
"""
__tablename__ = 'recurring_blocks'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False, index=True)
task_id = db.Column(db.Integer, db.ForeignKey('tasks.id'), nullable=True, index=True)
name = db.Column(db.String(200), nullable=False)
# Scheduling fields
# 'weekly' for now; room to add 'daily', 'monthly' later
recurrence = db.Column(db.String(20), nullable=False, default='weekly')
# Weekdays CSV: e.g., "mon,tue,wed"; canonical lower 3-letter names
weekdays = db.Column(db.String(50), nullable=True)
# Time window in local time: "HH:MM" strings
start_time_local = db.Column(db.String(5), nullable=False) # 09:00
end_time_local = db.Column(db.String(5), nullable=False) # 11:00
# Activation window
starts_on = db.Column(db.Date, nullable=True)
ends_on = db.Column(db.Date, nullable=True)
is_active = db.Column(db.Boolean, nullable=False, default=True)
# Entry details
notes = db.Column(db.Text, nullable=True)
tags = db.Column(db.String(500), nullable=True)
billable = db.Column(db.Boolean, nullable=False, default=True)
# Tracking last generation to avoid duplicates
last_generated_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'project_id': self.project_id,
'task_id': self.task_id,
'name': self.name,
'recurrence': self.recurrence,
'weekdays': self.weekdays,
'start_time_local': self.start_time_local,
'end_time_local': self.end_time_local,
'starts_on': self.starts_on.isoformat() if self.starts_on else None,
'ends_on': self.ends_on.isoformat() if self.ends_on else None,
'is_active': self.is_active,
'notes': self.notes,
'tags': self.tags,
'billable': self.billable,
'last_generated_at': self.last_generated_at.isoformat() if self.last_generated_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}