'use client'; import { useParams } from 'next/navigation'; import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, closestCorners, } from '@dnd-kit/core'; import { useState, useMemo, useEffect } from 'react'; import { useEpics } from '@/lib/hooks/use-epics'; import { useStories } from '@/lib/hooks/use-stories'; import { useTasks } from '@/lib/hooks/use-tasks'; 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 { IssueCard } from '@/components/features/kanban/IssueCard'; import { CreateIssueDialog } from '@/components/features/issues/CreateIssueDialog'; import type { Epic, Story, Task } from '@/types/project'; 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' }, ]; // Unified work item type for Kanban type WorkItemType = 'Epic' | 'Story' | 'Task'; interface KanbanWorkItem { id: string; title: string; description?: string; // Optional to match API response status: string; priority: string; type: WorkItemType; // Epic properties projectId?: string; // Story properties epicId?: string; // Task properties storyId?: string; // Metadata estimatedHours?: number; actualHours?: number; ownerId?: string; createdAt?: string; updatedAt?: string; } export default function KanbanPage() { const params = useParams(); const projectId = params.id as string; const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [activeItem, setActiveItem] = useState(null); // Fetch Epic/Story/Task from ProjectManagement API const { data: epics, isLoading: epicsLoading } = useEpics(projectId); const { data: stories, isLoading: storiesLoading } = useStories(); const { data: tasks, isLoading: tasksLoading } = useTasks(); const isLoading = epicsLoading || storiesLoading || tasksLoading; // SignalR real-time updates const queryClient = useQueryClient(); const { isConnected } = useSignalRConnection(); // Subscribe to SignalR events for real-time updates (Simplified with useSignalREvents) useSignalREvents( { // Epic events (6 events) 'EpicCreated': (event: any) => { console.log('[Kanban] Epic created:', event); queryClient.invalidateQueries({ queryKey: ['epics', projectId] }); }, 'EpicUpdated': (event: any) => { console.log('[Kanban] Epic updated:', event); queryClient.invalidateQueries({ queryKey: ['epics', projectId] }); }, 'EpicDeleted': (event: any) => { console.log('[Kanban] Epic deleted:', event); queryClient.invalidateQueries({ queryKey: ['epics', projectId] }); }, // Story events (3 events) 'StoryCreated': (event: any) => { console.log('[Kanban] Story created:', event); queryClient.invalidateQueries({ queryKey: ['stories'] }); }, 'StoryUpdated': (event: any) => { console.log('[Kanban] Story updated:', event); queryClient.invalidateQueries({ queryKey: ['stories'] }); }, 'StoryDeleted': (event: any) => { console.log('[Kanban] Story deleted:', event); queryClient.invalidateQueries({ queryKey: ['stories'] }); }, // Task events (4 events) 'TaskCreated': (event: any) => { console.log('[Kanban] Task created:', event); queryClient.invalidateQueries({ queryKey: ['tasks'] }); }, 'TaskUpdated': (event: any) => { console.log('[Kanban] Task updated:', event); queryClient.invalidateQueries({ queryKey: ['tasks'] }); }, 'TaskDeleted': (event: any) => { console.log('[Kanban] Task deleted:', event); queryClient.invalidateQueries({ queryKey: ['tasks'] }); }, 'TaskAssigned': (event: any) => { console.log('[Kanban] Task assigned:', event); queryClient.invalidateQueries({ queryKey: ['tasks'] }); }, }, [projectId, queryClient] ); // Combine all work items into unified format const allWorkItems = useMemo(() => { const items: KanbanWorkItem[] = [ ...(epics || []).map((e) => ({ ...e, type: 'Epic' as const, })), ...(stories || []).map((s) => ({ ...s, type: 'Story' as const, })), ...(tasks || []).map((t) => ({ ...t, type: 'Task' as const, })), ]; return items; }, [epics, stories, tasks]); // Group work items by status const itemsByStatus = useMemo(() => ({ Backlog: allWorkItems.filter((i) => i.status === 'Backlog'), Todo: allWorkItems.filter((i) => i.status === 'Todo'), InProgress: allWorkItems.filter((i) => i.status === 'InProgress'), Done: allWorkItems.filter((i) => i.status === 'Done'), }), [allWorkItems]); const handleDragStart = (event: DragStartEvent) => { const item = allWorkItems.find((i) => i.id === event.active.id); setActiveItem(item || null); }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; setActiveItem(null); if (!over || active.id === over.id) return; const newStatus = over.id as string; const item = allWorkItems.find((i) => i.id === active.id); if (item && item.status !== newStatus) { // TODO: Implement status change mutation for Epic/Story/Task // For now, we'll skip the mutation as we need to implement these hooks console.log(`TODO: Change ${item.type} ${item.id} status to ${newStatus}`); } }; if (isLoading) { return (
); } return (

Kanban Board

Drag and drop to update issue status

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