Adds a clickable flag marker (⚑) on each focus row to mark items as critical. Red when active, subtle when inactive. Toggle via POST. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
609 lines
22 KiB
Python
609 lines
22 KiB
Python
"""Focus: persistent commitment list — items stay until removed."""
|
|
|
|
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
|
|
from core.template_filters import autolink
|
|
|
|
router = APIRouter(prefix="/focus", tags=["focus"])
|
|
templates = Jinja2Templates(directory="templates")
|
|
templates.env.filters["autolink"] = autolink
|
|
|
|
|
|
@router.get("/")
|
|
async def focus_view(
|
|
request: Request,
|
|
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)
|
|
if not source_type:
|
|
source_type = "tasks"
|
|
|
|
# --- All active focus items ---
|
|
result = await db.execute(text("""
|
|
SELECT df.*,
|
|
t.title as task_title, t.priority, t.status as task_status,
|
|
t.project_id as task_project_id, t.due_date, t.estimated_minutes,
|
|
COALESCE(p.name, lp.name, sp.name) as project_name,
|
|
COALESCE(t.project_id, l.project_id, df.project_id) as effective_project_id,
|
|
COALESCE(d.name, ld.name, sd.name) as domain_name,
|
|
COALESCE(d.color, ld.color, sd.color) as domain_color,
|
|
COALESCE(d.id, ld.id, df.domain_id) as effective_domain_id,
|
|
COALESCE(a.name, pa.name, la.name) as area_name,
|
|
COALESCE(a.id, pa.id, la.id) as effective_area_id,
|
|
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
|
|
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 areas a ON t.area_id = a.id
|
|
LEFT JOIN areas pa ON p.area_id = pa.id
|
|
LEFT JOIN list_items li ON df.list_item_id = li.id
|
|
LEFT JOIN lists l ON li.list_id = l.id
|
|
LEFT JOIN projects lp ON l.project_id = lp.id
|
|
LEFT JOIN domains ld ON l.domain_id = ld.id
|
|
LEFT JOIN areas la ON l.area_id = la.id
|
|
LEFT JOIN domains sd ON df.domain_id = sd.id
|
|
LEFT JOIN projects sp ON df.project_id = sp.id
|
|
WHERE df.is_deleted = false
|
|
AND (t.id IS NULL OR t.is_deleted = false)
|
|
AND (li.id IS NULL OR li.is_deleted = false)
|
|
ORDER BY df.sort_order, df.created_at
|
|
"""))
|
|
items = [dict(r._mapping) for r in result]
|
|
|
|
# Group items by domain only — area/project shown as inline columns
|
|
from collections import OrderedDict
|
|
domain_map = OrderedDict()
|
|
for item in items:
|
|
dk = item.get("effective_domain_id") or "__none__"
|
|
dl = item.get("domain_name") or "General"
|
|
dc = item.get("domain_color") or ""
|
|
if dk not in domain_map:
|
|
domain_map[dk] = {"key": dk, "label": dl, "color": dc, "rows": []}
|
|
domain_map[dk]["rows"].append(item)
|
|
|
|
# Sort: General first, then by domain sort_order
|
|
domain_order = {str(d["id"]): d.get("sort_order", 0) for d in await BaseRepository("domains", db).list()}
|
|
hierarchy = sorted(domain_map.values(), key=lambda g: (-1 if g["key"] == "__none__" else domain_order.get(str(g["key"]), 999)))
|
|
|
|
# --- 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 is_deleted = false AND task_id IS NOT NULL)",
|
|
]
|
|
avail_params = {}
|
|
|
|
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)
|
|
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]
|
|
|
|
# --- 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 is_deleted = false AND list_item_id IS NOT NULL)",
|
|
]
|
|
li_params = {}
|
|
|
|
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)
|
|
|
|
# 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, "hierarchy": hierarchy,
|
|
"available_tasks": available_tasks,
|
|
"available_list_items": available_list_items,
|
|
"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": "Focus", "active_nav": "focus",
|
|
})
|
|
|
|
|
|
@router.post("/add")
|
|
async def add_to_focus(
|
|
request: Request,
|
|
task_id: Optional[str] = Form(None),
|
|
list_item_id: Optional[str] = Form(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("daily_focus", db)
|
|
# Get next sort order
|
|
result = await db.execute(text("""
|
|
SELECT COALESCE(MAX(sort_order), 0) + 10 FROM daily_focus
|
|
WHERE is_deleted = false
|
|
"""))
|
|
next_order = result.scalar()
|
|
|
|
data = {
|
|
"focus_date": date.today(),
|
|
"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="/focus", status_code=303)
|
|
|
|
|
|
@router.post("/quick-add")
|
|
async def quick_add_focus(
|
|
request: Request,
|
|
title: str = Form(...),
|
|
quick_domain_id: Optional[str] = Form(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("daily_focus", db)
|
|
result = await db.execute(text("""
|
|
SELECT COALESCE(MAX(sort_order), 0) + 10 FROM daily_focus
|
|
WHERE is_deleted = false
|
|
"""))
|
|
next_order = result.scalar()
|
|
|
|
data = {
|
|
"focus_date": date.today(),
|
|
"sort_order": next_order,
|
|
"completed": False,
|
|
"title": title.strip(),
|
|
}
|
|
if quick_domain_id:
|
|
data["domain_id"] = quick_domain_id
|
|
|
|
await repo.create(data)
|
|
return RedirectResponse(url="/focus", status_code=303)
|
|
|
|
|
|
@router.post("/reorder")
|
|
async def reorder_focus(
|
|
request: Request,
|
|
item_id: str = Form(...),
|
|
direction: str = Form(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("daily_focus", db)
|
|
await repo.move_in_order(item_id, direction)
|
|
return RedirectResponse(url="/focus", status_code=303)
|
|
|
|
|
|
@router.post("/reorder-all")
|
|
async def reorder_all_focus(
|
|
request: Request,
|
|
item_ids: str = Form(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("daily_focus", db)
|
|
ids = [i.strip() for i in item_ids.split(",") if i.strip()]
|
|
if ids:
|
|
await repo.reorder(ids)
|
|
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)
|
|
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)
|
|
domains = await BaseRepository("domains", db).list()
|
|
projects = await BaseRepository("projects", db).list()
|
|
lists = await BaseRepository("lists", db).list()
|
|
return templates.TemplateResponse("focus_edit.html", {
|
|
"request": request, "sidebar": sidebar, "item": item,
|
|
"domains": domains, "projects": projects, "lists": lists,
|
|
"page_title": "Edit Focus Item", "active_nav": "focus",
|
|
})
|
|
|
|
|
|
@router.post("/{focus_id}/edit")
|
|
async def update_focus_item(
|
|
focus_id: str, request: Request,
|
|
title: str = Form(...),
|
|
domain_id: Optional[str] = Form(None),
|
|
project_id: Optional[str] = Form(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("daily_focus", db)
|
|
await repo.update(focus_id, {
|
|
"title": title.strip(),
|
|
"domain_id": domain_id or None,
|
|
"project_id": project_id or None,
|
|
})
|
|
return RedirectResponse(url=f"/focus/{focus_id}", status_code=303)
|
|
|
|
|
|
@router.post("/{focus_id}/convert-to-task")
|
|
async def convert_focus_to_task(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 not item.get("title"):
|
|
return RedirectResponse(url="/focus", status_code=303)
|
|
|
|
# Look up default domain if none set
|
|
item_domain_id = item.get("domain_id")
|
|
if not item_domain_id:
|
|
result = await db.execute(text(
|
|
"SELECT id FROM domains WHERE is_deleted = false ORDER BY sort_order LIMIT 1"
|
|
))
|
|
item_domain_id = str(result.scalar())
|
|
|
|
# Create the task
|
|
task_repo = BaseRepository("tasks", db)
|
|
task = await task_repo.create({
|
|
"title": item["title"],
|
|
"domain_id": item_domain_id,
|
|
"project_id": item.get("project_id"),
|
|
"status": "open",
|
|
"priority": 3,
|
|
})
|
|
|
|
# Update focus item to point to new task, clear standalone fields
|
|
await db.execute(text("""
|
|
UPDATE daily_focus
|
|
SET task_id = :task_id, title = NULL, domain_id = NULL, project_id = NULL,
|
|
updated_at = now()
|
|
WHERE id = :id
|
|
"""), {"task_id": task["id"], "id": focus_id})
|
|
await db.commit()
|
|
|
|
return RedirectResponse(url=f"/tasks/{task['id']}/edit", status_code=303)
|
|
|
|
|
|
@router.post("/{focus_id}/convert-to-note")
|
|
async def convert_focus_to_note(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 not item.get("title"):
|
|
return RedirectResponse(url="/focus", status_code=303)
|
|
|
|
note_repo = BaseRepository("notes", db)
|
|
note = await note_repo.create({
|
|
"title": item["title"],
|
|
"domain_id": item.get("domain_id"),
|
|
"project_id": item.get("project_id"),
|
|
"body": "",
|
|
"content_format": "rich",
|
|
})
|
|
|
|
await repo.soft_delete(focus_id)
|
|
return RedirectResponse(url=f"/notes/{note['id']}/edit", status_code=303)
|
|
|
|
|
|
@router.post("/{focus_id}/convert-to-link")
|
|
async def convert_focus_to_link(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 not item.get("title"):
|
|
return RedirectResponse(url="/focus", status_code=303)
|
|
|
|
link_repo = BaseRepository("links", db)
|
|
link = await link_repo.create({
|
|
"label": item["title"],
|
|
"url": "",
|
|
"domain_id": item.get("domain_id"),
|
|
"project_id": item.get("project_id"),
|
|
})
|
|
|
|
await repo.soft_delete(focus_id)
|
|
return RedirectResponse(url=f"/links/{link['id']}/edit", status_code=303)
|
|
|
|
|
|
@router.post("/{focus_id}/convert-to-list-item")
|
|
async def convert_focus_to_list_item(
|
|
focus_id: str, request: Request,
|
|
list_id: str = Form(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("daily_focus", db)
|
|
item = await repo.get(focus_id)
|
|
if not item or not item.get("title"):
|
|
return RedirectResponse(url="/focus", status_code=303)
|
|
|
|
# Get next sort order in the list
|
|
result = await db.execute(text("""
|
|
SELECT COALESCE(MAX(sort_order), 0) + 10 FROM list_items
|
|
WHERE list_id = :list_id AND is_deleted = false
|
|
"""), {"list_id": list_id})
|
|
next_order = result.scalar()
|
|
|
|
li_repo = BaseRepository("list_items", db)
|
|
list_item = await li_repo.create({
|
|
"list_id": list_id,
|
|
"content": item["title"],
|
|
"completed": False,
|
|
"sort_order": next_order,
|
|
})
|
|
|
|
# Update focus item to point to the new list item
|
|
await db.execute(text("""
|
|
UPDATE daily_focus
|
|
SET list_item_id = :li_id, title = NULL, domain_id = NULL, project_id = NULL,
|
|
updated_at = now()
|
|
WHERE id = :id
|
|
"""), {"li_id": list_item["id"], "id": focus_id})
|
|
await db.commit()
|
|
|
|
return RedirectResponse(url=f"/lists/{list_id}", 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"]})
|
|
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()
|
|
referer = request.headers.get("referer", "/focus")
|
|
return RedirectResponse(url=referer, status_code=303)
|
|
|
|
|
|
@router.post("/{focus_id}/toggle-critical")
|
|
async def toggle_critical(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, {"critical": not item.get("critical", False)})
|
|
return RedirectResponse(url="/focus", 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)
|
|
await repo.soft_delete(focus_id)
|
|
return RedirectResponse(url="/focus", status_code=303)
|