'use client'; import { useEffect, useState, useCallback } from 'react'; import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Calendar, GripVertical, Plus } from 'lucide-react'; import { toast } from 'sonner'; import { TaskDetailPanel } from './task-detail-panel'; import { useRealtimeContext } from '@/components/realtime-provider'; interface Task { id: string; title: string; description?: string | null; status: 'todo' | 'in_progress' | 'done' | 'cancelled'; priority: 'low' | 'medium' | 'high' | 'urgent'; domainId: string; dueDate?: string | null; projectId?: string | null; sectionId?: string | null; parentId?: string | null; order: number; tags: { id: string; name: string; color: string | null }[]; completedAt?: string | null; estimatedMinutes?: number | null; } interface Domain { id: string; name: string; color: string | null; } const columns = [ { id: 'todo', title: 'To Do', color: 'bg-slate-500' }, { id: 'in_progress', title: 'In Progress', color: 'bg-blue-500' }, { id: 'done', title: 'Done', color: 'bg-green-500' }, { id: 'cancelled', title: 'Cancelled', color: 'bg-red-500' }, ]; const priorityColors: Record = { urgent: 'destructive', high: 'default', medium: 'secondary', low: 'secondary', }; function TaskCard({ task, domainName, domainColor, onClick, }: { task: Task; domainName: string; domainColor: string | null; onClick: () => void; }) { const subtaskCount = 0; // Will be populated from API return (
{task.title}
{task.priority} {domainColor && (
); } function DroppableColumn({ id, title, color, tasks, domainMap, onTaskClick, onAddTask, }: { id: string; title: string; color: string; tasks: Task[]; domainMap: Map; onTaskClick: (task: Task) => void; onAddTask: () => void; }) { return (
{tasks.length === 0 ? (

No tasks

) : ( tasks.map((task) => ( onTaskClick(task)} /> )) )}
); } export function TasksKanbanView({ domainId, onRefresh, }: { domainId: string; onRefresh?: () => void; }) { const [tasks, setTasks] = useState([]); const [domainMap, setDomainMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [selectedTask, setSelectedTask] = useState(null); const { subscribe } = useRealtimeContext(); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }) ); const fetchTasks = useCallback(async () => { if (!domainId) return; try { const response = await fetch(`/api/domains/${domainId}/tasks?sort=order&limit=200`); if (!response.ok) throw new Error('Unable to load tasks'); const data = await response.json(); setTasks(data.items || []); } catch (error) { console.error('Failed to fetch tasks:', error); toast.error('Unable to load tasks'); } finally { setLoading(false); } }, [domainId]); const fetchDomains = useCallback(async () => { try { const response = await fetch('/api/domains?sort=sort_order'); if (!response.ok) return; const data = await response.json(); const map = new Map(); for (const d of data.items || []) { map.set(d.id, { name: d.name, color: d.color || null }); } setDomainMap(map); } catch { // Non-critical } }, []); useEffect(() => { if (!domainId) return; setLoading(true); fetchTasks(); fetchDomains(); }, [domainId, fetchTasks, fetchDomains]); // Subscribe to realtime updates useEffect(() => { if (!domainId) return; const unsubscribe = subscribe(['task'], (event: any) => { if (event.type === 'task') { fetchTasks(); onRefresh?.(); } }); return unsubscribe; }, [domainId, subscribe, fetchTasks, onRefresh]); async function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over) return; const taskId = active.id as string; const newStatus = over.id as Task['status']; const task = tasks.find((t) => t.id === taskId); if (!task || task.status === newStatus) return; // Optimistic update setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t)) ); try { const response = await fetch(`/api/domains/${domainId}/tasks/${taskId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }), }); if (!response.ok) throw new Error('Unable to move task'); toast.success( `Moved "${task.title}" to ${columns.find((c) => c.id === newStatus)?.title}` ); } catch (error) { console.error('Failed to update task status:', error); toast.error(`Unable to move "${task.title}"`); fetchTasks(); // Revert } } function handleAddTask(status: string) { // Open create dialog with pre-filled status document.dispatchEvent( new CustomEvent('open-create-task', { detail: { status, domainId } }) ); } if (loading) { return

Loading tasks...

; } return ( <>
{columns.map((column) => ( t.status === column.id)} domainMap={domainMap} onTaskClick={setSelectedTask} onAddTask={() => handleAddTask(column.id)} /> ))}
{selectedTask && ( !open && setSelectedTask(null)} onUpdate={fetchTasks} /> )} ); }