diff --git a/src/app/page.tsx b/src/app/page.tsx index b803cd7..8fa4759 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -359,6 +359,7 @@ export default function Home() { setCurrentUser, syncFromServer, isLoading, + updateSprint, } = useTaskStore() const [newTaskOpen, setNewTaskOpen] = useState(false) @@ -583,15 +584,17 @@ 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 const now = new Date() - const currentSprint = sprints.find((s) => - s.status === 'active' && - new Date(s.startDate) <= now && - new Date(s.endDate) >= now - ) + const currentSprint = sprints.find((s) => { + if (s.status !== 'active') return false + const sprintEnd = new Date(s.endDate) + sprintEnd.setHours(23, 59, 59, 999) + return new Date(s.startDate) <= now && sprintEnd >= now + }) // Filter tasks to only show current sprint tasks in Kanban (from ALL projects) - const sprintTasks = currentSprint + const sprintTasks = currentSprint ? tasks.filter((t) => { if (t.sprintId !== currentSprint.id) return false // Apply search filter @@ -604,6 +607,51 @@ export default function Home() { return true }) : [] + + // Auto-rollover: Move incomplete tasks from ended sprints to next sprint + useEffect(() => { + if (!authReady || sprints.length === 0) return + + const now = new Date() + const endedSprints = sprints.filter((s) => { + if (s.status !== 'active') return false + const sprintEnd = new Date(s.endDate) + sprintEnd.setHours(23, 59, 59, 999) + return sprintEnd < now + }) + + if (endedSprints.length === 0) return + + // 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))) + .sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime())[0] + + if (!nextSprint) return + + // Process each ended sprint + endedSprints.forEach((endedSprint) => { + const incompleteTasks = tasks.filter( + (t) => t.sprintId === endedSprint.id && t.status !== 'done' && t.status !== 'canceled' && t.status !== 'archived' + ) + + if (incompleteTasks.length > 0) { + console.log(`Auto-rolling over ${incompleteTasks.length} tasks from "${endedSprint.name}" to "${nextSprint.name}"`) + + // Move incomplete tasks to next sprint + incompleteTasks.forEach((task) => { + updateTask(task.id, { sprintId: nextSprint.id }) + }) + + // Mark ended sprint as completed + updateSprint(endedSprint.id, { status: 'completed' }) + } else { + // No incomplete tasks, just mark as completed + updateSprint(endedSprint.id, { status: 'completed' }) + } + }) + }, [authReady, sprints, tasks, updateTask, updateSprint]) + const activeKanbanTask = activeKanbanTaskId ? sprintTasks.find((task) => task.id === activeKanbanTaskId) : null diff --git a/src/components/BacklogView.tsx b/src/components/BacklogView.tsx index 31635f8..6aa5fd7 100644 --- a/src/components/BacklogView.tsx +++ b/src/components/BacklogView.tsx @@ -306,13 +306,14 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) { ) // Get current active sprint + // Treat end date as end-of-day (23:59:59) to handle timezone issues const now = new Date() - const currentSprint = sprints.find( - (s) => - s.status === "active" && - new Date(s.startDate) <= now && - new Date(s.endDate) >= now - ) + const currentSprint = sprints.find((s) => { + if (s.status !== "active") return false + const sprintEnd = new Date(s.endDate) + sprintEnd.setHours(23, 59, 59, 999) + return new Date(s.startDate) <= now && sprintEnd >= now + }) // Get other sprints (not current) const otherSprints = sprints.filter((s) => s.id !== currentSprint?.id)