feat: focus item detail page with inline note + checklist
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
157
routers/focus.py
157
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")
|
||||
|
||||
Reference in New Issue
Block a user