Signed-off-by: Max <ai-agent@topdoglabs.com>

This commit is contained in:
Max 2026-02-23 07:43:18 -06:00
parent 027496baf0
commit 97a8ab1a55
5 changed files with 117 additions and 38 deletions

View File

@ -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

View File

@ -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<Sprint> & {
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(

View File

@ -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<string, string> = {
@ -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,
})

View File

@ -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<Sprint, "id" | "createdAt"> = {
name: newSprint.name,
goal: newSprint.goal,
startDate: newSprint.startDate || toLocalDateInputValue(),
endDate: newSprint.endDate || toLocalDateInputValue(),
status: "planning",
startDate,
endDate,
status: inferSprintStatusForDateRange(startDate, endDate),
projectId: selectedProjectId,
}

View File

@ -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")