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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { Label } from "@/components/ui/label"
|
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 { generateAvatarDataUrl } from "@/lib/avatar"
|
||||||
import {
|
import {
|
||||||
blobFromDataUrl,
|
blobFromDataUrl,
|
||||||
@ -609,15 +609,11 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}, [selectedTask])
|
}, [selectedTask])
|
||||||
|
|
||||||
// Get current active sprint (across all projects)
|
// Get current sprint (across all projects) using local-day boundaries.
|
||||||
// Treat end date as end-of-day (23:59:59) to handle timezone issues
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const currentSprint = sprints.find((s) => {
|
const currentSprint =
|
||||||
if (s.status !== 'active') return false
|
sprints.find((s) => s.status === "active" && isSprintInProgress(s.startDate, s.endDate, now)) ??
|
||||||
const sprintStart = parseSprintStart(s.startDate)
|
sprints.find((s) => s.status !== "completed" && isSprintInProgress(s.startDate, s.endDate, now))
|
||||||
const sprintEnd = parseSprintEnd(s.endDate)
|
|
||||||
return sprintStart <= now && sprintEnd >= now
|
|
||||||
})
|
|
||||||
|
|
||||||
// Filter tasks to only show current sprint tasks in Kanban (from ALL projects)
|
// Filter tasks to only show current sprint tasks in Kanban (from ALL projects)
|
||||||
// Sort by updatedAt descending (latest first)
|
// Sort by updatedAt descending (latest first)
|
||||||
@ -643,7 +639,7 @@ export default function Home() {
|
|||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const endedSprints = sprints.filter((s) => {
|
const endedSprints = sprints.filter((s) => {
|
||||||
if (s.status !== 'active') return false
|
if (s.status === "completed") return false
|
||||||
const sprintEnd = parseSprintEnd(s.endDate)
|
const sprintEnd = parseSprintEnd(s.endDate)
|
||||||
return sprintEnd < now
|
return sprintEnd < now
|
||||||
})
|
})
|
||||||
@ -652,7 +648,7 @@ export default function Home() {
|
|||||||
|
|
||||||
// Find next sprint (earliest start date that's in the future or active)
|
// Find next sprint (earliest start date that's in the future or active)
|
||||||
const nextSprint = sprints
|
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]
|
.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime())[0]
|
||||||
|
|
||||||
if (!nextSprint) return
|
if (!nextSprint) return
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
import { parseSprintEnd, parseSprintStart } from "@/lib/utils"
|
import { inferSprintStatusForDateRange, parseSprintEnd, parseSprintStart } from "@/lib/utils"
|
||||||
|
|
||||||
interface Sprint {
|
interface Sprint {
|
||||||
id: string
|
id: string
|
||||||
@ -46,6 +46,56 @@ interface SprintDetail {
|
|||||||
rolledOverTasks: Task[]
|
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 {
|
function formatDateRange(startDate: string, endDate: string): string {
|
||||||
const start = parseSprintStart(startDate)
|
const start = parseSprintStart(startDate)
|
||||||
const end = parseSprintEnd(endDate)
|
const end = parseSprintEnd(endDate)
|
||||||
@ -93,20 +143,21 @@ export default function SprintArchivePage() {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
// Fetch completed sprints
|
// Fetch normalized sprint + task data from a single endpoint.
|
||||||
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
|
|
||||||
const tasksRes = await fetch("/api/tasks")
|
const tasksRes = await fetch("/api/tasks")
|
||||||
const tasksData = await tasksRes.json()
|
const tasksData = await tasksRes.json()
|
||||||
const allTasks: Task[] = tasksData.tasks || []
|
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
|
// 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 sprintTasks = allTasks.filter((t) => t.sprintId === sprint.id)
|
||||||
const completedTasks = sprintTasks.filter(
|
const completedTasks = sprintTasks.filter(
|
||||||
(t) => t.status === "done" || t.status === "archived"
|
(t) => t.status === "done" || t.status === "archived"
|
||||||
@ -152,7 +203,8 @@ export default function SprintArchivePage() {
|
|||||||
const sprintData = await sprintRes.json()
|
const sprintData = await sprintRes.json()
|
||||||
const tasksData = await tasksRes.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 allTasks: Task[] = tasksData.tasks || []
|
||||||
const sprintTasks = allTasks.filter((t) => t.sprintId === sprintId)
|
const sprintTasks = allTasks.filter((t) => t.sprintId === sprintId)
|
||||||
const completedTasks = sprintTasks.filter(
|
const completedTasks = sprintTasks.filter(
|
||||||
|
|||||||
@ -25,7 +25,13 @@ import { Badge } from "@/components/ui/badge"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Plus, GripVertical, ChevronDown, ChevronRight, Calendar } from "lucide-react"
|
import { Plus, GripVertical, ChevronDown, ChevronRight, Calendar } from "lucide-react"
|
||||||
import { format, isValid } from "date-fns"
|
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"
|
import { generateAvatarDataUrl } from "@/lib/avatar"
|
||||||
|
|
||||||
const priorityColors: Record<string, string> = {
|
const priorityColors: Record<string, string> = {
|
||||||
@ -306,8 +312,7 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get current active sprint
|
// Get current sprint using local-day boundaries (00:00 start, 23:59:59 end)
|
||||||
// Treat end date as end-of-day (23:59:59) to handle timezone issues
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectSprints = selectedProjectId
|
const projectSprints = selectedProjectId
|
||||||
? sprints.filter((s) => s.projectId === selectedProjectId)
|
? sprints.filter((s) => s.projectId === selectedProjectId)
|
||||||
@ -316,12 +321,9 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) {
|
|||||||
? tasks.filter((t) => t.projectId === selectedProjectId)
|
? tasks.filter((t) => t.projectId === selectedProjectId)
|
||||||
: tasks
|
: tasks
|
||||||
|
|
||||||
const currentSprint = projectSprints.find((s) => {
|
const currentSprint =
|
||||||
if (s.status !== "active") return false
|
projectSprints.find((s) => s.status === "active" && isSprintInProgress(s.startDate, s.endDate, now)) ??
|
||||||
const sprintStart = parseSprintStart(s.startDate)
|
projectSprints.find((s) => s.status !== "completed" && isSprintInProgress(s.startDate, s.endDate, now))
|
||||||
const sprintEnd = parseSprintEnd(s.endDate)
|
|
||||||
return sprintStart <= now && sprintEnd >= now
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get other sprints (not current)
|
// Get other sprints (not current)
|
||||||
const otherSprints = projectSprints.filter((s) => s.id !== currentSprint?.id)
|
const otherSprints = projectSprints.filter((s) => s.id !== currentSprint?.id)
|
||||||
@ -379,12 +381,14 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) {
|
|||||||
const handleCreateSprint = () => {
|
const handleCreateSprint = () => {
|
||||||
if (!newSprint.name || !selectedProjectId) return
|
if (!newSprint.name || !selectedProjectId) return
|
||||||
|
|
||||||
|
const startDate = newSprint.startDate || toLocalDateInputValue()
|
||||||
|
const endDate = newSprint.endDate || toLocalDateInputValue()
|
||||||
addSprint({
|
addSprint({
|
||||||
name: newSprint.name,
|
name: newSprint.name,
|
||||||
goal: newSprint.goal,
|
goal: newSprint.goal,
|
||||||
startDate: newSprint.startDate || toLocalDateInputValue(),
|
startDate,
|
||||||
endDate: newSprint.endDate || toLocalDateInputValue(),
|
endDate,
|
||||||
status: "planning",
|
status: inferSprintStatusForDateRange(startDate, endDate),
|
||||||
projectId: selectedProjectId,
|
projectId: selectedProjectId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,12 @@ import { Badge } from "@/components/ui/badge"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Plus, Calendar, Flag, GripVertical } from "lucide-react"
|
import { Plus, Calendar, Flag, GripVertical } from "lucide-react"
|
||||||
import { format, isValid } from "date-fns"
|
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
|
const statusColumns = ["backlog", "in-progress", "review", "done"] as const
|
||||||
type SprintColumnStatus = typeof statusColumns[number]
|
type SprintColumnStatus = typeof statusColumns[number]
|
||||||
@ -207,12 +212,14 @@ export function SprintBoard() {
|
|||||||
const handleCreateSprint = () => {
|
const handleCreateSprint = () => {
|
||||||
if (!newSprint.name || !selectedProjectId) return
|
if (!newSprint.name || !selectedProjectId) return
|
||||||
|
|
||||||
|
const startDate = newSprint.startDate || toLocalDateInputValue()
|
||||||
|
const endDate = newSprint.endDate || toLocalDateInputValue()
|
||||||
const sprint: Omit<Sprint, "id" | "createdAt"> = {
|
const sprint: Omit<Sprint, "id" | "createdAt"> = {
|
||||||
name: newSprint.name,
|
name: newSprint.name,
|
||||||
goal: newSprint.goal,
|
goal: newSprint.goal,
|
||||||
startDate: newSprint.startDate || toLocalDateInputValue(),
|
startDate,
|
||||||
endDate: newSprint.endDate || toLocalDateInputValue(),
|
endDate,
|
||||||
status: "planning",
|
status: inferSprintStatusForDateRange(startDate, endDate),
|
||||||
projectId: selectedProjectId,
|
projectId: selectedProjectId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,26 @@ export function parseSprintEnd(endDate: string): Date {
|
|||||||
return parsed
|
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 {
|
export function toLocalDateInputValue(date: Date = new Date()): string {
|
||||||
const year = date.getFullYear()
|
const year = date.getFullYear()
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0")
|
const month = String(date.getMonth() + 1).padStart(2, "0")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user