"""Daily Focus: date-scoped task commitment list.""" from fastapi import APIRouter, Request, Form, Depends from fastapi.templating import Jinja2Templates from fastapi.responses import RedirectResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import text from typing import Optional from datetime import date, datetime, timezone from core.database import get_db from core.base_repository import BaseRepository from core.sidebar import get_sidebar_data router = APIRouter(prefix="/focus", tags=["focus"]) templates = Jinja2Templates(directory="templates") @router.get("/") async def focus_view(request: Request, focus_date: Optional[str] = None, db: AsyncSession = Depends(get_db)): sidebar = await get_sidebar_data(db) target_date = focus_date or str(date.today()) result = await db.execute(text(""" SELECT df.*, t.title, t.priority, t.status as task_status, t.project_id, t.due_date, t.estimated_minutes, p.name as project_name, d.name as domain_name, d.color as domain_color FROM daily_focus df JOIN tasks t ON df.task_id = t.id LEFT JOIN projects p ON t.project_id = p.id LEFT JOIN domains d ON t.domain_id = d.id WHERE df.focus_date = :target_date AND df.is_deleted = false ORDER BY df.sort_order, df.created_at """), {"target_date": target_date}) items = [dict(r._mapping) for r in result] # Available tasks to add (open, not already in today's focus) result = await db.execute(text(""" SELECT t.id, t.title, t.priority, t.due_date, p.name as project_name, d.name as domain_name FROM tasks t LEFT JOIN projects p ON t.project_id = p.id LEFT JOIN domains d ON t.domain_id = d.id WHERE t.is_deleted = false AND t.status NOT IN ('done', 'cancelled') AND t.id NOT IN ( SELECT task_id FROM daily_focus WHERE focus_date = :target_date AND is_deleted = false ) ORDER BY t.priority ASC, t.due_date ASC NULLS LAST LIMIT 50 """), {"target_date": target_date}) available_tasks = [dict(r._mapping) for r in result] # Estimated total minutes total_est = sum(i.get("estimated_minutes") or 0 for i in items) return templates.TemplateResponse("focus.html", { "request": request, "sidebar": sidebar, "items": items, "available_tasks": available_tasks, "focus_date": target_date, "total_estimated": total_est, "page_title": "Daily Focus", "active_nav": "focus", }) @router.post("/add") async def add_to_focus( request: Request, task_id: str = Form(...), focus_date: str = Form(...), db: AsyncSession = Depends(get_db), ): repo = BaseRepository("daily_focus", db) # Get next sort order result = await db.execute(text(""" SELECT COALESCE(MAX(sort_order), 0) + 10 FROM daily_focus WHERE focus_date = :fd AND is_deleted = false """), {"fd": focus_date}) next_order = result.scalar() await repo.create({ "task_id": task_id, "focus_date": focus_date, "sort_order": next_order, "completed": False, }) return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303) @router.post("/{focus_id}/toggle") async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("daily_focus", db) item = await repo.get(focus_id) if item: await repo.update(focus_id, {"completed": not item["completed"]}) # Also toggle the task status task_repo = BaseRepository("tasks", db) if not item["completed"]: await task_repo.update(item["task_id"], {"status": "done", "completed_at": datetime.now(timezone.utc)}) else: await task_repo.update(item["task_id"], {"status": "open", "completed_at": None}) focus_date = item["focus_date"] if item else date.today() return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303) @router.post("/{focus_id}/remove") async def remove_from_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("daily_focus", db) item = await repo.get(focus_id) await repo.soft_delete(focus_id) focus_date = item["focus_date"] if item else date.today() return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)