feat: daily focus search, show all tasks, and list item support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
165
routers/focus.py
165
routers/focus.py
@@ -1,4 +1,4 @@
|
||||
"""Daily Focus: date-scoped task commitment list."""
|
||||
"""Daily Focus: date-scoped task/list-item commitment list."""
|
||||
|
||||
from fastapi import APIRouter, Request, Form, Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
@@ -23,56 +23,108 @@ async def focus_view(
|
||||
domain_id: Optional[str] = None,
|
||||
area_id: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
source_type: 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()
|
||||
if not source_type:
|
||||
source_type = "tasks"
|
||||
|
||||
# --- Focus items (both tasks and list items) ---
|
||||
result = await db.execute(text("""
|
||||
SELECT df.*, t.title, t.priority, t.status as task_status,
|
||||
SELECT df.*,
|
||||
t.title as 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
|
||||
d.name as domain_name, d.color as domain_color,
|
||||
li.content as list_item_content, li.list_id as list_item_list_id,
|
||||
li.completed as list_item_completed,
|
||||
l.name as list_name
|
||||
FROM daily_focus df
|
||||
JOIN tasks t ON df.task_id = t.id
|
||||
LEFT 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
|
||||
LEFT JOIN list_items li ON df.list_item_id = li.id
|
||||
LEFT JOIN lists l ON li.list_id = l.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}
|
||||
# --- Available tasks ---
|
||||
available_tasks = []
|
||||
if source_type == "tasks":
|
||||
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 AND task_id IS NOT NULL)",
|
||||
]
|
||||
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
|
||||
if search:
|
||||
avail_where.append("t.title ILIKE :search")
|
||||
avail_params["search"] = f"%{search}%"
|
||||
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)
|
||||
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 200
|
||||
"""), avail_params)
|
||||
available_tasks = [dict(r._mapping) for r in result]
|
||||
|
||||
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]
|
||||
# --- Available list items ---
|
||||
available_list_items = []
|
||||
if source_type == "list_items":
|
||||
li_where = [
|
||||
"li.is_deleted = false",
|
||||
"li.completed = false",
|
||||
"l.is_deleted = false",
|
||||
"li.id NOT IN (SELECT list_item_id FROM daily_focus WHERE focus_date = :target_date AND is_deleted = false AND list_item_id IS NOT NULL)",
|
||||
]
|
||||
li_params = {"target_date": target_date}
|
||||
|
||||
if search:
|
||||
li_where.append("li.content ILIKE :search")
|
||||
li_params["search"] = f"%{search}%"
|
||||
if domain_id:
|
||||
li_where.append("l.domain_id = :domain_id")
|
||||
li_params["domain_id"] = domain_id
|
||||
if area_id:
|
||||
li_where.append("l.area_id = :area_id")
|
||||
li_params["area_id"] = area_id
|
||||
if project_id:
|
||||
li_where.append("l.project_id = :project_id")
|
||||
li_params["project_id"] = project_id
|
||||
|
||||
li_sql = " AND ".join(li_where)
|
||||
result = await db.execute(text(f"""
|
||||
SELECT li.id, li.content, li.list_id, l.name as list_name,
|
||||
d.name as domain_name
|
||||
FROM list_items li
|
||||
JOIN lists l ON li.list_id = l.id
|
||||
LEFT JOIN domains d ON l.domain_id = d.id
|
||||
WHERE {li_sql}
|
||||
ORDER BY l.name ASC, li.sort_order ASC
|
||||
LIMIT 200
|
||||
"""), li_params)
|
||||
available_list_items = [dict(r._mapping) for r in result]
|
||||
|
||||
# Estimated total minutes
|
||||
total_est = sum(i.get("estimated_minutes") or 0 for i in items)
|
||||
@@ -87,13 +139,17 @@ async def focus_view(
|
||||
|
||||
return templates.TemplateResponse("focus.html", {
|
||||
"request": request, "sidebar": sidebar,
|
||||
"items": items, "available_tasks": available_tasks,
|
||||
"items": items,
|
||||
"available_tasks": available_tasks,
|
||||
"available_list_items": available_list_items,
|
||||
"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 "",
|
||||
"current_search": search or "",
|
||||
"current_source_type": source_type,
|
||||
"page_title": "Daily Focus", "active_nav": "focus",
|
||||
})
|
||||
|
||||
@@ -101,8 +157,9 @@ async def focus_view(
|
||||
@router.post("/add")
|
||||
async def add_to_focus(
|
||||
request: Request,
|
||||
task_id: str = Form(...),
|
||||
focus_date: str = Form(...),
|
||||
task_id: Optional[str] = Form(None),
|
||||
list_item_id: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("daily_focus", db)
|
||||
@@ -114,10 +171,17 @@ async def add_to_focus(
|
||||
"""), {"fd": parsed_date})
|
||||
next_order = result.scalar()
|
||||
|
||||
await repo.create({
|
||||
"task_id": task_id, "focus_date": parsed_date,
|
||||
"sort_order": next_order, "completed": False,
|
||||
})
|
||||
data = {
|
||||
"focus_date": parsed_date,
|
||||
"sort_order": next_order,
|
||||
"completed": False,
|
||||
}
|
||||
if task_id:
|
||||
data["task_id"] = task_id
|
||||
if list_item_id:
|
||||
data["list_item_id"] = list_item_id
|
||||
|
||||
await repo.create(data)
|
||||
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
||||
|
||||
|
||||
@@ -141,12 +205,25 @@ async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depen
|
||||
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})
|
||||
if item["task_id"]:
|
||||
# Sync 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})
|
||||
elif item["list_item_id"]:
|
||||
# Sync list item completed status
|
||||
now = datetime.now(timezone.utc)
|
||||
if not item["completed"]:
|
||||
await db.execute(text(
|
||||
"UPDATE list_items SET completed = true, completed_at = :now, updated_at = :now WHERE id = :id"
|
||||
), {"id": item["list_item_id"], "now": now})
|
||||
else:
|
||||
await db.execute(text(
|
||||
"UPDATE list_items SET completed = false, completed_at = NULL, updated_at = :now WHERE id = :id"
|
||||
), {"id": item["list_item_id"], "now": now})
|
||||
await db.commit()
|
||||
focus_date = item["focus_date"] if item else date.today()
|
||||
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user