import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { aethexToast } from "@/lib/aethex-toast"; import { supabase } from "@/lib/supabase"; import { Plus, Users, Calendar, CheckCircle } from "lucide-react"; const API_BASE = import.meta.env.VITE_API_BASE || ""; interface Sprint { id: string; project_id: string; sprint_number: number; title: string; description?: string; goal?: string; phase: "planning" | "active" | "completed" | "cancelled"; status: "pending" | "active" | "on_hold" | "completed"; start_date?: string; end_date?: string; planned_velocity?: number; actual_velocity?: number; gameforge_projects?: { name: string }; gameforge_sprint_members?: Array<{ user_id: string }>; } interface SprintManagerProps { projectId: string; projectName?: string; isProjectLead?: boolean; onSprintJoined?: (sprint: Sprint) => void; } export default function SprintManager({ projectId, projectName, isProjectLead = false, onSprintJoined, }: SprintManagerProps) { const [sprints, setSprints] = useState([]); const [loading, setLoading] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [userSprints, setUserSprints] = useState>(new Set()); const [formData, setFormData] = useState({ title: "", description: "", goal: "", startDate: "", endDate: "", plannedVelocity: "", }); useEffect(() => { loadSprints(); }, [projectId]); const loadSprints = async () => { setLoading(true); try { const { data: { session }, } = await supabase.auth.getSession(); const token = session?.access_token; if (!token) throw new Error("No auth token"); const res = await fetch( `${API_BASE}/api/gameforge/sprint?projectId=${projectId}`, { headers: { Authorization: `Bearer ${token}` }, }, ); if (res.ok) { const data = await res.json(); setSprints(Array.isArray(data) ? data : []); // Track which sprints user is already in const userSprintIds = new Set( data .filter((sprint: Sprint) => sprint.gameforge_sprint_members?.some( (m: any) => m.user_id === session?.user?.id, ), ) .map((s: Sprint) => s.id), ); setUserSprints(userSprintIds); } } catch (error) { console.error("Failed to load sprints:", error); } finally { setLoading(false); } }; const handleCreateSprint = async () => { if (!formData.title.trim()) { aethexToast.error({ title: "Error", description: "Sprint title is required", }); return; } try { const { data: { session }, } = await supabase.auth.getSession(); const token = session?.access_token; if (!token) throw new Error("No auth token"); const res = await fetch(`${API_BASE}/api/gameforge/sprint`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ projectId, title: formData.title, description: formData.description, goal: formData.goal, startDate: formData.startDate, endDate: formData.endDate, plannedVelocity: formData.plannedVelocity ? parseInt(formData.plannedVelocity) : null, }), }); if (!res.ok) { throw new Error(await res.text()); } const newSprint = await res.json(); setSprints([newSprint, ...sprints]); setCreateOpen(false); setFormData({ title: "", description: "", goal: "", startDate: "", endDate: "", plannedVelocity: "", }); aethexToast.success({ title: "Sprint created", description: `Sprint ${newSprint.sprint_number}: ${newSprint.title}`, }); } catch (error: any) { aethexToast.error({ title: "Failed to create sprint", description: error.message, }); } }; const handleJoinSprint = async (sprintId: string) => { try { const { data: { session }, } = await supabase.auth.getSession(); const token = session?.access_token; if (!token) throw new Error("No auth token"); const res = await fetch(`${API_BASE}/api/gameforge/sprint-join`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ sprintId }), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || "Failed to join sprint"); } // Add to user sprints set and reload setUserSprints((prev) => new Set([...prev, sprintId])); const sprint = sprints.find((s) => s.id === sprintId); if (sprint && onSprintJoined) { onSprintJoined(sprint); } aethexToast.success({ title: "Joined sprint", description: "You've been added to the sprint", }); } catch (error: any) { aethexToast.error({ title: "Failed to join sprint", description: error.message, }); } }; const getPhaseColor = (phase: string) => { switch (phase) { case "planning": return "bg-blue-500/20 text-blue-200 border-blue-500/40"; case "active": return "bg-green-500/20 text-green-200 border-green-500/40"; case "completed": return "bg-purple-500/20 text-purple-200 border-purple-500/40"; default: return "bg-gray-500/20 text-gray-200 border-gray-500/40"; } }; const getStatusColor = (status: string) => { switch (status) { case "active": return "bg-green-600"; case "completed": return "bg-blue-600"; case "on_hold": return "bg-orange-600"; default: return "bg-gray-600"; } }; return (
{/* Create Sprint Button (Project Leads Only) */} {isProjectLead && ( Create New Sprint Create a new sprint for {projectName || "this project"}
setFormData({ ...formData, title: e.target.value }) } placeholder="e.g., Sprint 1: Core Mechanics" className="bg-slate-900 border-slate-700" />
setFormData({ ...formData, goal: e.target.value }) } placeholder="e.g., Implement character movement and combat" className="bg-slate-900 border-slate-700" />