"use client"; import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { CalendarIcon, PlayIcon, CheckCircleIcon, PlusIcon, TimerIcon } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Item, ItemContent, ItemDescription, ItemFooter, ItemHeader, ItemMedia, ItemTitle, } from "@/components/ui/item"; import { Progress } from "@/components/ui/progress"; import { CreateSprintDialog } from "@/components/frontend/projects/CreateSprintDialog"; import { startSprint, completeSprint } from "@/app/(frontend)/projects/actions"; import { cn } from "@/lib/utils"; export interface SprintItem { readonly id: number; readonly name: string; readonly status: string; readonly goal?: string | null; readonly startDate?: string | null; readonly endDate?: string | null; readonly capacity?: number | null; readonly storyPointsCompleted: number; readonly storyPointsTotal: number; } export interface SprintsViewProps { readonly project: { id: number; name: string; key: string }; readonly sprints: readonly SprintItem[]; readonly isAdmin: boolean; } const SPRINT_STATUS_STYLES: Readonly> = { planning: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30", active: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30", completed: "bg-violet-500/15 text-violet-400 border-violet-500/30", }; function labelFor(value: string): string { return value .replaceAll("_", " ") .replace(/\b\w/g, (character) => character.toUpperCase()); } function dateFor(value: string | null | undefined): string { if (!value) return "TBD"; return new Date(value).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }); } export function SprintsView({ project, sprints, isAdmin }: SprintsViewProps) { const router = useRouter(); const [createOpen, setCreateOpen] = useState(false); async function handleStartSprint(sprintId: number) { const result = await startSprint({ sprintId }); if (result.success) { toast.success("Sprint started"); router.refresh(); } else { toast.error(result.error ?? "Failed to start sprint"); } } async function handleCompleteSprint(sprintId: number) { const result = await completeSprint({ sprintId }); if (result.success) { toast.success("Sprint completed"); router.refresh(); } else { toast.error(result.error ?? "Failed to complete sprint"); } } return (

Sprints

{sprints.length} total

Plan focused delivery windows and track progress.

{isAdmin && ( )}
{sprints.length === 0 ? ( No sprints yet Create a sprint to start planning your work. ) : (
{sprints.map((sprint) => { const hasCapacity = sprint.capacity != null && sprint.capacity > 0; const usedPoints = hasCapacity ? Math.min(sprint.storyPointsTotal, sprint.capacity!) : 0; const progressPercent = hasCapacity ? Math.round((usedPoints / sprint.capacity!) * 100) : 0; return (
{sprint.name} {labelFor(sprint.status)}
{isAdmin && (
{sprint.status === "planning" && ( )} {sprint.status === "active" && ( )}
)}
{sprint.goal && {sprint.goal}} {hasCapacity && (
Story points: {sprint.storyPointsTotal} / {sprint.capacity} {progressPercent}%
)} {!hasCapacity && (
Story points: {sprint.storyPointsTotal}
)}
{dateFor(sprint.startDate)} — {dateFor(sprint.endDate)}
); })}
)}
); }