from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, or_ from datetime import datetime, date, timedelta from typing import Optional, List, Dict, Any from app.database import get_db from app.models.settings import Settings from app.models.todo import Todo from app.models.calendar_event import CalendarEvent from app.models.calendar import Calendar from app.models.reminder import Reminder from app.models.project import Project from app.models.user import User from app.routers.auth import get_current_user, get_current_settings router = APIRouter() # Reusable filter: exclude parent template events (they have a real recurrence_rule). # Some legacy events have "" instead of NULL, so allow both. _not_parent_template = or_( CalendarEvent.recurrence_rule == None, CalendarEvent.recurrence_rule == "", ) @router.get("/dashboard") async def get_dashboard( client_date: Optional[date] = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), current_settings: Settings = Depends(get_current_settings), ): """Get aggregated dashboard data.""" today = client_date or date.today() upcoming_cutoff = today + timedelta(days=current_settings.upcoming_days) # Subquery: calendar IDs belonging to this user (for event scoping) user_calendar_ids = select(Calendar.id).where(Calendar.user_id == current_user.id) # Today's events (exclude parent templates — they are hidden, children are shown) today_start = datetime.combine(today, datetime.min.time()) today_end = datetime.combine(today, datetime.max.time()) events_query = select(CalendarEvent).where( CalendarEvent.calendar_id.in_(user_calendar_ids), CalendarEvent.start_datetime >= today_start, CalendarEvent.start_datetime <= today_end, _not_parent_template, ) events_result = await db.execute(events_query) todays_events = events_result.scalars().all() # Upcoming todos (not completed, with due date from today through upcoming_days) todos_query = select(Todo).where( Todo.user_id == current_user.id, Todo.completed == False, Todo.due_date.isnot(None), Todo.due_date >= today, Todo.due_date <= upcoming_cutoff ).order_by(Todo.due_date.asc()) todos_result = await db.execute(todos_query) upcoming_todos = todos_result.scalars().all() # Active reminders (not dismissed, is_active = true, from today onward) reminders_query = select(Reminder).where( Reminder.user_id == current_user.id, Reminder.is_active == True, Reminder.is_dismissed == False, Reminder.remind_at >= today_start ).order_by(Reminder.remind_at.asc()) reminders_result = await db.execute(reminders_query) active_reminders = reminders_result.scalars().all() # Project stats (scoped to user) total_projects_result = await db.execute( select(func.count(Project.id)).where(Project.user_id == current_user.id) ) total_projects = total_projects_result.scalar() projects_by_status_query = select( Project.status, func.count(Project.id).label("count") ).where(Project.user_id == current_user.id).group_by(Project.status) projects_by_status_result = await db.execute(projects_by_status_query) projects_by_status = {row[0]: row[1] for row in projects_by_status_result} # Total incomplete todos count (scoped to user) total_incomplete_result = await db.execute( select(func.count(Todo.id)).where( Todo.user_id == current_user.id, Todo.completed == False, ) ) total_incomplete_todos = total_incomplete_result.scalar() # Starred events (upcoming, ordered by date, scoped to user's calendars) now = datetime.now() starred_query = select(CalendarEvent).where( CalendarEvent.calendar_id.in_(user_calendar_ids), CalendarEvent.is_starred == True, CalendarEvent.start_datetime > now, _not_parent_template, ).order_by(CalendarEvent.start_datetime.asc()).limit(5) starred_result = await db.execute(starred_query) starred_events = starred_result.scalars().all() starred_events_data = [ { "id": e.id, "title": e.title, "start_datetime": e.start_datetime } for e in starred_events ] return { "todays_events": [ { "id": event.id, "title": event.title, "start_datetime": event.start_datetime, "end_datetime": event.end_datetime, "all_day": event.all_day, "color": event.color, "is_starred": event.is_starred } for event in todays_events ], "upcoming_todos": [ { "id": todo.id, "title": todo.title, "due_date": todo.due_date, "priority": todo.priority, "category": todo.category } for todo in upcoming_todos ], "active_reminders": [ { "id": reminder.id, "title": reminder.title, "remind_at": reminder.remind_at } for reminder in active_reminders ], "project_stats": { "total": total_projects, "by_status": projects_by_status }, "total_incomplete_todos": total_incomplete_todos, "starred_events": starred_events_data } @router.get("/upcoming") async def get_upcoming( days: int = Query(default=7, ge=1, le=90), client_date: Optional[date] = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), current_settings: Settings = Depends(get_current_settings), ): """Get unified list of upcoming items (todos, events, reminders) sorted by date.""" today = client_date or date.today() cutoff_date = today + timedelta(days=days) cutoff_datetime = datetime.combine(cutoff_date, datetime.max.time()) today_start = datetime.combine(today, datetime.min.time()) # Subquery: calendar IDs belonging to this user user_calendar_ids = select(Calendar.id).where(Calendar.user_id == current_user.id) # Get upcoming todos with due dates (today onward only, scoped to user) todos_query = select(Todo).where( Todo.user_id == current_user.id, Todo.completed == False, Todo.due_date.isnot(None), Todo.due_date >= today, Todo.due_date <= cutoff_date ) todos_result = await db.execute(todos_query) todos = todos_result.scalars().all() # Get upcoming events (from today onward, exclude parent templates, scoped to user's calendars) events_query = select(CalendarEvent).where( CalendarEvent.calendar_id.in_(user_calendar_ids), CalendarEvent.start_datetime >= today_start, CalendarEvent.start_datetime <= cutoff_datetime, _not_parent_template, ) events_result = await db.execute(events_query) events = events_result.scalars().all() # Get upcoming reminders (today onward only, scoped to user) reminders_query = select(Reminder).where( Reminder.user_id == current_user.id, Reminder.is_active == True, Reminder.is_dismissed == False, Reminder.remind_at >= today_start, Reminder.remind_at <= cutoff_datetime ) reminders_result = await db.execute(reminders_query) reminders = reminders_result.scalars().all() # Combine into unified list upcoming_items: List[Dict[str, Any]] = [] for todo in todos: upcoming_items.append({ "type": "todo", "id": todo.id, "title": todo.title, "date": todo.due_date.isoformat() if todo.due_date else None, "datetime": None, "priority": todo.priority, "category": todo.category }) for event in events: upcoming_items.append({ "type": "event", "id": event.id, "title": event.title, "date": event.start_datetime.date().isoformat(), "datetime": event.start_datetime.isoformat(), "all_day": event.all_day, "color": event.color, "is_starred": event.is_starred }) for reminder in reminders: upcoming_items.append({ "type": "reminder", "id": reminder.id, "title": reminder.title, "date": reminder.remind_at.date().isoformat(), "datetime": reminder.remind_at.isoformat() }) # Sort by date/datetime upcoming_items.sort(key=lambda x: x["datetime"] if x["datetime"] else x["date"]) return { "items": upcoming_items, "days": days, "cutoff_date": cutoff_date.isoformat() }