"""Decisions: knowledge base of decisions with rationale, status, and supersession.""" 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 core.database import get_db from core.base_repository import BaseRepository from core.sidebar import get_sidebar_data router = APIRouter(prefix="/decisions", tags=["decisions"]) templates = Jinja2Templates(directory="templates") @router.get("/") async def list_decisions( request: Request, status: Optional[str] = None, impact: Optional[str] = None, db: AsyncSession = Depends(get_db), ): sidebar = await get_sidebar_data(db) where_clauses = ["d.is_deleted = false"] params = {} if status: where_clauses.append("d.status = :status") params["status"] = status if impact: where_clauses.append("d.impact = :impact") params["impact"] = impact where_sql = " AND ".join(where_clauses) result = await db.execute(text(f""" SELECT d.*, m.title as meeting_title FROM decisions d LEFT JOIN meetings m ON d.meeting_id = m.id WHERE {where_sql} ORDER BY d.created_at DESC """), params) items = [dict(r._mapping) for r in result] return templates.TemplateResponse("decisions.html", { "request": request, "sidebar": sidebar, "items": items, "current_status": status or "", "current_impact": impact or "", "page_title": "Decisions", "active_nav": "decisions", }) @router.get("/create") async def create_form( request: Request, meeting_id: Optional[str] = None, task_id: Optional[str] = None, db: AsyncSession = Depends(get_db), ): sidebar = await get_sidebar_data(db) # Meetings for linking result = await db.execute(text(""" SELECT id, title, meeting_date FROM meetings WHERE is_deleted = false ORDER BY meeting_date DESC LIMIT 50 """)) meetings = [dict(r._mapping) for r in result] return templates.TemplateResponse("decision_form.html", { "request": request, "sidebar": sidebar, "meetings": meetings, "page_title": "New Decision", "active_nav": "decisions", "item": None, "prefill_meeting_id": meeting_id or "", "prefill_task_id": task_id or "", }) @router.post("/create") async def create_decision( request: Request, title: str = Form(...), rationale: Optional[str] = Form(None), status: str = Form("proposed"), impact: str = Form("medium"), decided_at: Optional[str] = Form(None), meeting_id: Optional[str] = Form(None), task_id: Optional[str] = Form(None), tags: Optional[str] = Form(None), db: AsyncSession = Depends(get_db), ): repo = BaseRepository("decisions", db) data = { "title": title, "status": status, "impact": impact, "rationale": rationale, } if decided_at and decided_at.strip(): data["decided_at"] = decided_at if meeting_id and meeting_id.strip(): data["meeting_id"] = meeting_id if task_id and task_id.strip(): data["task_id"] = task_id if tags and tags.strip(): data["tags"] = [t.strip() for t in tags.split(",") if t.strip()] decision = await repo.create(data) if task_id and task_id.strip(): return RedirectResponse(url=f"/tasks/{task_id}?tab=decisions", status_code=303) return RedirectResponse(url=f"/decisions/{decision['id']}", status_code=303) @router.get("/{decision_id}") async def decision_detail(decision_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("decisions", db) sidebar = await get_sidebar_data(db) item = await repo.get(decision_id) if not item: return RedirectResponse(url="/decisions", status_code=303) # Meeting info meeting = None if item.get("meeting_id"): result = await db.execute(text( "SELECT id, title, meeting_date FROM meetings WHERE id = :id" ), {"id": str(item["meeting_id"])}) row = result.first() meeting = dict(row._mapping) if row else None # Superseded by superseded_by = None if item.get("superseded_by_id"): result = await db.execute(text( "SELECT id, title FROM decisions WHERE id = :id" ), {"id": str(item["superseded_by_id"])}) row = result.first() superseded_by = dict(row._mapping) if row else None # Decisions that this one supersedes result = await db.execute(text(""" SELECT id, title FROM decisions WHERE superseded_by_id = :did AND is_deleted = false """), {"did": decision_id}) supersedes = [dict(r._mapping) for r in result] return templates.TemplateResponse("decision_detail.html", { "request": request, "sidebar": sidebar, "item": item, "meeting": meeting, "superseded_by": superseded_by, "supersedes": supersedes, "page_title": item["title"], "active_nav": "decisions", }) @router.get("/{decision_id}/edit") async def edit_form(decision_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("decisions", db) sidebar = await get_sidebar_data(db) item = await repo.get(decision_id) if not item: return RedirectResponse(url="/decisions", status_code=303) result = await db.execute(text(""" SELECT id, title, meeting_date FROM meetings WHERE is_deleted = false ORDER BY meeting_date DESC LIMIT 50 """)) meetings = [dict(r._mapping) for r in result] # Other decisions for supersession result = await db.execute(text(""" SELECT id, title FROM decisions WHERE is_deleted = false AND id != :did ORDER BY created_at DESC LIMIT 50 """), {"did": decision_id}) other_decisions = [dict(r._mapping) for r in result] return templates.TemplateResponse("decision_form.html", { "request": request, "sidebar": sidebar, "meetings": meetings, "other_decisions": other_decisions, "page_title": "Edit Decision", "active_nav": "decisions", "item": item, "prefill_meeting_id": "", }) @router.post("/{decision_id}/edit") async def update_decision( decision_id: str, title: str = Form(...), rationale: Optional[str] = Form(None), status: str = Form("proposed"), impact: str = Form("medium"), decided_at: Optional[str] = Form(None), meeting_id: Optional[str] = Form(None), superseded_by_id: Optional[str] = Form(None), tags: Optional[str] = Form(None), db: AsyncSession = Depends(get_db), ): repo = BaseRepository("decisions", db) data = { "title": title, "status": status, "impact": impact, "rationale": rationale if rationale and rationale.strip() else None, "decided_at": decided_at if decided_at and decided_at.strip() else None, "meeting_id": meeting_id if meeting_id and meeting_id.strip() else None, "superseded_by_id": superseded_by_id if superseded_by_id and superseded_by_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(decision_id, data) return RedirectResponse(url=f"/decisions/{decision_id}", status_code=303) @router.post("/{decision_id}/delete") async def delete_decision(decision_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("decisions", db) await repo.soft_delete(decision_id) return RedirectResponse(url="/decisions", status_code=303)