UMBRA/backend/app/routers/dashboard.py
Kyle Pope c2c4446ea6 Fix upcoming widget showing past items (yesterday's events, overdue todos)
- Add lower-bound date filter (>= today) for todos and reminders in /upcoming endpoint
- Accept client_date param for timezone-correct filtering
- Pass client_date from frontend to /upcoming API call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 11:54:52 +08:00

214 lines
7.1 KiB
Python

from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
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.reminder import Reminder
from app.models.project import Project
from app.routers.auth import get_current_session
router = APIRouter()
@router.get("/dashboard")
async def get_dashboard(
client_date: Optional[date] = Query(None),
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Get aggregated dashboard data."""
today = client_date or date.today()
upcoming_cutoff = today + timedelta(days=current_user.upcoming_days)
# Today's events
today_start = datetime.combine(today, datetime.min.time())
today_end = datetime.combine(today, datetime.max.time())
events_query = select(CalendarEvent).where(
CalendarEvent.start_datetime >= today_start,
CalendarEvent.start_datetime <= today_end
)
events_result = await db.execute(events_query)
todays_events = events_result.scalars().all()
# Upcoming todos (not completed, with due date within upcoming_days)
todos_query = select(Todo).where(
Todo.completed == False,
Todo.due_date.isnot(None),
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)
reminders_query = select(Reminder).where(
Reminder.is_active == True,
Reminder.is_dismissed == False
).order_by(Reminder.remind_at.asc())
reminders_result = await db.execute(reminders_query)
active_reminders = reminders_result.scalars().all()
# Project stats
total_projects_result = await db.execute(select(func.count(Project.id)))
total_projects = total_projects_result.scalar()
projects_by_status_query = select(
Project.status,
func.count(Project.id).label("count")
).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
total_incomplete_result = await db.execute(
select(func.count(Todo.id)).where(Todo.completed == False)
)
total_incomplete_todos = total_incomplete_result.scalar()
# Starred events (upcoming, ordered by date)
now = datetime.now()
starred_query = select(CalendarEvent).where(
CalendarEvent.is_starred == True,
CalendarEvent.start_datetime > now
).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: Settings = Depends(get_current_session)
):
"""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())
# Get upcoming todos with due dates (today onward only)
todos_query = select(Todo).where(
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)
events_query = select(CalendarEvent).where(
CalendarEvent.start_datetime >= today_start,
CalendarEvent.start_datetime <= cutoff_datetime,
)
events_result = await db.execute(events_query)
events = events_result.scalars().all()
# Get upcoming reminders (today onward only)
reminders_query = select(Reminder).where(
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()
}