From 01f57239a897954679679af76194147afe38c49b Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 15 Nov 2025 17:02:29 +0000 Subject: [PATCH] Update sprint endpoint to support CRUD and listing cgen-cdfaf793dbd64d42b6476f847b95bf27 --- api/gameforge/sprint.ts | 242 ++++++++++++++++++++++++++++++++-------- 1 file changed, 198 insertions(+), 44 deletions(-) diff --git a/api/gameforge/sprint.ts b/api/gameforge/sprint.ts index e7f0936b..7876bb3e 100644 --- a/api/gameforge/sprint.ts +++ b/api/gameforge/sprint.ts @@ -1,55 +1,209 @@ -import { supabase } from "../_supabase"; +import type { VercelRequest, VercelResponse } from "@vercel/node"; +import { getAdminClient } from "../../_supabase"; -export default async (req: Request) => { - if (req.method !== "GET") { - return new Response("Method not allowed", { status: 405 }); +const admin = getAdminClient(); + +export default async function handler(req: VercelRequest, res: VercelResponse) { + const authHeader = req.headers.authorization; + if (!authHeader) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const token = authHeader.replace("Bearer ", ""); + const { + data: { user }, + error: authError, + } = await admin.auth.getUser(token); + + if (authError || !user) { + return res.status(401).json({ error: "Invalid token" }); } try { - const token = req.headers.get("Authorization")?.replace("Bearer ", ""); - if (!token) { - return new Response("Unauthorized", { status: 401 }); - } + // GET: List sprints for user's projects or sprints user is a member of + if (req.method === "GET") { + const { projectId, status } = req.query; - const { data: userData } = await supabase.auth.getUser(token); - if (!userData.user) { - return new Response("Unauthorized", { status: 401 }); - } + let query = admin + .from("gameforge_sprints") + .select( + ` + id, + project_id, + sprint_number, + title, + description, + phase, + status, + goal, + start_date, + end_date, + planned_velocity, + actual_velocity, + created_by, + created_at, + updated_at, + gameforge_projects(name), + gameforge_sprint_members(user_id) + `, + ); - const { data: sprint, error } = await supabase - .from("gameforge_sprints") - .select( - ` - id, - project_id, - title, - phase, - status, - start_date, - end_date, - deadline, - gdd, - scope - `, - ) - .eq("user_id", userData.user.id) - .order("created_at", { ascending: false }) - .limit(1) - .single(); + if (projectId) { + query = query.eq("project_id", projectId); + } else { + // Get sprints for projects user is on + query = query.in( + "project_id", + ` + select id from gameforge_projects + where lead_id = '${user.id}' + or id in ( + select distinct project_id from gameforge_team_members + where user_id = '${user.id}' + ) + `, + ); + } - if (error && error.code !== "PGRST116") { - console.error("Sprint fetch error:", error); - return new Response(JSON.stringify({ error: error.message }), { - status: 500, + if (status) { + query = query.eq("status", status); + } + + const { data: sprints, error } = await query.order("created_at", { + ascending: false, }); + + if (error) { + return res.status(500).json({ error: error.message }); + } + + return res.status(200).json(sprints || []); } - return new Response(JSON.stringify(sprint || null), { - headers: { "Content-Type": "application/json" }, - }); - } catch (err: any) { - return new Response(JSON.stringify({ error: err.message }), { - status: 500, - }); + // POST: Create a sprint + if (req.method === "POST") { + const { + projectId, + title, + description, + goal, + startDate, + endDate, + plannedVelocity, + } = req.body; + + // Verify user is project lead + const { data: project, error: projectError } = await admin + .from("gameforge_projects") + .select("id") + .eq("id", projectId) + .eq("lead_id", user.id) + .single(); + + if (projectError || !project) { + return res.status(403).json({ error: "Not project lead" }); + } + + // Get next sprint number + const { data: lastSprint, error: lastSprintError } = await admin + .from("gameforge_sprints") + .select("sprint_number") + .eq("project_id", projectId) + .order("sprint_number", { ascending: false }) + .limit(1) + .single(); + + const nextSprintNumber = (lastSprint?.sprint_number || 0) + 1; + + const { data: sprint, error: createError } = await admin + .from("gameforge_sprints") + .insert([ + { + project_id: projectId, + sprint_number: nextSprintNumber, + title, + description, + goal, + start_date: startDate, + end_date: endDate, + planned_velocity: plannedVelocity, + created_by: user.id, + phase: "planning", + status: "pending", + }, + ]) + .select() + .single(); + + if (createError) { + return res.status(500).json({ error: createError.message }); + } + + // Auto-add creator as sprint lead + await admin.from("gameforge_sprint_members").insert([ + { + sprint_id: sprint.id, + user_id: user.id, + role: "lead", + }, + ]); + + return res.status(201).json(sprint); + } + + // PUT: Update a sprint + if (req.method === "PUT") { + const { sprintId } = req.query; + const { title, description, goal, startDate, endDate, phase, status } = + req.body; + + // Verify user is project lead + const { data: sprint, error: sprintError } = await admin + .from("gameforge_sprints") + .select("project_id") + .eq("id", sprintId) + .single(); + + if (sprintError || !sprint) { + return res.status(404).json({ error: "Sprint not found" }); + } + + const { data: project, error: projectError } = await admin + .from("gameforge_projects") + .select("id") + .eq("id", sprint.project_id) + .eq("lead_id", user.id) + .single(); + + if (projectError || !project) { + return res.status(403).json({ error: "Not project lead" }); + } + + const { data: updated, error: updateError } = await admin + .from("gameforge_sprints") + .update({ + title, + description, + goal, + start_date: startDate, + end_date: endDate, + phase, + status, + }) + .eq("id", sprintId) + .select() + .single(); + + if (updateError) { + return res.status(500).json({ error: updateError.message }); + } + + return res.status(200).json(updated); + } + + return res.status(405).json({ error: "Method not allowed" }); + } catch (error: any) { + console.error("[GameForge Sprint]", error); + return res.status(500).json({ error: error?.message || "Server error" }); } -}; +}