From 3c0266bc19e1de2db775d51a8875c7281c1b0f9b Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 3 Mar 2026 14:53:10 +0000 Subject: [PATCH] feat: daily focus search, show all tasks, and list item support Co-Authored-By: Claude Opus 4.6 --- core/base_repository.py | 2 +- routers/focus.py | 165 +++++++++++++++++++++++++++++----------- templates/focus.html | 151 ++++++++++++++++++++++++++---------- 3 files changed, 234 insertions(+), 84 deletions(-) diff --git a/core/base_repository.py b/core/base_repository.py index 6c6b3de..a5e4c05 100644 --- a/core/base_repository.py +++ b/core/base_repository.py @@ -161,7 +161,7 @@ class BaseRepository: "category", "instructions", "expected_output", "estimated_days", "contact_id", "started_at", "weekly_hours", "effective_from", - "task_id", "meeting_id", + "task_id", "meeting_id", "list_item_id", } clean_data = {} for k, v in data.items(): diff --git a/routers/focus.py b/routers/focus.py index 00aa374..3d5f0e4 100644 --- a/routers/focus.py +++ b/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) diff --git a/templates/focus.html b/templates/focus.html index 926ac77..587ea27 100644 --- a/templates/focus.html +++ b/templates/focus.html @@ -26,11 +26,25 @@ - - {{ item.title }} - {% if item.project_name %}{{ item.project_name }}{% endif %} - {% if item.estimated_minutes %}~{{ item.estimated_minutes }}min{% endif %} - {% if item.due_date %}{{ item.due_date }}{% endif %} + {% if item.task_id %} + + {% if item.title %} + {{ item.title }} + {% else %} + [Deleted] + {% endif %} + {% if item.project_name %}{{ item.project_name }}{% endif %} + {% if item.estimated_minutes %}~{{ item.estimated_minutes }}min{% endif %} + {% if item.due_date %}{{ item.due_date }}{% endif %} + {% elif item.list_item_id %} + + {% if item.list_item_content %} + {{ item.list_item_content }} + {% else %} + [Deleted] + {% endif %} + {% if item.list_name %}{{ item.list_name }}{% endif %} + {% endif %}
@@ -41,47 +55,94 @@
No focus items for this day
{% endif %} - +

Add to Focus

+ + + +
+ + + {% if current_source_type == 'tasks' %} + {% endif %} + + {% if current_search or current_domain_id or current_area_id or current_project_id %} + Clear + {% endif %}
- {% for t in available_tasks[:15] %} -
- - {{ t.title }} - {% if t.project_name %}{{ t.project_name }}{% endif %} - {% if t.due_date %}{{ t.due_date }}{% endif %} -
- - - -
-
- {% else %} -
No available tasks matching filters
- {% endfor %} + + {% if current_source_type == 'tasks' %} + {% for t in available_tasks %} +
+ + {{ t.title }} + {% if t.project_name %}{{ t.project_name }}{% endif %} + {% if t.due_date %}{{ t.due_date }}{% endif %} +
+ + + +
+
+ {% else %} +
No available tasks matching filters
+ {% endfor %} + {% if available_tasks|length > 25 %} +
+ +
+ {% endif %} + + + {% elif current_source_type == 'list_items' %} + {% for li in available_list_items %} +
+ + {{ li.content }} + {% if li.list_name %}{{ li.list_name }}{% endif %} +
+ + + +
+
+ {% else %} +
No available list items matching filters
+ {% endfor %} + {% if available_list_items|length > 25 %} +
+ +
+ {% endif %} + {% endif %}
{% endblock %}