- Add generic move_in_order() to BaseRepository for reorder support - Add reusable reorder_arrows.html partial with grip dot handles - Add reorder routes to all 9 list routers (tasks, notes, links, contacts, meetings, decisions, appointments, lists, focus) - Compact row padding (6px 12px, gap 8px) on .list-row and .focus-item - Reduce font size to 0.80rem on row titles, sidebar nav, domain tree Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
161 lines
5.9 KiB
Python
161 lines
5.9 KiB
Python
"""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,
|
|
domain_id: Optional[str] = None,
|
|
area_id: Optional[str] = None,
|
|
project_id: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
sidebar = await get_sidebar_data(db)
|
|
target_date = date.fromisoformat(focus_date) if focus_date else 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)
|
|
avail_where = [
|
|
"t.is_deleted = false",
|
|
"t.status NOT IN ('done', 'cancelled')",
|
|
"t.id NOT IN (SELECT task_id FROM daily_focus WHERE focus_date = :target_date AND is_deleted = false)",
|
|
]
|
|
avail_params = {"target_date": target_date}
|
|
|
|
if domain_id:
|
|
avail_where.append("t.domain_id = :domain_id")
|
|
avail_params["domain_id"] = domain_id
|
|
if area_id:
|
|
avail_where.append("t.area_id = :area_id")
|
|
avail_params["area_id"] = area_id
|
|
if project_id:
|
|
avail_where.append("t.project_id = :project_id")
|
|
avail_params["project_id"] = project_id
|
|
|
|
avail_sql = " AND ".join(avail_where)
|
|
|
|
result = await db.execute(text(f"""
|
|
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 {avail_sql}
|
|
ORDER BY t.priority ASC, t.due_date ASC NULLS LAST
|
|
LIMIT 50
|
|
"""), avail_params)
|
|
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)
|
|
|
|
# Filter options
|
|
domains_repo = BaseRepository("domains", db)
|
|
domains = await domains_repo.list()
|
|
areas_repo = BaseRepository("areas", db)
|
|
areas = await areas_repo.list()
|
|
projects_repo = BaseRepository("projects", db)
|
|
projects = await projects_repo.list()
|
|
|
|
return templates.TemplateResponse("focus.html", {
|
|
"request": request, "sidebar": sidebar,
|
|
"items": items, "available_tasks": available_tasks,
|
|
"focus_date": target_date,
|
|
"total_estimated": total_est,
|
|
"domains": domains, "areas": areas, "projects": projects,
|
|
"current_domain_id": domain_id or "",
|
|
"current_area_id": area_id or "",
|
|
"current_project_id": project_id or "",
|
|
"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)
|
|
parsed_date = date.fromisoformat(focus_date)
|
|
# 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": parsed_date})
|
|
next_order = result.scalar()
|
|
|
|
await repo.create({
|
|
"task_id": task_id, "focus_date": parsed_date,
|
|
"sort_order": next_order, "completed": False,
|
|
})
|
|
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
|
|
|
|
|
@router.post("/reorder")
|
|
async def reorder_focus(
|
|
request: Request,
|
|
item_id: str = Form(...),
|
|
direction: str = Form(...),
|
|
focus_date: str = Form(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("daily_focus", db)
|
|
parsed_date = date.fromisoformat(focus_date)
|
|
await repo.move_in_order(item_id, direction, filters={"focus_date": parsed_date})
|
|
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)
|