import { useState } from 'react' import { CustomValueResponse } from '../services/customFields' interface Task { id: string project_id: string title: string description: string | null priority: string status_id: string | null status_name: string | null status_color: string | null assignee_id: string | null assignee_name: string | null due_date: string | null start_date: string | null time_estimate: number | null subtask_count: number custom_values?: CustomValueResponse[] } interface TaskStatus { id: string name: string color: string is_done: boolean } interface KanbanBoardProps { tasks: Task[] statuses: TaskStatus[] onStatusChange: (taskId: string, statusId: string) => void onTaskClick: (task: Task) => void } export function KanbanBoard({ tasks, statuses, onStatusChange, onTaskClick, }: KanbanBoardProps) { const [draggedTaskId, setDraggedTaskId] = useState(null) const [dragOverColumnId, setDragOverColumnId] = useState(null) // Group tasks by status const tasksByStatus: Record = {} statuses.forEach((status) => { tasksByStatus[status.id] = tasks.filter((task) => task.status_id === status.id) }) // Tasks without status const unassignedTasks = tasks.filter((task) => !task.status_id) const handleDragStart = (e: React.DragEvent, taskId: string) => { setDraggedTaskId(taskId) e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/plain', taskId) // Add a slight delay to allow the drag image to be captured const target = e.target as HTMLElement setTimeout(() => { target.style.opacity = '0.5' }, 0) } const handleDragEnd = (e: React.DragEvent) => { const target = e.target as HTMLElement target.style.opacity = '1' setDraggedTaskId(null) setDragOverColumnId(null) } const handleDragOver = (e: React.DragEvent, statusId: string) => { e.preventDefault() e.dataTransfer.dropEffect = 'move' if (dragOverColumnId !== statusId) { setDragOverColumnId(statusId) } } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() setDragOverColumnId(null) } const handleDrop = (e: React.DragEvent, statusId: string) => { e.preventDefault() const taskId = e.dataTransfer.getData('text/plain') if (taskId && draggedTaskId) { const task = tasks.find((t) => t.id === taskId) if (task && task.status_id !== statusId) { onStatusChange(taskId, statusId) } } setDraggedTaskId(null) setDragOverColumnId(null) } const getPriorityColor = (priority: string): string => { const colors: Record = { low: '#808080', medium: '#0066cc', high: '#ff9800', urgent: '#f44336', } return colors[priority] || colors.medium } const renderTaskCard = (task: Task) => (
handleDragStart(e, task.id)} onDragEnd={handleDragEnd} onClick={() => onTaskClick(task)} >
{task.title}
{task.description && (
{task.description.length > 80 ? task.description.substring(0, 80) + '...' : task.description}
)}
{task.assignee_name && ( {task.assignee_name} )} {task.due_date && ( {new Date(task.due_date).toLocaleDateString()} )} {task.subtask_count > 0 && ( {task.subtask_count} subtasks )} {/* Display custom field values (limit to first 2 for compact display) */} {task.custom_values?.slice(0, 2).map((cv) => ( {cv.field_name}: {cv.display_value || cv.value || '-'} ))}
) return (
{/* Unassigned column (if there are tasks without status) */} {unassignedTasks.length > 0 && (
No Status {unassignedTasks.length}
{unassignedTasks.map(renderTaskCard)}
)} {/* Status columns */} {statuses.map((status) => (
handleDragOver(e, status.id)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, status.id)} >
{status.name} {tasksByStatus[status.id]?.length || 0}
{tasksByStatus[status.id]?.map(renderTaskCard)} {(!tasksByStatus[status.id] || tasksByStatus[status.id].length === 0) && (
Drop tasks here
)}
))}
) } const styles: Record = { board: { display: 'flex', gap: '16px', overflowX: 'auto', paddingBottom: '16px', minHeight: '500px', }, column: { flex: '0 0 280px', backgroundColor: '#f5f5f5', borderRadius: '8px', display: 'flex', flexDirection: 'column', maxHeight: 'calc(100vh - 200px)', transition: 'background-color 0.2s ease', }, columnDragOver: { backgroundColor: '#e3f2fd', boxShadow: 'inset 0 0 0 2px #0066cc', }, columnHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', borderRadius: '8px 8px 0 0', color: 'white', fontWeight: 600, }, columnTitle: { fontSize: '14px', }, taskCount: { fontSize: '12px', backgroundColor: 'rgba(255, 255, 255, 0.3)', padding: '2px 8px', borderRadius: '10px', }, taskList: { flex: 1, padding: '12px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '8px', }, taskCard: { backgroundColor: 'white', borderRadius: '6px', padding: '12px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)', cursor: 'grab', borderLeft: '4px solid', transition: 'box-shadow 0.2s ease, transform 0.2s ease', }, taskTitle: { fontSize: '14px', fontWeight: 500, marginBottom: '6px', lineHeight: 1.4, }, taskDescription: { fontSize: '12px', color: '#666', marginBottom: '8px', lineHeight: 1.4, }, taskMeta: { display: 'flex', flexWrap: 'wrap', gap: '6px', fontSize: '11px', }, assigneeBadge: { backgroundColor: '#e3f2fd', color: '#1565c0', padding: '2px 6px', borderRadius: '4px', }, dueDate: { color: '#666', }, subtaskBadge: { color: '#767676', // WCAG AA compliant }, customValueBadge: { backgroundColor: '#f3e5f5', color: '#7b1fa2', padding: '2px 6px', borderRadius: '4px', fontSize: '10px', maxWidth: '100px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, emptyColumn: { textAlign: 'center', padding: '24px', color: '#767676', // WCAG AA compliant fontSize: '13px', border: '2px dashed #ddd', borderRadius: '6px', }, } export default KanbanBoard