This commit is contained in:
2026-03-29 06:57:34 -04:00
commit 37503231b3
31 changed files with 3444 additions and 0 deletions

140
app/routers/steps.py Normal file
View File

@@ -0,0 +1,140 @@
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)