diff --git a/screenshots/main-page-with-archive.png b/screenshots/main-page-with-archive.png new file mode 100644 index 0000000..6790801 Binary files /dev/null and b/screenshots/main-page-with-archive.png differ diff --git a/screenshots/sprint-archive.png b/screenshots/sprint-archive.png new file mode 100644 index 0000000..6790801 Binary files /dev/null and b/screenshots/sprint-archive.png differ diff --git a/src/app/api/sprints/route.ts b/src/app/api/sprints/route.ts index 344b1c7..4ba1a75 100644 --- a/src/app/api/sprints/route.ts +++ b/src/app/api/sprints/route.ts @@ -4,20 +4,31 @@ import { getAuthenticatedUser } from "@/lib/server/auth"; export const runtime = "nodejs"; -// GET - fetch all sprints -export async function GET() { +// GET - fetch all sprints (optionally filtered by status) +export async function GET(request: Request) { try { const user = await getAuthenticatedUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } + // Parse query params + const { searchParams } = new URL(request.url); + const status = searchParams.get("status"); + const supabase = getServiceSupabase(); - const { data: sprints, error } = await supabase + let query = supabase .from("sprints") .select("*") .order("start_date", { ascending: true }); + // Filter by status if provided + if (status && ["planning", "active", "completed"].includes(status)) { + query = query.eq("status", status); + } + + const { data: sprints, error } = await query; + if (error) throw error; return NextResponse.json({ sprints: sprints || [] }); diff --git a/src/app/page.tsx b/src/app/page.tsx index 8fa4759..3b453b8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -35,7 +35,7 @@ import { } from "@/lib/attachments" import { useTaskStore, Task, TaskType, TaskStatus, Priority, TaskAttachment, type CommentAuthor } from "@/stores/useTaskStore" import { BacklogView } from "@/components/BacklogView" -import { Plus, MessageSquare, Calendar, Trash2, X, LayoutGrid, ListTodo, GripVertical, Paperclip, Download, Search } from "lucide-react" +import { Plus, MessageSquare, Calendar, Trash2, X, LayoutGrid, ListTodo, GripVertical, Paperclip, Download, Search, Archive } from "lucide-react" interface AssignableUser { id: string @@ -594,18 +594,21 @@ export default function Home() { }) // Filter tasks to only show current sprint tasks in Kanban (from ALL projects) + // Sort by updatedAt descending (latest first) const sprintTasks = currentSprint - ? tasks.filter((t) => { - if (t.sprintId !== currentSprint.id) return false - // Apply search filter - if (debouncedSearchQuery.trim()) { - const query = debouncedSearchQuery.toLowerCase() - const matchesTitle = t.title.toLowerCase().includes(query) - const matchesDescription = t.description?.toLowerCase().includes(query) ?? false - return matchesTitle || matchesDescription - } - return true - }) + ? tasks + .filter((t) => { + if (t.sprintId !== currentSprint.id) return false + // Apply search filter + if (debouncedSearchQuery.trim()) { + const query = debouncedSearchQuery.toLowerCase() + const matchesTitle = t.title.toLowerCase().includes(query) + const matchesDescription = t.description?.toLowerCase().includes(query) ?? false + return matchesTitle || matchesDescription + } + return true + }) + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) : [] // Auto-rollover: Move incomplete tasks from ended sprints to next sprint @@ -989,6 +992,15 @@ export default function Home() { {currentUser.name} + +
+

+ Sprint Archive +

+

+ View completed sprints and their metrics +

+
+ + + + + +
+ {isLoading ? ( +
+
+ + + + + Loading sprint history... +
+
+ ) : sprintStats.length === 0 ? ( +
+ +

No Completed Sprints

+

+ When sprints are marked as completed, they will appear here with their metrics and statistics. +

+ +
+ ) : ( +
+ {sprintStats.map((stat) => ( + handleSprintClick(stat.sprint.id)} + > + +
+
+

+ {stat.sprint.name} +

+ {stat.sprint.goal && ( +

{stat.sprint.goal}

+ )} +
+ + Completed + +
+
+ + {/* Date Range */} +
+ + {formatDateRange(stat.sprint.startDate, stat.sprint.endDate)} +
+ + {/* Stats Grid */} +
+
+
+ + Total Tasks +
+

{stat.totalTasks}

+
+
+
+ + Completed +
+

{stat.completedTasks}

+
+
+ + {/* Completion Rate */} +
+
+ Completion Rate + {stat.completionRate}% +
+
+
= 80 + ? "bg-emerald-500" + : stat.completionRate >= 50 + ? "bg-blue-500" + : "bg-amber-500" + }`} + style={{ width: `${stat.completionRate}%` }} + /> +
+
+ + {/* Velocity & Duration */} +
+
+ + Velocity: {stat.velocity} tasks/day +
+
+ + {formatDuration(stat.durationDays)} +
+
+ + {/* Click hint */} +
+ View Details + +
+ + + ))} +
+ )} +
+ + {/* Sprint Detail Dialog */} + + + {selectedSprint && ( + <> + + Sprint Details +
+
+

{selectedSprint.sprint.name}

+ {selectedSprint.sprint.goal && ( +

{selectedSprint.sprint.goal}

+ )} +
+ + Completed + +
+
+ + {formatDateRange(selectedSprint.sprint.startDate, selectedSprint.sprint.endDate)} +
+
+ +
+ {/* Summary Stats */} +
+
+

{selectedSprint.tasks.length}

+

Total Tasks

+
+
+

{selectedSprint.completedTasks.length}

+

Completed

+
+
+

{selectedSprint.rolledOverTasks.length}

+

Rolled Over

+
+
+ + {/* Completed Tasks */} + {selectedSprint.completedTasks.length > 0 && ( +
+

+ + Completed Tasks ({selectedSprint.completedTasks.length}) +

+
+ {selectedSprint.completedTasks.map((task) => ( +
+ + {task.type} + + {task.title} + + {task.priority} + +
+ ))} +
+
+ )} + + {/* Rolled Over Tasks */} + {selectedSprint.rolledOverTasks.length > 0 && ( +
+

+ + Rolled Over Tasks ({selectedSprint.rolledOverTasks.length}) +

+
+ {selectedSprint.rolledOverTasks.map((task) => ( +
+ + {task.type} + + {task.title} + + {task.status} + +
+ ))} +
+
+ )} + + {/* Canceled Tasks */} + {selectedSprint.tasks.filter((t) => t.status === "canceled").length > 0 && ( +
+

+ + Canceled Tasks ({selectedSprint.tasks.filter((t) => t.status === "canceled").length}) +

+
+ {selectedSprint.tasks + .filter((t) => t.status === "canceled") + .map((task) => ( +
+ + {task.type} + + {task.title} + + canceled + +
+ ))} +
+
+ )} +
+ + )} +
+
+
+ ) +}