From 97a8ab1a557d75c3d087bc51be73e9699de30806 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 23 Feb 2026 07:43:18 -0600 Subject: [PATCH] Signed-off-by: Max --- src/app/page.tsx | 18 +++----- src/app/sprints/archive/page.tsx | 74 +++++++++++++++++++++++++++----- src/components/BacklogView.tsx | 28 ++++++------ src/components/SprintBoard.tsx | 15 +++++-- src/lib/utils.ts | 20 +++++++++ 5 files changed, 117 insertions(+), 38 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 21e6320..dbf6933 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -24,7 +24,7 @@ import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import { Textarea } from "@/components/ui/textarea" import { Label } from "@/components/ui/label" -import { parseSprintEnd, parseSprintStart } from "@/lib/utils" +import { isSprintInProgress, parseSprintEnd, parseSprintStart } from "@/lib/utils" import { generateAvatarDataUrl } from "@/lib/avatar" import { blobFromDataUrl, @@ -609,15 +609,11 @@ export default function Home() { } }, [selectedTask]) - // Get current active sprint (across all projects) - // Treat end date as end-of-day (23:59:59) to handle timezone issues + // Get current sprint (across all projects) using local-day boundaries. const now = new Date() - const currentSprint = sprints.find((s) => { - if (s.status !== 'active') return false - const sprintStart = parseSprintStart(s.startDate) - const sprintEnd = parseSprintEnd(s.endDate) - return sprintStart <= now && sprintEnd >= now - }) + const currentSprint = + sprints.find((s) => s.status === "active" && isSprintInProgress(s.startDate, s.endDate, now)) ?? + sprints.find((s) => s.status !== "completed" && isSprintInProgress(s.startDate, s.endDate, now)) // Filter tasks to only show current sprint tasks in Kanban (from ALL projects) // Sort by updatedAt descending (latest first) @@ -643,7 +639,7 @@ export default function Home() { const now = new Date() const endedSprints = sprints.filter((s) => { - if (s.status !== 'active') return false + if (s.status === "completed") return false const sprintEnd = parseSprintEnd(s.endDate) return sprintEnd < now }) @@ -652,7 +648,7 @@ export default function Home() { // Find next sprint (earliest start date that's in the future or active) const nextSprint = sprints - .filter((s) => s.status === 'planning' || (s.status === 'active' && !endedSprints.find((e) => e.id === s.id))) + .filter((s) => s.status !== "completed" && !endedSprints.find((e) => e.id === s.id)) .sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime())[0] if (!nextSprint) return diff --git a/src/app/sprints/archive/page.tsx b/src/app/sprints/archive/page.tsx index 83ae80c..69ba76d 100644 --- a/src/app/sprints/archive/page.tsx +++ b/src/app/sprints/archive/page.tsx @@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { parseSprintEnd, parseSprintStart } from "@/lib/utils" +import { inferSprintStatusForDateRange, parseSprintEnd, parseSprintStart } from "@/lib/utils" interface Sprint { id: string @@ -46,6 +46,56 @@ interface SprintDetail { rolledOverTasks: Task[] } +type SprintInput = Partial & { + start_date?: string + end_date?: string + project_id?: string + created_at?: string +} + +function normalizeSprint(input: SprintInput | null | undefined): Sprint | null { + if (!input) return null + const id = typeof input.id === "string" ? input.id : "" + const name = typeof input.name === "string" ? input.name : "" + const startDate = typeof input.startDate === "string" + ? input.startDate + : typeof input.start_date === "string" + ? input.start_date + : "" + const endDate = typeof input.endDate === "string" + ? input.endDate + : typeof input.end_date === "string" + ? input.end_date + : "" + + if (!id || !name || !startDate || !endDate) return null + + const status = input.status === "planning" || input.status === "active" || input.status === "completed" + ? input.status + : inferSprintStatusForDateRange(startDate, endDate) + const projectId = typeof input.projectId === "string" + ? input.projectId + : typeof input.project_id === "string" + ? input.project_id + : "" + const createdAt = typeof input.createdAt === "string" + ? input.createdAt + : typeof input.created_at === "string" + ? input.created_at + : new Date().toISOString() + + return { + id, + name, + goal: typeof input.goal === "string" ? input.goal : null, + startDate, + endDate, + status, + projectId, + createdAt, + } +} + function formatDateRange(startDate: string, endDate: string): string { const start = parseSprintStart(startDate) const end = parseSprintEnd(endDate) @@ -93,20 +143,21 @@ export default function SprintArchivePage() { const fetchData = async () => { setIsLoading(true) try { - // Fetch completed sprints - const sprintsRes = await fetch("/api/sprints?status=completed") - const sprintsData = await sprintsRes.json() - const completedSprints: Sprint[] = (sprintsData.sprints || []).filter( - (s: Sprint) => s.status === "completed" - ) - - // Fetch all tasks + // Fetch normalized sprint + task data from a single endpoint. const tasksRes = await fetch("/api/tasks") const tasksData = await tasksRes.json() const allTasks: Task[] = tasksData.tasks || [] + const allSprints: Sprint[] = (tasksData.sprints || []) + .map((entry: SprintInput) => normalizeSprint(entry)) + .filter((entry: Sprint | null): entry is Sprint => entry !== null) + const now = new Date() + const archivedSprints = allSprints.filter((sprint) => { + if (sprint.status === "completed") return true + return parseSprintEnd(sprint.endDate).getTime() < now.getTime() + }) // Calculate stats for each sprint - const stats: SprintStats[] = completedSprints.map((sprint) => { + const stats: SprintStats[] = archivedSprints.map((sprint) => { const sprintTasks = allTasks.filter((t) => t.sprintId === sprint.id) const completedTasks = sprintTasks.filter( (t) => t.status === "done" || t.status === "archived" @@ -152,7 +203,8 @@ export default function SprintArchivePage() { const sprintData = await sprintRes.json() const tasksData = await tasksRes.json() - const sprint: Sprint = sprintData.sprint + const sprint = normalizeSprint(sprintData.sprint) + if (!sprint) throw new Error("Invalid sprint payload") const allTasks: Task[] = tasksData.tasks || [] const sprintTasks = allTasks.filter((t) => t.sprintId === sprintId) const completedTasks = sprintTasks.filter( diff --git a/src/components/BacklogView.tsx b/src/components/BacklogView.tsx index 2cef96e..81cffe0 100644 --- a/src/components/BacklogView.tsx +++ b/src/components/BacklogView.tsx @@ -25,7 +25,13 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Plus, GripVertical, ChevronDown, ChevronRight, Calendar } from "lucide-react" import { format, isValid } from "date-fns" -import { parseSprintEnd, parseSprintStart, toLocalDateInputValue } from "@/lib/utils" +import { + inferSprintStatusForDateRange, + isSprintInProgress, + parseSprintEnd, + parseSprintStart, + toLocalDateInputValue, +} from "@/lib/utils" import { generateAvatarDataUrl } from "@/lib/avatar" const priorityColors: Record = { @@ -306,8 +312,7 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) { }) ) - // Get current active sprint - // Treat end date as end-of-day (23:59:59) to handle timezone issues + // Get current sprint using local-day boundaries (00:00 start, 23:59:59 end) const now = new Date() const projectSprints = selectedProjectId ? sprints.filter((s) => s.projectId === selectedProjectId) @@ -316,12 +321,9 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) { ? tasks.filter((t) => t.projectId === selectedProjectId) : tasks - const currentSprint = projectSprints.find((s) => { - if (s.status !== "active") return false - const sprintStart = parseSprintStart(s.startDate) - const sprintEnd = parseSprintEnd(s.endDate) - return sprintStart <= now && sprintEnd >= now - }) + const currentSprint = + projectSprints.find((s) => s.status === "active" && isSprintInProgress(s.startDate, s.endDate, now)) ?? + projectSprints.find((s) => s.status !== "completed" && isSprintInProgress(s.startDate, s.endDate, now)) // Get other sprints (not current) const otherSprints = projectSprints.filter((s) => s.id !== currentSprint?.id) @@ -379,12 +381,14 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) { const handleCreateSprint = () => { if (!newSprint.name || !selectedProjectId) return + const startDate = newSprint.startDate || toLocalDateInputValue() + const endDate = newSprint.endDate || toLocalDateInputValue() addSprint({ name: newSprint.name, goal: newSprint.goal, - startDate: newSprint.startDate || toLocalDateInputValue(), - endDate: newSprint.endDate || toLocalDateInputValue(), - status: "planning", + startDate, + endDate, + status: inferSprintStatusForDateRange(startDate, endDate), projectId: selectedProjectId, }) diff --git a/src/components/SprintBoard.tsx b/src/components/SprintBoard.tsx index 5a3e9d4..71e6d18 100644 --- a/src/components/SprintBoard.tsx +++ b/src/components/SprintBoard.tsx @@ -23,7 +23,12 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Plus, Calendar, Flag, GripVertical } from "lucide-react" import { format, isValid } from "date-fns" -import { parseSprintEnd, parseSprintStart, toLocalDateInputValue } from "@/lib/utils" +import { + inferSprintStatusForDateRange, + parseSprintEnd, + parseSprintStart, + toLocalDateInputValue, +} from "@/lib/utils" const statusColumns = ["backlog", "in-progress", "review", "done"] as const type SprintColumnStatus = typeof statusColumns[number] @@ -207,12 +212,14 @@ export function SprintBoard() { const handleCreateSprint = () => { if (!newSprint.name || !selectedProjectId) return + const startDate = newSprint.startDate || toLocalDateInputValue() + const endDate = newSprint.endDate || toLocalDateInputValue() const sprint: Omit = { name: newSprint.name, goal: newSprint.goal, - startDate: newSprint.startDate || toLocalDateInputValue(), - endDate: newSprint.endDate || toLocalDateInputValue(), - status: "planning", + startDate, + endDate, + status: inferSprintStatusForDateRange(startDate, endDate), projectId: selectedProjectId, } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index cf8093e..cc015db 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -36,6 +36,26 @@ export function parseSprintEnd(endDate: string): Date { return parsed } +export function isSprintInProgress(startDate: string, endDate: string, now: Date = new Date()): boolean { + const sprintStart = parseSprintStart(startDate) + const sprintEnd = parseSprintEnd(endDate) + return sprintStart <= now && sprintEnd >= now +} + +export function inferSprintStatusForDateRange( + startDate: string, + endDate: string, + now: Date = new Date() +): "planning" | "active" | "completed" { + const sprintStart = parseSprintStart(startDate) + if (now < sprintStart) return "planning" + + const sprintEnd = parseSprintEnd(endDate) + if (now > sprintEnd) return "completed" + + return "active" +} + export function toLocalDateInputValue(date: Date = new Date()): string { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, "0")