from uuid import UUID from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from app.middleware.auth import get_current_user_id from app.models import StepOut, StepUpdate from app.services.db import get_pool router = APIRouter(prefix="", tags=["steps"]) STEP_COLUMNS = "s.id, s.task_id, s.sort_order, s.title, s.description, s.estimated_minutes, s.status, s.checkpoint_note, s.last_checked_at, s.completed_at, s.created_at" class CreateStepRequest(BaseModel): title: str description: str | None = None estimated_minutes: int | None = None @router.post("/tasks/{task_id}/steps", response_model=StepOut) async def create_step(task_id: UUID, req: CreateStepRequest, user_id: str = Depends(get_current_user_id)): pool = await get_pool() task = await pool.fetchrow("SELECT id FROM tasks WHERE id = $1 AND user_id = $2::uuid", task_id, user_id) if not task: raise HTTPException(status_code=404, detail="Task not found") # Place new step at the end max_order = await pool.fetchval("SELECT COALESCE(MAX(sort_order), 0) FROM steps WHERE task_id = $1", task_id) row = await pool.fetchrow( """INSERT INTO steps (task_id, sort_order, title, description, estimated_minutes) VALUES ($1, $2, $3, $4, $5) RETURNING id, task_id, sort_order, title, description, estimated_minutes, status, checkpoint_note, last_checked_at, completed_at, created_at""", task_id, max_order + 1, req.title, req.description, req.estimated_minutes, ) return StepOut(**dict(row)) @router.get("/tasks/{task_id}/steps", response_model=list[StepOut]) async def list_steps(task_id: UUID, user_id: str = Depends(get_current_user_id)): pool = await get_pool() # Verify task belongs to user task = await pool.fetchrow("SELECT id FROM tasks WHERE id = $1 AND user_id = $2::uuid", task_id, user_id) if not task: raise HTTPException(status_code=404, detail="Task not found") rows = await pool.fetch( f"SELECT {STEP_COLUMNS} FROM steps s WHERE s.task_id = $1 ORDER BY s.sort_order", task_id, ) return [StepOut(**dict(r)) for r in rows] @router.patch("/steps/{step_id}", response_model=StepOut) async def update_step(step_id: UUID, req: StepUpdate, user_id: str = Depends(get_current_user_id)): pool = await get_pool() # Verify step belongs to user's task step = await pool.fetchrow( """SELECT s.id FROM steps s JOIN tasks t ON s.task_id = t.id WHERE s.id = $1 AND t.user_id = $2::uuid""", step_id, user_id, ) if not step: raise HTTPException(status_code=404, detail="Step not found") fields = [] values = [] idx = 2 # $1 = step_id update_data = req.model_dump(exclude_unset=True) for key, val in update_data.items(): fields.append(f"{key} = ${idx}") values.append(val) idx += 1 if not fields: raise HTTPException(status_code=400, detail="No fields to update") set_clause = ", ".join(fields) row = await pool.fetchrow( f"""UPDATE steps SET {set_clause} WHERE id = $1 RETURNING id, task_id, sort_order, title, description, estimated_minutes, status, checkpoint_note, last_checked_at, completed_at, created_at""", step_id, *values, ) return StepOut(**dict(row)) @router.post("/steps/{step_id}/complete", response_model=StepOut) async def complete_step(step_id: UUID, user_id: str = Depends(get_current_user_id)): pool = await get_pool() step = await pool.fetchrow( """SELECT s.id FROM steps s JOIN tasks t ON s.task_id = t.id WHERE s.id = $1 AND t.user_id = $2::uuid""", step_id, user_id, ) if not step: raise HTTPException(status_code=404, detail="Step not found") row = await pool.fetchrow( """UPDATE steps SET status = 'done', completed_at = now() WHERE id = $1 RETURNING id, task_id, sort_order, title, description, estimated_minutes, status, checkpoint_note, last_checked_at, completed_at, created_at""", step_id, ) return StepOut(**dict(row)) @router.delete("/steps/{step_id}", status_code=204) async def delete_step(step_id: UUID, user_id: str = Depends(get_current_user_id)): pool = await get_pool() step = await pool.fetchrow( """SELECT s.id FROM steps s JOIN tasks t ON s.task_id = t.id WHERE s.id = $1 AND t.user_id = $2::uuid""", step_id, user_id, ) if not step: raise HTTPException(status_code=404, detail="Step not found") await pool.execute("DELETE FROM steps WHERE id = $1", step_id)