276 lines
9.5 KiB
Python
276 lines
9.5 KiB
Python
"""Lists: checklist/ordered list management with inline items."""
|
|
|
|
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 datetime, timezone
|
|
|
|
from core.database import get_db
|
|
from core.base_repository import BaseRepository
|
|
from core.sidebar import get_sidebar_data
|
|
|
|
router = APIRouter(prefix="/lists", tags=["lists"])
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
|
|
@router.get("/")
|
|
async def list_lists(
|
|
request: Request,
|
|
domain_id: Optional[str] = None,
|
|
project_id: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
sidebar = await get_sidebar_data(db)
|
|
|
|
where_clauses = ["l.is_deleted = false"]
|
|
params = {}
|
|
if domain_id:
|
|
where_clauses.append("l.domain_id = :domain_id")
|
|
params["domain_id"] = domain_id
|
|
if project_id:
|
|
where_clauses.append("l.project_id = :project_id")
|
|
params["project_id"] = project_id
|
|
|
|
where_sql = " AND ".join(where_clauses)
|
|
|
|
result = await db.execute(text(f"""
|
|
SELECT l.*,
|
|
d.name as domain_name, d.color as domain_color,
|
|
p.name as project_name,
|
|
(SELECT count(*) FROM list_items li WHERE li.list_id = l.id AND li.is_deleted = false) as item_count,
|
|
(SELECT count(*) FROM list_items li WHERE li.list_id = l.id AND li.is_deleted = false AND li.completed = true) as completed_count
|
|
FROM lists l
|
|
LEFT JOIN domains d ON l.domain_id = d.id
|
|
LEFT JOIN projects p ON l.project_id = p.id
|
|
WHERE {where_sql}
|
|
ORDER BY l.sort_order, l.created_at DESC
|
|
"""), params)
|
|
items = [dict(r._mapping) for r in result]
|
|
|
|
# Filter options
|
|
domains_repo = BaseRepository("domains", db)
|
|
domains = await domains_repo.list()
|
|
projects_repo = BaseRepository("projects", db)
|
|
projects = await projects_repo.list()
|
|
|
|
return templates.TemplateResponse("lists.html", {
|
|
"request": request, "sidebar": sidebar, "items": items,
|
|
"domains": domains, "projects": projects,
|
|
"current_domain_id": domain_id or "",
|
|
"current_project_id": project_id or "",
|
|
"page_title": "Lists", "active_nav": "lists",
|
|
})
|
|
|
|
|
|
@router.get("/create")
|
|
async def create_form(
|
|
request: Request,
|
|
domain_id: Optional[str] = None,
|
|
project_id: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
sidebar = await get_sidebar_data(db)
|
|
domains_repo = BaseRepository("domains", db)
|
|
domains = await domains_repo.list()
|
|
projects_repo = BaseRepository("projects", db)
|
|
projects = await projects_repo.list()
|
|
areas_repo = BaseRepository("areas", db)
|
|
areas = await areas_repo.list()
|
|
|
|
return templates.TemplateResponse("list_form.html", {
|
|
"request": request, "sidebar": sidebar,
|
|
"domains": domains, "projects": projects, "areas": areas,
|
|
"page_title": "New List", "active_nav": "lists",
|
|
"item": None,
|
|
"prefill_domain_id": domain_id or "",
|
|
"prefill_project_id": project_id or "",
|
|
})
|
|
|
|
|
|
@router.post("/create")
|
|
async def create_list(
|
|
request: Request,
|
|
name: str = Form(...),
|
|
domain_id: str = Form(...),
|
|
area_id: Optional[str] = Form(None),
|
|
project_id: Optional[str] = Form(None),
|
|
list_type: str = Form("checklist"),
|
|
description: Optional[str] = Form(None),
|
|
tags: Optional[str] = Form(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("lists", db)
|
|
data = {
|
|
"name": name, "domain_id": domain_id,
|
|
"list_type": list_type,
|
|
"description": description,
|
|
}
|
|
if area_id and area_id.strip():
|
|
data["area_id"] = area_id
|
|
if project_id and project_id.strip():
|
|
data["project_id"] = project_id
|
|
if tags and tags.strip():
|
|
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
|
|
|
new_list = await repo.create(data)
|
|
return RedirectResponse(url=f"/lists/{new_list['id']}", status_code=303)
|
|
|
|
|
|
@router.get("/{list_id}")
|
|
async def list_detail(list_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
|
repo = BaseRepository("lists", db)
|
|
sidebar = await get_sidebar_data(db)
|
|
item = await repo.get(list_id)
|
|
if not item:
|
|
return RedirectResponse(url="/lists", status_code=303)
|
|
|
|
# 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
|
|
|
|
# List items (ordered, with hierarchy)
|
|
result = await db.execute(text("""
|
|
SELECT * FROM list_items
|
|
WHERE list_id = :list_id AND is_deleted = false
|
|
ORDER BY sort_order, created_at
|
|
"""), {"list_id": list_id})
|
|
list_items = [dict(r._mapping) for r in result]
|
|
|
|
# Separate top-level and child items
|
|
top_items = [i for i in list_items if i.get("parent_item_id") is None]
|
|
child_map = {}
|
|
for i in list_items:
|
|
pid = i.get("parent_item_id")
|
|
if pid:
|
|
child_map.setdefault(str(pid), []).append(i)
|
|
|
|
return templates.TemplateResponse("list_detail.html", {
|
|
"request": request, "sidebar": sidebar, "item": item,
|
|
"domain": domain, "project": project,
|
|
"list_items": top_items, "child_map": child_map,
|
|
"page_title": item["name"], "active_nav": "lists",
|
|
})
|
|
|
|
|
|
@router.get("/{list_id}/edit")
|
|
async def edit_form(list_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
|
repo = BaseRepository("lists", db)
|
|
sidebar = await get_sidebar_data(db)
|
|
item = await repo.get(list_id)
|
|
if not item:
|
|
return RedirectResponse(url="/lists", status_code=303)
|
|
|
|
domains_repo = BaseRepository("domains", db)
|
|
domains = await domains_repo.list()
|
|
projects_repo = BaseRepository("projects", db)
|
|
projects = await projects_repo.list()
|
|
areas_repo = BaseRepository("areas", db)
|
|
areas = await areas_repo.list()
|
|
|
|
return templates.TemplateResponse("list_form.html", {
|
|
"request": request, "sidebar": sidebar,
|
|
"domains": domains, "projects": projects, "areas": areas,
|
|
"page_title": "Edit List", "active_nav": "lists",
|
|
"item": item,
|
|
"prefill_domain_id": "", "prefill_project_id": "",
|
|
})
|
|
|
|
|
|
@router.post("/{list_id}/edit")
|
|
async def update_list(
|
|
list_id: str,
|
|
name: str = Form(...),
|
|
domain_id: str = Form(...),
|
|
area_id: Optional[str] = Form(None),
|
|
project_id: Optional[str] = Form(None),
|
|
list_type: str = Form("checklist"),
|
|
description: Optional[str] = Form(None),
|
|
tags: Optional[str] = Form(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("lists", db)
|
|
data = {
|
|
"name": name, "domain_id": domain_id,
|
|
"list_type": list_type, "description": description,
|
|
"area_id": area_id if area_id and area_id.strip() else None,
|
|
"project_id": project_id if project_id and project_id.strip() else None,
|
|
}
|
|
if tags and tags.strip():
|
|
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
|
else:
|
|
data["tags"] = None
|
|
|
|
await repo.update(list_id, data)
|
|
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
|
|
|
|
|
@router.post("/{list_id}/delete")
|
|
async def delete_list(list_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
|
repo = BaseRepository("lists", db)
|
|
await repo.soft_delete(list_id)
|
|
return RedirectResponse(url="/lists", status_code=303)
|
|
|
|
|
|
# ---- List Items ----
|
|
|
|
@router.post("/{list_id}/items/add")
|
|
async def add_item(
|
|
list_id: str,
|
|
request: Request,
|
|
content: str = Form(...),
|
|
parent_item_id: Optional[str] = Form(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("list_items", db)
|
|
data = {"list_id": list_id, "content": content, "completed": False}
|
|
if parent_item_id and parent_item_id.strip():
|
|
data["parent_item_id"] = parent_item_id
|
|
await repo.create(data)
|
|
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
|
|
|
|
|
@router.post("/{list_id}/items/{item_id}/toggle")
|
|
async def toggle_item(list_id: str, item_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
|
repo = BaseRepository("list_items", db)
|
|
item = await repo.get(item_id)
|
|
if not item:
|
|
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
|
|
|
if item["completed"]:
|
|
await repo.update(item_id, {"completed": False, "completed_at": None})
|
|
else:
|
|
await repo.update(item_id, {"completed": True, "completed_at": datetime.now(timezone.utc)})
|
|
|
|
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
|
|
|
|
|
@router.post("/{list_id}/items/{item_id}/delete")
|
|
async def delete_item(list_id: str, item_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
|
repo = BaseRepository("list_items", db)
|
|
await repo.soft_delete(item_id)
|
|
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|
|
|
|
|
|
@router.post("/{list_id}/items/{item_id}/edit")
|
|
async def edit_item(
|
|
list_id: str,
|
|
item_id: str,
|
|
content: str = Form(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
repo = BaseRepository("list_items", db)
|
|
await repo.update(item_id, {"content": content})
|
|
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
|