From e15024b1352b93835fc54e6b1576ed338351e5c6 Mon Sep 17 00:00:00 2001 From: clawdina Date: Tue, 10 Mar 2026 10:57:33 -0700 Subject: [PATCH] rollover incomplete tickets to active sprint on sprint review completion - Add rolloverIncompleteTodos(from, to) DB method: moves pending/in_progress/blocked non-archived todos to the target sprint - PATCH /api/sprints/[id]: when reviewed_at + status=completed, auto-rollover to active sprint (via getActiveSprint or rotateSprintIfNeeded) - Returns rolled_over count in response for visibility --- src/app/api/sprints/[id]/route.ts | 10 +++++++++- src/lib/db/index.ts | 20 ++++++++++++++++++++ src/lib/db/types.ts | 1 + 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/app/api/sprints/[id]/route.ts b/src/app/api/sprints/[id]/route.ts index 09b53aa..02ce706 100644 --- a/src/app/api/sprints/[id]/route.ts +++ b/src/app/api/sprints/[id]/route.ts @@ -56,13 +56,21 @@ export async function PATCH(request: NextRequest, context: RouteContext) { const sprint = db.updateSprint(params.id, data); + let rolledOver = 0; + if (data.reviewed_at && data.status === 'completed') { + const activeSprint = db.getActiveSprint() ?? db.rotateSprintIfNeeded(); + if (activeSprint && activeSprint.id !== params.id) { + rolledOver = db.rolloverIncompleteTodos(params.id, activeSprint.id); + } + } + eventBus.publish({ type: 'sprint:updated', payload: { sprint }, timestamp: Date.now(), }); - return NextResponse.json({ sprint }, { status: 200, headers: corsHeaders(request) }); + return NextResponse.json({ sprint, rolled_over: rolledOver }, { status: 200, headers: corsHeaders(request) }); } catch (error) { if (error instanceof z.ZodError) { return NextResponse.json({ error: 'Invalid request body', details: error.issues }, { status: 400, headers: corsHeaders(request) }); diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 3d33031..b2ac940 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -804,6 +804,26 @@ const db: DatabaseOperations = { stmt.run(todoId, sprintId); }, + rolloverIncompleteTodos(fromSprintId: string, toSprintId: string): number { + const database = getDatabase(); + const todos = db.getSprintTodos(fromSprintId); + const incomplete = todos.filter( + (t) => (t.status === 'pending' || t.status === 'in_progress' || t.status === 'blocked') && !t.archived_at + ); + + const insertStmt = database.prepare(` + INSERT INTO todo_sprints (todo_id, sprint_id) + VALUES (?, ?) + ON CONFLICT(todo_id, sprint_id) DO NOTHING + `); + + for (const todo of incomplete) { + insertStmt.run(todo.id, toSprintId); + } + + return incomplete.length; + }, + removeTodoFromSprint(todoId: string, sprintId: string): void { const database = getDatabase(); const stmt = database.prepare('DELETE FROM todo_sprints WHERE todo_id = ? AND sprint_id = ?'); diff --git a/src/lib/db/types.ts b/src/lib/db/types.ts index 7c6d596..271a7d6 100644 --- a/src/lib/db/types.ts +++ b/src/lib/db/types.ts @@ -263,6 +263,7 @@ export interface DatabaseOperations { getSprint(id: string): Sprint | null; getAllSprints(): Sprint[]; updateSprint(id: string, updates: Partial>): Sprint; + rolloverIncompleteTodos(fromSprintId: string, toSprintId: string): number; assignTodoToSprint(todoId: string, sprintId: string): void; removeTodoFromSprint(todoId: string, sprintId: string): void; getSprintTodos(sprintId: string): Todo[];