mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-03 19:00:13 -05:00
154f9b37a6
Refactored the existing calendar API endpoint to properly display calendar
events, tasks, and time entries with distinct visual representations.
Changes:
- Updated /api/calendar/events endpoint in api.py to use new
CalendarEvent.get_events_in_range() method that fetches all three item types
- Fixed user_id bug where it was defaulting to None instead of current_user.id
- Modified API response format to include all items in unified 'events' array
with item_type field ('event', 'task', 'time_entry') for differentiation
- Updated calendar.js to parse unified response format and filter items by type
- Added visual distinctions:
* Tasks: 📋 emoji, orange (#f59e0b) color, clickable
* Time entries: ⏱ emoji, project-based colors, non-clickable
* Calendar events: 📅 emoji, custom colors, clickable
- Fixed task detail route from /tasks/view/{id} to /tasks/{id}
- Updated all calendar view renderers (month, week, day) to use correct
data structure with extendedProps
- Added cache-busting to calendar.js (v7) and calendar.css (v2)
- Preserved backward compatibility with existing calendar filtering
(project_id, task_id, tags)
The calendar now correctly displays all time tracking data in a unified view
with proper visual hierarchy and interaction patterns.
Fixes: Calendar not showing tasks and time entries
Related: Calendar/Agenda Support feature implementation
228 lines
9.8 KiB
Python
228 lines
9.8 KiB
Python
from datetime import datetime
|
|
from app import db
|
|
from app.utils.timezone import now_in_app_timezone
|
|
|
|
|
|
class CalendarEvent(db.Model):
|
|
"""Calendar event model for scheduling meetings, appointments, and other events"""
|
|
|
|
__tablename__ = 'calendar_events'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
|
|
title = db.Column(db.String(200), nullable=False)
|
|
description = db.Column(db.Text, nullable=True)
|
|
start_time = db.Column(db.DateTime, nullable=False, index=True)
|
|
end_time = db.Column(db.DateTime, nullable=False, index=True)
|
|
all_day = db.Column(db.Boolean, default=False, nullable=False)
|
|
location = db.Column(db.String(200), nullable=True)
|
|
|
|
# Event type: meeting, appointment, reminder, deadline, or custom
|
|
event_type = db.Column(db.String(50), default='event', nullable=False, index=True)
|
|
|
|
# Optional associations
|
|
project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=True, index=True)
|
|
task_id = db.Column(db.Integer, db.ForeignKey('tasks.id'), nullable=True, index=True)
|
|
client_id = db.Column(db.Integer, db.ForeignKey('clients.id'), nullable=True, index=True)
|
|
|
|
# Recurring event support
|
|
is_recurring = db.Column(db.Boolean, default=False, nullable=False)
|
|
recurrence_rule = db.Column(db.String(200), nullable=True) # RRULE format (e.g., "FREQ=WEEKLY;BYDAY=MO,WE,FR")
|
|
recurrence_end_date = db.Column(db.DateTime, nullable=True)
|
|
parent_event_id = db.Column(db.Integer, db.ForeignKey('calendar_events.id'), nullable=True, index=True)
|
|
|
|
# Reminders
|
|
reminder_minutes = db.Column(db.Integer, nullable=True) # Minutes before event to remind
|
|
|
|
# Color coding
|
|
color = db.Column(db.String(7), nullable=True) # Hex color code (e.g., #FF5733)
|
|
|
|
# Privacy
|
|
is_private = db.Column(db.Boolean, default=False, nullable=False)
|
|
|
|
created_at = db.Column(db.DateTime, default=now_in_app_timezone, nullable=False)
|
|
updated_at = db.Column(db.DateTime, default=now_in_app_timezone, onupdate=now_in_app_timezone, nullable=False)
|
|
|
|
# Relationships
|
|
user = db.relationship('User', backref=db.backref('calendar_events', lazy='dynamic', cascade='all, delete-orphan'))
|
|
project = db.relationship('Project', backref=db.backref('calendar_events', lazy='dynamic'))
|
|
task = db.relationship('Task', backref=db.backref('calendar_events', lazy='dynamic'))
|
|
client = db.relationship('Client', backref=db.backref('calendar_events', lazy='dynamic'))
|
|
|
|
# For recurring events - parent/child relationship
|
|
child_events = db.relationship(
|
|
'CalendarEvent',
|
|
backref=db.backref('parent_event', remote_side=[id]),
|
|
foreign_keys=[parent_event_id],
|
|
lazy='dynamic',
|
|
cascade='all, delete-orphan'
|
|
)
|
|
|
|
def __init__(self, user_id, title, start_time, end_time, **kwargs):
|
|
"""Initialize a CalendarEvent instance.
|
|
|
|
Args:
|
|
user_id: ID of the user who created this event
|
|
title: Title of the event
|
|
start_time: Start datetime of the event
|
|
end_time: End datetime of the event
|
|
**kwargs: Additional optional fields
|
|
"""
|
|
self.user_id = user_id
|
|
self.title = title
|
|
self.start_time = start_time
|
|
self.end_time = end_time
|
|
|
|
for key, value in kwargs.items():
|
|
if hasattr(self, key):
|
|
setattr(self, key, value)
|
|
|
|
def __repr__(self):
|
|
return f'<CalendarEvent {self.title} ({self.start_time})>'
|
|
|
|
def to_dict(self):
|
|
"""Convert event to dictionary for API responses"""
|
|
return {
|
|
'id': self.id,
|
|
'title': self.title,
|
|
'description': self.description,
|
|
'start': self.start_time.isoformat() if self.start_time else None,
|
|
'end': self.end_time.isoformat() if self.end_time else None,
|
|
'allDay': self.all_day,
|
|
'location': self.location,
|
|
'eventType': self.event_type,
|
|
'projectId': self.project_id,
|
|
'taskId': self.task_id,
|
|
'clientId': self.client_id,
|
|
'isRecurring': self.is_recurring,
|
|
'recurrenceRule': self.recurrence_rule,
|
|
'recurrenceEndDate': self.recurrence_end_date.isoformat() if self.recurrence_end_date else None,
|
|
'parentEventId': self.parent_event_id,
|
|
'reminderMinutes': self.reminder_minutes,
|
|
'color': self.color,
|
|
'isPrivate': self.is_private,
|
|
'createdAt': self.created_at.isoformat() if self.created_at else None,
|
|
'updatedAt': self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
def duration_hours(self):
|
|
"""Calculate duration of event in hours"""
|
|
if self.start_time and self.end_time:
|
|
delta = self.end_time - self.start_time
|
|
return delta.total_seconds() / 3600
|
|
return 0
|
|
|
|
@staticmethod
|
|
def get_events_in_range(user_id, start_date, end_date, include_tasks=False, include_time_entries=False):
|
|
"""Get all events for a user within a date range.
|
|
|
|
Args:
|
|
user_id: ID of the user
|
|
start_date: Start of date range
|
|
end_date: End of date range
|
|
include_tasks: Whether to include tasks with due dates
|
|
include_time_entries: Whether to include time entries
|
|
|
|
Returns:
|
|
Dictionary with events, tasks, and time entries
|
|
"""
|
|
from app.models import Task, TimeEntry
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
print(f"\n{'*'*80}")
|
|
print(f"MODEL - get_events_in_range called:")
|
|
print(f" user_id={user_id}")
|
|
print(f" start={start_date}")
|
|
print(f" end={end_date}")
|
|
print(f" include_tasks={include_tasks} (type: {type(include_tasks)})")
|
|
print(f" include_time_entries={include_time_entries} (type: {type(include_time_entries)})")
|
|
print(f"{'*'*80}\n")
|
|
|
|
logger.info(f"get_events_in_range called: user_id={user_id}, start={start_date}, end={end_date}, include_tasks={include_tasks}, include_time_entries={include_time_entries}")
|
|
|
|
result = {
|
|
'events': [],
|
|
'tasks': [],
|
|
'time_entries': []
|
|
}
|
|
|
|
# Get calendar events
|
|
events = CalendarEvent.query.filter(
|
|
CalendarEvent.user_id == user_id,
|
|
CalendarEvent.start_time >= start_date,
|
|
CalendarEvent.start_time <= end_date
|
|
).order_by(CalendarEvent.start_time).all()
|
|
|
|
logger.info(f"Found {len(events)} calendar events")
|
|
print(f"MODEL - Found {len(events)} calendar events")
|
|
result['events'] = [event.to_dict() for event in events]
|
|
|
|
# Optionally include tasks with due dates
|
|
if include_tasks:
|
|
print(f"MODEL - Querying tasks for user {user_id}")
|
|
logger.info(f"Querying tasks for user {user_id}")
|
|
tasks = Task.query.filter(
|
|
Task.assigned_to == user_id,
|
|
Task.due_date.isnot(None),
|
|
Task.due_date >= start_date.date() if hasattr(start_date, 'date') else start_date,
|
|
Task.due_date <= end_date.date() if hasattr(end_date, 'date') else end_date,
|
|
Task.status.in_(['todo', 'in_progress', 'review'])
|
|
).all()
|
|
|
|
print(f"MODEL - Found {len(tasks)} tasks with due dates")
|
|
logger.info(f"Found {len(tasks)} tasks with due dates")
|
|
|
|
result['tasks'] = [{
|
|
'id': task.id,
|
|
'title': task.name,
|
|
'description': task.description,
|
|
'dueDate': task.due_date.isoformat() if task.due_date else None,
|
|
'status': task.status,
|
|
'priority': task.priority,
|
|
'projectId': task.project_id,
|
|
'type': 'task'
|
|
} for task in tasks]
|
|
else:
|
|
print(f"MODEL - Not including tasks (include_tasks=False)")
|
|
logger.info("Not including tasks (include_tasks=False)")
|
|
|
|
# Optionally include time entries
|
|
if include_time_entries:
|
|
print(f"MODEL - Querying time entries for user {user_id}")
|
|
logger.info(f"Querying time entries for user {user_id}")
|
|
time_entries = TimeEntry.query.filter(
|
|
TimeEntry.user_id == user_id,
|
|
TimeEntry.start_time >= start_date,
|
|
TimeEntry.start_time <= end_date
|
|
).order_by(TimeEntry.start_time).all()
|
|
|
|
print(f"MODEL - Found {len(time_entries)} time entries")
|
|
logger.info(f"Found {len(time_entries)} time entries")
|
|
|
|
result['time_entries'] = [{
|
|
'id': entry.id,
|
|
'title': f"Time: {entry.project.name if entry.project else 'Unknown'}",
|
|
'start': entry.start_time.isoformat() if entry.start_time else None,
|
|
'end': entry.end_time.isoformat() if entry.end_time else None,
|
|
'projectId': entry.project_id,
|
|
'taskId': entry.task_id,
|
|
'notes': entry.notes,
|
|
'type': 'time_entry'
|
|
} for entry in time_entries]
|
|
else:
|
|
print(f"MODEL - Not including time entries (include_time_entries=False)")
|
|
logger.info("Not including time entries (include_time_entries=False)")
|
|
|
|
print(f"\n{'*'*80}")
|
|
print(f"MODEL - Returning:")
|
|
print(f" events: {len(result['events'])}")
|
|
print(f" tasks: {len(result['tasks'])}")
|
|
print(f" time_entries: {len(result['time_entries'])}")
|
|
print(f"{'*'*80}\n")
|
|
|
|
logger.info(f"Returning: {len(result['events'])} events, {len(result['tasks'])} tasks, {len(result['time_entries'])} time_entries")
|
|
return result
|
|
|