'use client'; import { useEffect, useState } from 'react'; import { KeyboardSensor, DndContext, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, useSensor, useSensors, useDraggable, useDroppable } from '@dnd-kit/core'; import { sortableKeyboardCoordinates } from '@dnd-kit/sortable'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Calendar, GripVertical } from 'lucide-react'; import { toast } from 'sonner'; import { TaskDetailPanel } from './task-detail-panel'; interface Task { id: string; title: string; description?: string; status: 'todo' | 'in_progress' | 'done'; priority: 'low' | 'medium' | 'high' | 'urgent'; domain: string; due_date?: string; project_id?: string; tags: string[]; } interface Domain { id: string; name: string; color: string; } 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' } ]; function DraggableTask({ task, domainName, onClick, onStatusChange }: { task: Task; domainName: string; onClick: () => void; onStatusChange: (task: Task, status: Task['status']) => void; }) { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: task.id, data: { task } }); const style = transform ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)` } : undefined; return (
{task.priority} {domainName && ( {domainName} )} {task.due_date && ( {new Date(task.due_date).toLocaleDateString()} )}
); } function DroppableColumn({ id, title, color, tasks, domainMap, onTaskClick, onStatusChange }: { id: string; title: string; color: string; tasks: Task[]; domainMap: Map; onTaskClick: (task: Task) => void; onStatusChange: (task: Task, status: Task['status']) => void; }) { const { setNodeRef, isOver } = useDroppable({ id }); return (
{tasks.map((task) => ( onTaskClick(task)} onStatusChange={onStatusChange} /> ))}
); } export function TasksKanbanView() { const [tasks, setTasks] = useState([]); const [domainMap, setDomainMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [activeTask, setActiveTask] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); useEffect(() => { fetchTasks(); fetchDomains(); }, []); async function fetchTasks() { try { const response = await fetch('/api/tasks?sort=-created'); 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); } } async function fetchDomains() { 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, d.name); } setDomainMap(map); } catch { // Non-critical — domains will show as raw IDs } } async function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over) { setActiveTask(null); return; } const task = active.data.current?.task as Task; const newStatus = over.id as Task['status']; if (task.status !== newStatus) { await updateTaskStatus(task, newStatus); } setActiveTask(null); } function handleDragStart(event: DragStartEvent) { const task = event.active.data.current?.task as Task; setActiveTask(task); } async function updateTaskStatus(task: Task, status: Task['status']) { if (task.status === status) return; try { const response = await fetch(`/api/tasks/${task.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (!response.ok) throw new Error('Unable to move task'); await fetchTasks(); toast.success( `Moved ${task.title} to ${columns.find((column) => column.id === status)?.title}` ); } catch (error) { console.error('Failed to update task status:', error); toast.error(`Unable to move ${task.title}`); } } if (loading) { return

Loading tasks...

; } if (tasks.length === 0) { return (

No tasks yet. Create your first task to get started!

); } return ( <> setActiveTask(null)} >
{columns.map((column) => ( t.status === column.id)} domainMap={domainMap} onTaskClick={setSelectedTask} onStatusChange={updateTaskStatus} /> ))}
{activeTask ? (

{activeTask.title}

) : null}
{selectedTask && ( !open && setSelectedTask(null)} onUpdate={fetchTasks} /> )} ); }