diff --git a/core/base_repository.py b/core/base_repository.py index 79e5ecb..eaf77f9 100644 --- a/core/base_repository.py +++ b/core/base_repository.py @@ -153,7 +153,7 @@ class BaseRepository: nullable_fields = { "description", "notes", "body", "area_id", "project_id", "parent_id", "parent_item_id", "release_id", "due_date", "deadline", "tags", - "context", "folder_id", "meeting_id", "completed_at", + "context", "folder_id", "meeting_id", "focus_id", "completed_at", "waiting_for_contact_id", "waiting_since", "color", "rationale", "decided_at", "superseded_by_id", "start_at", "end_at", "location", "agenda", "transcript", "notes_body", diff --git a/routers/focus.py b/routers/focus.py index 6c7b3bd..f651e4f 100644 --- a/routers/focus.py +++ b/routers/focus.py @@ -263,6 +263,158 @@ async def reorder_all_focus( return RedirectResponse(url="/focus", status_code=303) +async def _ensure_focus_note_and_list(focus_id: str, item: dict, db: AsyncSession): + """Find or create the single note + list attached to a focus item.""" + note_repo = BaseRepository("notes", db) + list_repo = BaseRepository("lists", db) + + # Find existing note + result = await db.execute(text( + "SELECT id FROM notes WHERE focus_id = :fid AND is_deleted = false LIMIT 1" + ), {"fid": focus_id}) + note_id = result.scalar() + if not note_id: + note = await note_repo.create({ + "title": f'Notes: {item["title"]}', + "focus_id": focus_id, + "domain_id": item.get("domain_id"), + "project_id": item.get("project_id"), + "body": "", + "content_format": "rich", + }) + note_id = note["id"] + + # Find existing list + result = await db.execute(text( + "SELECT id FROM lists WHERE focus_id = :fid AND is_deleted = false LIMIT 1" + ), {"fid": focus_id}) + list_id = result.scalar() + if not list_id: + lst = await list_repo.create({ + "name": f'List: {item["title"]}', + "focus_id": focus_id, + "domain_id": item.get("domain_id"), + "project_id": item.get("project_id"), + "list_type": "checklist", + }) + list_id = lst["id"] + + return str(note_id), str(list_id) + + +@router.get("/{focus_id}") +async def focus_detail( + focus_id: str, request: Request, + db: AsyncSession = Depends(get_db), +): + repo = BaseRepository("daily_focus", db) + item = await repo.get(focus_id) + if not item or item.get("task_id") or item.get("list_item_id"): + return RedirectResponse(url="/focus", status_code=303) + + sidebar = await get_sidebar_data(db) + + # Domain / project info + domain = None + if item.get("domain_id"): + result = await db.execute(text("SELECT name, color FROM domains WHERE id = :id"), {"id": str(item["domain_id"])}) + row = result.first() + domain = dict(row._mapping) if row else None + + project = None + if item.get("project_id"): + result = await db.execute(text("SELECT id, name FROM projects WHERE id = :id"), {"id": str(item["project_id"])}) + row = result.first() + project = dict(row._mapping) if row else None + + # Lists for convert-to-list-item + all_lists = await BaseRepository("lists", db).list() + + # Ensure note + list exist + note_id, list_id = await _ensure_focus_note_and_list(focus_id, item, db) + + # Load note + note = await BaseRepository("notes", db).get(note_id) + + # Load list items + result = await db.execute(text(""" + SELECT * FROM list_items + WHERE list_id = :lid AND is_deleted = false + ORDER BY sort_order, created_at + """), {"lid": list_id}) + list_items = [dict(r._mapping) for r in result] + + return templates.TemplateResponse("focus_detail.html", { + "request": request, "sidebar": sidebar, "item": item, + "domain": domain, "project": project, "all_lists": all_lists, + "note": note, "list_id": list_id, "list_items": list_items, + "page_title": item.get("title", "Focus Item"), "active_nav": "focus", + }) + + +@router.post("/{focus_id}/save-note") +async def save_focus_note( + focus_id: str, request: Request, + note_id: str = Form(...), + body: Optional[str] = Form(None), + db: AsyncSession = Depends(get_db), +): + note_repo = BaseRepository("notes", db) + await note_repo.update(note_id, {"body": body or ""}) + return RedirectResponse(url="/focus", status_code=303) + + +@router.post("/{focus_id}/list-item/add") +async def add_focus_list_item( + focus_id: str, request: Request, + list_id: str = Form(...), + content: str = Form(...), + db: AsyncSession = Depends(get_db), +): + li_repo = BaseRepository("list_items", db) + await li_repo.create({"list_id": list_id, "content": content, "completed": False}) + return RedirectResponse(url=f"/focus/{focus_id}", status_code=303) + + +@router.post("/{focus_id}/list-item/{item_id}/toggle") +async def toggle_focus_list_item( + focus_id: str, item_id: str, request: Request, + db: AsyncSession = Depends(get_db), +): + li_repo = BaseRepository("list_items", db) + item = await li_repo.get(item_id) + if item: + now = datetime.now(timezone.utc) + if item["completed"]: + await li_repo.update(item_id, {"completed": False, "completed_at": None}) + else: + await li_repo.update(item_id, {"completed": True, "completed_at": now}) + return RedirectResponse(url=f"/focus/{focus_id}", status_code=303) + + +@router.post("/{focus_id}/list-item/{item_id}/delete") +async def delete_focus_list_item( + focus_id: str, item_id: str, request: Request, + db: AsyncSession = Depends(get_db), +): + li_repo = BaseRepository("list_items", db) + await li_repo.soft_delete(item_id) + return RedirectResponse(url=f"/focus/{focus_id}", status_code=303) + + +@router.post("/{focus_id}/list-item/reorder-all") +async def reorder_focus_list_items( + focus_id: str, request: Request, + item_ids: str = Form(...), + db: AsyncSession = Depends(get_db), +): + repo = BaseRepository("list_items", db) + ids = [i.strip() for i in item_ids.split(",") if i.strip()] + if ids: + await repo.reorder(ids) + return RedirectResponse(url=f"/focus/{focus_id}", status_code=303) + + @router.get("/{focus_id}/edit") async def edit_focus_item(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("daily_focus", db) @@ -294,7 +446,7 @@ async def update_focus_item( "domain_id": domain_id or None, "project_id": project_id or None, }) - return RedirectResponse(url="/focus", status_code=303) + return RedirectResponse(url=f"/focus/{focus_id}", status_code=303) @router.post("/{focus_id}/convert-to-task") @@ -436,7 +588,8 @@ async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depen "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() - return RedirectResponse(url="/focus", status_code=303) + referer = request.headers.get("referer", "/focus") + return RedirectResponse(url=referer, status_code=303) @router.post("/{focus_id}/remove") diff --git a/routers/lists.py b/routers/lists.py index 8be02a0..aa75349 100644 --- a/routers/lists.py +++ b/routers/lists.py @@ -74,6 +74,7 @@ async def create_form( project_id: Optional[str] = None, task_id: Optional[str] = None, meeting_id: Optional[str] = None, + focus_id: Optional[str] = None, db: AsyncSession = Depends(get_db), ): sidebar = await get_sidebar_data(db) @@ -93,6 +94,7 @@ async def create_form( "prefill_project_id": project_id or "", "prefill_task_id": task_id or "", "prefill_meeting_id": meeting_id or "", + "prefill_focus_id": focus_id or "", }) @@ -100,11 +102,12 @@ async def create_form( async def create_list( request: Request, name: str = Form(...), - domain_id: str = Form(...), + domain_id: Optional[str] = Form(None), area_id: Optional[str] = Form(None), project_id: Optional[str] = Form(None), task_id: Optional[str] = Form(None), meeting_id: Optional[str] = Form(None), + focus_id: Optional[str] = Form(None), list_type: str = Form("checklist"), description: Optional[str] = Form(None), tags: Optional[str] = Form(None), @@ -112,10 +115,12 @@ async def create_list( ): repo = BaseRepository("lists", db) data = { - "name": name, "domain_id": domain_id, + "name": name, "list_type": list_type, "description": description, } + if domain_id and domain_id.strip(): + data["domain_id"] = domain_id if area_id and area_id.strip(): data["area_id"] = area_id if project_id and project_id.strip(): @@ -124,6 +129,8 @@ async def create_list( data["task_id"] = task_id if meeting_id and meeting_id.strip(): data["meeting_id"] = meeting_id + if focus_id and focus_id.strip(): + data["focus_id"] = focus_id if tags and tags.strip(): data["tags"] = [t.strip() for t in tags.split(",") if t.strip()] @@ -132,6 +139,8 @@ async def create_list( return RedirectResponse(url=f"/tasks/{task_id}?tab=lists", status_code=303) if meeting_id and meeting_id.strip(): return RedirectResponse(url=f"/meetings/{meeting_id}?tab=lists", status_code=303) + if focus_id and focus_id.strip(): + return RedirectResponse(url=f"/focus/{focus_id}?tab=lists", status_code=303) return RedirectResponse(url=f"/lists/{new_list['id']}", status_code=303) diff --git a/routers/notes.py b/routers/notes.py index e7fb4e8..c2a7c1d 100644 --- a/routers/notes.py +++ b/routers/notes.py @@ -63,6 +63,7 @@ async def create_form( project_id: Optional[str] = None, task_id: Optional[str] = None, meeting_id: Optional[str] = None, + focus_id: Optional[str] = None, db: AsyncSession = Depends(get_db), ): sidebar = await get_sidebar_data(db) @@ -79,6 +80,7 @@ async def create_form( "prefill_project_id": project_id or "", "prefill_task_id": task_id or "", "prefill_meeting_id": meeting_id or "", + "prefill_focus_id": focus_id or "", }) @@ -86,10 +88,11 @@ async def create_form( async def create_note( request: Request, title: str = Form(...), - domain_id: str = Form(...), + domain_id: Optional[str] = Form(None), project_id: Optional[str] = Form(None), task_id: Optional[str] = Form(None), meeting_id: Optional[str] = Form(None), + focus_id: Optional[str] = Form(None), body: Optional[str] = Form(None), content_format: str = Form("rich"), tags: Optional[str] = Form(None), @@ -97,15 +100,19 @@ async def create_note( ): repo = BaseRepository("notes", db) data = { - "title": title, "domain_id": domain_id, + "title": title, "body": body, "content_format": content_format, } + if domain_id and domain_id.strip(): + data["domain_id"] = domain_id if project_id and project_id.strip(): data["project_id"] = project_id if task_id and task_id.strip(): data["task_id"] = task_id if meeting_id and meeting_id.strip(): data["meeting_id"] = meeting_id + if focus_id and focus_id.strip(): + data["focus_id"] = focus_id if tags and tags.strip(): data["tags"] = [t.strip() for t in tags.split(",") if t.strip()] note = await repo.create(data) @@ -113,6 +120,8 @@ async def create_note( return RedirectResponse(url=f"/tasks/{task_id}?tab=notes", status_code=303) if meeting_id and meeting_id.strip(): return RedirectResponse(url=f"/meetings/{meeting_id}?tab=notes", status_code=303) + if focus_id and focus_id.strip(): + return RedirectResponse(url=f"/focus/{focus_id}?tab=notes", status_code=303) return RedirectResponse(url=f"/notes/{note['id']}", status_code=303) diff --git a/templates/focus.html b/templates/focus.html index b69db59..be4f921 100644 --- a/templates/focus.html +++ b/templates/focus.html @@ -52,7 +52,7 @@ {{ item.due_date or '' }} {% if item.task_id %}{% elif item.list_item_id %}{% else %}{% endif %} - {% if item.task_id %}{% if item.task_title %}{{ item.task_title }}{% else %}[Deleted]{% endif %}{% elif item.list_item_id %}{% if item.list_item_content %}{{ item.list_item_content }}{% else %}[Deleted]{% endif %}{% else %}{{ item.title }}{% endif %} + {% if item.task_id %}{% if item.task_title %}{{ item.task_title }}{% else %}[Deleted]{% endif %}{% elif item.list_item_id %}{% if item.list_item_content %}{{ item.list_item_content }}{% else %}[Deleted]{% endif %}{% else %}{{ item.title }}{% endif %} {% if item.area_name %}{{ item.area_name }}{% endif %} {% if item.project_name %}{{ item.project_name }}{% elif item.list_item_id and item.list_name %}{{ item.list_name }}{% endif %} {{ '~%smin'|format(item.estimated_minutes) if item.estimated_minutes else '' }} diff --git a/templates/focus_detail.html b/templates/focus_detail.html new file mode 100644 index 0000000..8332b11 --- /dev/null +++ b/templates/focus_detail.html @@ -0,0 +1,173 @@ +{% extends "base.html" %} +{% block content %} + + +
+
+

{{ item.title }}

+
+ Edit +
+ +
+
+ +
+
+
+
+ {% if item.completed %}completed{% endif %} + {% if domain %}{{ domain.name }}{% endif %} + {% if project %}{{ project.name }}{% endif %} +
+
+ + +
+ Convert to: +
+ +
+
+ +
+
+ +
+
+ + +
+
+ + +
+ + +
+
+ Notes +
+
+ + +
+ + Back +
+
+
+ + +
+
+ Checklist + {{ list_items|length }} item{{ 's' if list_items|length != 1 }} +
+ + +
+ + + +
+ + +
+ {% for li in list_items %} +
+ +
+
+ + +
+
+ {{ li.content }} +
+ +
+
+ {% else %} +
No items yet
+ {% endfor %} +
+ +
+ +
+ + +{% endblock %} diff --git a/templates/focus_edit.html b/templates/focus_edit.html index 4c38d5a..f61ec00 100644 --- a/templates/focus_edit.html +++ b/templates/focus_edit.html @@ -3,7 +3,9 @@ diff --git a/templates/list_form.html b/templates/list_form.html index 4eb4ccd..595767e 100644 --- a/templates/list_form.html +++ b/templates/list_form.html @@ -14,9 +14,9 @@
- - + {% for d in domains %}
diff --git a/templates/note_form.html b/templates/note_form.html index 750896d..027c62d 100644 --- a/templates/note_form.html +++ b/templates/note_form.html @@ -6,8 +6,8 @@
-
-
+
+
@@ -15,7 +15,8 @@ {% if prefill_task_id is defined and prefill_task_id %}{% endif %} {% if prefill_meeting_id is defined and prefill_meeting_id %}{% endif %} -
Cancel
+ {% if prefill_focus_id is defined and prefill_focus_id %}{% endif %} +
Cancel
{% endblock %}