'use client'; import { useParams } from 'next/navigation'; import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, closestCorners, } from '@dnd-kit/core'; import { useState, useMemo } from 'react'; import { useProjectStories } from '@/lib/hooks/use-stories'; import { useEpics } from '@/lib/hooks/use-epics'; import { useChangeStoryStatus } from '@/lib/hooks/use-stories'; import { useSignalREvents, useSignalRConnection } from '@/lib/signalr/SignalRContext'; import { useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Plus, Loader2 } from 'lucide-react'; import { KanbanColumn } from '@/components/features/kanban/KanbanColumn'; import { StoryCard } from '@/components/features/kanban/StoryCard'; import { CreateStoryDialog } from '@/components/features/stories/CreateStoryDialog'; import type { Story, WorkItemStatus } from '@/types/project'; import { logger } from '@/lib/utils/logger'; const COLUMNS = [ { id: 'Backlog', title: 'Backlog', color: 'bg-gray-100' }, { id: 'Todo', title: 'To Do', color: 'bg-blue-100' }, { id: 'InProgress', title: 'In Progress', color: 'bg-yellow-100' }, { id: 'Done', title: 'Done', color: 'bg-green-100' }, ]; export default function KanbanPage() { const params = useParams(); const projectId = params.id as string; const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [activeStory, setActiveStory] = useState(null); // Fetch all stories for the project and epics for name mapping const { data: stories = [], isLoading: storiesLoading } = useProjectStories(projectId); const { data: epics = [], isLoading: epicsLoading } = useEpics(projectId); const isLoading = storiesLoading || epicsLoading; // SignalR real-time updates const queryClient = useQueryClient(); const { isConnected } = useSignalRConnection(); const changeStatusMutation = useChangeStoryStatus(); // Subscribe to SignalR events for real-time updates useSignalREvents( { // Story events (3 events) 'StoryCreated': (event: any) => { logger.debug('[Kanban] Story created:', event); queryClient.invalidateQueries({ queryKey: ['project-stories', projectId] }); }, 'StoryUpdated': (event: any) => { logger.debug('[Kanban] Story updated:', event); queryClient.invalidateQueries({ queryKey: ['project-stories', projectId] }); }, 'StoryDeleted': (event: any) => { logger.debug('[Kanban] Story deleted:', event); queryClient.invalidateQueries({ queryKey: ['project-stories', projectId] }); }, }, [projectId, queryClient] ); // Create epic name mapping for displaying in story cards const epicNames = useMemo(() => { const nameMap: Record = {}; epics.forEach((epic) => { nameMap[epic.id] = epic.name; }); return nameMap; }, [epics]); // Group stories by status const storiesByStatus = useMemo(() => ({ Backlog: stories.filter((s) => s.status === 'Backlog'), Todo: stories.filter((s) => s.status === 'Todo'), InProgress: stories.filter((s) => s.status === 'InProgress'), Done: stories.filter((s) => s.status === 'Done'), }), [stories]); const handleDragStart = (event: DragStartEvent) => { const story = stories.find((s) => s.id === event.active.id); setActiveStory(story || null); }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; setActiveStory(null); if (!over || active.id === over.id) return; const newStatus = over.id as WorkItemStatus; const story = stories.find((s) => s.id === active.id); if (story && story.status !== newStatus) { logger.debug(`[Kanban] Changing story ${story.id} status to ${newStatus}`); changeStatusMutation.mutate({ id: story.id, status: newStatus }); } }; if (isLoading) { return (
); } return (

Kanban Board

Drag and drop to update story status

{COLUMNS.map((column) => ( ))}
{activeStory && ( )}
); }