141 lines
4.7 KiB
Python
141 lines
4.7 KiB
Python
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)
|