Signed-off-by: Max <ai-agent@topdoglabs.com>
This commit is contained in:
parent
027496baf0
commit
97a8ab1a55
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user