diff --git a/ui/src/App.tsx b/ui/src/App.tsx index b4bf30c..f824207 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -49,6 +49,10 @@ export const App = () => { } /> + }> + + + } /> + ) => void; + onTaskUpdate: (taskId: string, updates: Partial) => void; + onTaskDelete: (taskId: string) => void; + isLoading?: boolean; +} + +export const KanbanBoard: React.FC = ({ + board, + onTaskCreate, + onTaskUpdate, + onTaskDelete, + isLoading = false, +}) => { + const [selectedTask, setSelectedTask] = useState(null); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showFilters, setShowFilters] = useState(false); + const [filters] = useState({}); + const [searchQuery, setSearchQuery] = useState(""); + + const handleCreateTask = useCallback(() => { + setShowCreateModal(true); + console.log({ onTaskCreate, onTaskUpdate, onTaskDelete, isLoading }); // TODO: Implement actual functionality + }, [onTaskCreate, onTaskUpdate, onTaskDelete, isLoading]); + + const handleTaskClick = useCallback((task: KanbanTask) => { + setSelectedTask(task); + }, []); + + // Filter tasks based on search and filters + const filteredBoard = React.useMemo(() => { + if (!searchQuery && Object.keys(filters).length === 0) { + return board; + } + + const filteredColumns = board.columns.map((column) => { + const filteredTasks = column.tasks.filter((task) => { + // Search filter + if (searchQuery) { + const searchLower = searchQuery.toLowerCase(); + if ( + !task.title.toLowerCase().includes(searchLower) && + !task.description?.toLowerCase().includes(searchLower) && + !task.assignee_name?.toLowerCase().includes(searchLower) + ) { + return false; + } + } + + // Assignee filter + if (filters.assignee && task.assignee_id !== filters.assignee) { + return false; + } + + // Priority filter + if (filters.priority && filters.priority.length > 0) { + if (!filters.priority.includes(task.priority)) { + return false; + } + } + + // Type filter + if (filters.type && filters.type.length > 0) { + if (!filters.type.includes(task.type)) { + return false; + } + } + + // Labels filter + if (filters.labels && filters.labels.length > 0) { + if ( + !task.labels || + !filters.labels.some((label) => task.labels?.includes(label)) + ) { + return false; + } + } + + return true; + }); + + return { + ...column, + tasks: filteredTasks, + }; + }); + + return { + ...board, + columns: filteredColumns, + }; + }, [board, searchQuery, filters]); + + const totalTasks = board.columns.reduce( + (sum, column) => sum + column.tasks.length, + 0 + ); + const filteredTasks = filteredBoard.columns.reduce( + (sum, column) => sum + column.tasks.length, + 0 + ); + + const getPriorityColor = (priority: string) => { + switch (priority) { + case "highest": + return "text-red-600"; + case "high": + return "text-orange-600"; + case "medium": + return "text-yellow-600"; + case "low": + return "text-green-600"; + case "lowest": + return "text-gray-600"; + default: + return "text-gray-600"; + } + }; + + const getTypeIcon = (type: string) => { + switch (type) { + case "bug": + return "🐛"; + case "story": + return "📖"; + case "task": + return "✓"; + case "epic": + return "⚡"; + case "subtask": + return "📝"; + default: + return "📋"; + } + }; + + return ( +
+ {/* Header */} +
+
+

+ {board.name} +

+
+ {filteredTasks} of {totalTasks} tasks +
+
+ +
+ + + + + + + +
+
+ + {/* Filters Panel */} + {showFilters && ( +
+
+ Filters will be implemented here +
+
+ )} + + {/* Board Content */} +
+
+ {filteredBoard.columns.map((column) => ( +
+ {/* Column Header */} +
+
+

+ {column.title} +

+ + {column.tasks.length} + +
+ +
+ + {/* Column Tasks */} +
+ {column.tasks.map((task) => ( +
handleTaskClick(task)} + > +
+
+ + {getTypeIcon(task.type)} + + + {task.priority.toUpperCase()} + +
+ {task.assignee_name && ( +
+ {task.assignee_name.charAt(0).toUpperCase()} +
+ )} +
+ +

+ {task.title} +

+ + {task.description && ( +

+ {task.description} +

+ )} + +
+
+ {task.labels && task.labels.length > 0 && ( +
+ {task.labels.slice(0, 2).map((label, index) => ( + + {label} + + ))} + {task.labels.length > 2 && ( + + +{task.labels.length - 2} + + )} +
+ )} +
+ + {task.story_points && ( + + {task.story_points} + + )} +
+
+ ))} +
+
+ ))} +
+
+ + {/* TODO: Add modals when components are created */} + {showCreateModal && ( +
+
+

Create Task

+

+ Task creation modal will be implemented here +

+
+ +
+
+
+ )} + + {selectedTask && ( +
+
+

{selectedTask.title}

+

+ {selectedTask.description || "No description"} +

+
+
+ Priority: {selectedTask.priority} +
+
+ Type: {selectedTask.type} +
+
+ Status: {selectedTask.status} +
+
+ Assignee:{" "} + {selectedTask.assignee_name || "Unassigned"} +
+
+
+ +
+
+
+ )} +
+ ); +}; diff --git a/ui/src/pages/kanban.tsx b/ui/src/pages/kanban.tsx new file mode 100644 index 0000000..c1745ac --- /dev/null +++ b/ui/src/pages/kanban.tsx @@ -0,0 +1,330 @@ +import React, { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { KanbanBoard } from "@ui/components/kanban/KanbanBoard"; +import { LoadingSpinner } from "@ui/components/LoadingSpinner"; +import { useTablosList } from "@ui/hooks/tablos"; +import { + KanbanBoard as KanbanBoardType, + KanbanTask, + KanbanColumn, + TaskStatus, +} from "@ui/types/kanban.types"; +import { + Select, + SelectButton, + SelectPopover, + SelectListBox, + SelectListItem, +} from "@ui/ui-library/select"; + +// Mock data for demonstration +const createMockKanbanBoard = ( + tabloId: string, + tabloName: string +): KanbanBoardType => { + const columns: KanbanColumn[] = [ + { + id: "backlog", + title: "Backlog", + status: "backlog" as TaskStatus, + position: 0, + tasks: [ + { + id: "1", + title: "Set up project structure", + description: "Create the initial project structure and configuration", + status: "backlog" as TaskStatus, + priority: "medium", + type: "task", + assignee_id: "user-1", + assignee_name: "John Doe", + reporter_id: "user-admin", + tablo_id: tabloId, + tablo_name: tabloName, + story_points: 3, + labels: ["setup", "infrastructure"], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + position: 0, + comments_count: 2, + attachments_count: 0, + }, + { + id: "2", + title: "Design user interface mockups", + description: + "Create wireframes and mockups for the main user interface", + status: "backlog" as TaskStatus, + priority: "high", + type: "story", + assignee_id: "user-2", + assignee_name: "Jane Smith", + reporter_id: "user-admin", + tablo_id: tabloId, + tablo_name: tabloName, + story_points: 5, + labels: ["design", "ui", "mockups"], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + position: 1, + comments_count: 0, + attachments_count: 1, + }, + ], + }, + { + id: "todo", + title: "To Do", + status: "todo" as TaskStatus, + position: 1, + tasks: [ + { + id: "3", + title: "Implement authentication system", + description: "Build login and registration functionality", + status: "todo" as TaskStatus, + priority: "highest", + type: "story", + assignee_id: "user-1", + assignee_name: "John Doe", + reporter_id: "user-admin", + tablo_id: tabloId, + tablo_name: tabloName, + story_points: 8, + labels: ["authentication", "security"], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + position: 0, + comments_count: 1, + attachments_count: 0, + }, + ], + }, + { + id: "in_progress", + title: "In Progress", + status: "in_progress" as TaskStatus, + position: 2, + tasks: [ + { + id: "4", + title: "Fix critical bug in payment processing", + description: + "Users are unable to complete payments due to a validation error", + status: "in_progress" as TaskStatus, + priority: "highest", + type: "bug", + assignee_id: "user-2", + assignee_name: "Jane Smith", + reporter_id: "user-3", + tablo_id: tabloId, + tablo_name: tabloName, + story_points: 2, + labels: ["critical", "payment", "bugfix"], + due_date: new Date(Date.now() + 86400000).toISOString(), // Tomorrow + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + position: 0, + comments_count: 5, + attachments_count: 2, + }, + ], + }, + { + id: "in_review", + title: "In Review", + status: "in_review" as TaskStatus, + position: 3, + tasks: [ + { + id: "5", + title: "Add search functionality to dashboard", + description: + "Implement search and filter capabilities for the main dashboard", + status: "in_review" as TaskStatus, + priority: "medium", + type: "story", + assignee_id: "user-1", + assignee_name: "John Doe", + reporter_id: "user-admin", + tablo_id: tabloId, + tablo_name: tabloName, + story_points: 5, + labels: ["search", "dashboard", "enhancement"], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + position: 0, + comments_count: 3, + attachments_count: 0, + }, + ], + }, + { + id: "done", + title: "Done", + status: "done" as TaskStatus, + position: 4, + tasks: [ + { + id: "6", + title: "Update project documentation", + description: "Update README and API documentation", + status: "done" as TaskStatus, + priority: "low", + type: "task", + assignee_id: "user-2", + assignee_name: "Jane Smith", + reporter_id: "user-admin", + tablo_id: tabloId, + tablo_name: tabloName, + story_points: 2, + labels: ["documentation"], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + position: 0, + comments_count: 1, + attachments_count: 0, + }, + ], + }, + ]; + + return { + id: "kanban-" + tabloId, + name: `${tabloName} Kanban Board`, + description: "Project task management board", + tablo_id: tabloId, + columns, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; +}; + +export const KanbanPage: React.FC = () => { + const { tablo_id } = useParams(); + const [selectedTabloId, setSelectedTabloId] = useState( + tablo_id || "all" + ); + const [kanbanBoard, setKanbanBoard] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // Fetch tablos + const { data: tablos, isLoading: tablosLoading } = useTablosList(); + + // Create mock kanban board based on selected tablo + useEffect(() => { + if (tablos && selectedTabloId !== "all") { + const selectedTablo = tablos.find((t) => t.id === selectedTabloId); + if (selectedTablo) { + const mockBoard = createMockKanbanBoard( + selectedTablo.id, + selectedTablo.name + ); + setKanbanBoard(mockBoard); + } + } else if (selectedTabloId === "all" && tablos && tablos.length > 0) { + // Create a combined board for all tablos + const mockBoard = createMockKanbanBoard("all", "All Projects"); + setKanbanBoard(mockBoard); + } + setIsLoading(false); + }, [selectedTabloId, tablos]); + + const handleTaskCreate = ( + task: Omit + ) => { + // TODO: Implement task creation + console.log("Create task:", task); + }; + + const handleTaskUpdate = (taskId: string, updates: Partial) => { + // TODO: Implement task updates + console.log("Update task:", taskId, updates); + }; + + const handleTaskDelete = (taskId: string) => { + // TODO: Implement task deletion + console.log("Delete task:", taskId); + }; + + if (tablosLoading || isLoading) { + return ( +
+ +
+ ); + } + + if (!kanbanBoard) { + return ( +
+

+ No Kanban Board +

+

+ Select a tablo to view its kanban board +

+ {tablos && tablos.length > 0 && ( + + )} +
+ ); + } + + return ( +
+ {/* Tablo Selector */} +
+
+ + +
+
+ + {/* Kanban Board */} +
+ +
+
+ ); +}; diff --git a/ui/src/types/kanban.types.ts b/ui/src/types/kanban.types.ts new file mode 100644 index 0000000..72dad04 --- /dev/null +++ b/ui/src/types/kanban.types.ts @@ -0,0 +1,153 @@ +export type Priority = "lowest" | "low" | "medium" | "high" | "highest"; +export type TaskStatus = + | "backlog" + | "todo" + | "in_progress" + | "in_review" + | "done"; +export type TaskType = "story" | "bug" | "task" | "epic" | "subtask"; + +export interface KanbanTask { + id: string; + title: string; + description?: string; + status: TaskStatus; + priority: Priority; + type: TaskType; + assignee_id?: string; + assignee_name?: string; + assignee_avatar?: string; + reporter_id: string; + reporter_name?: string; + tablo_id: string; + tablo_name?: string; + story_points?: number; + labels?: string[]; + due_date?: string; + created_at: string; + updated_at: string; + position: number; + parent_task_id?: string; // For subtasks + comments_count?: number; + attachments_count?: number; +} + +export interface KanbanColumn { + id: string; + title: string; + status: TaskStatus; + position: number; + color?: string; + task_limit?: number; + tasks: KanbanTask[]; +} + +export interface KanbanBoard { + id: string; + name: string; + description?: string; + tablo_id: string; + columns: KanbanColumn[]; + created_at: string; + updated_at: string; +} + +export interface TaskComment { + id: string; + task_id: string; + user_id: string; + user_name: string; + user_avatar?: string; + content: string; + created_at: string; + updated_at: string; +} + +export interface TaskAttachment { + id: string; + task_id: string; + filename: string; + file_url: string; + file_size: number; + file_type: string; + uploaded_by: string; + uploaded_at: string; +} + +// Insert types for creating new items +export interface KanbanTaskInsert { + title: string; + description?: string; + priority: Priority; + type: TaskType; + assignee_id?: string; + tablo_id: string; + story_points?: number; + labels?: string[]; + due_date?: string; + position: number; + parent_task_id?: string; +} + +export interface KanbanTaskUpdate { + title?: string; + description?: string; + status?: TaskStatus; + priority?: Priority; + type?: TaskType; + assignee_id?: string; + story_points?: number; + labels?: string[]; + due_date?: string; + position?: number; + parent_task_id?: string; +} + +export interface KanbanColumnUpdate { + title?: string; + position?: number; + color?: string; + task_limit?: number; +} + +// UI-specific types for drag and drop +export interface DragItem { + id: string; + type: "task" | "column"; + sourceColumnId: string; + sourceIndex: number; +} + +export interface DropResult { + draggedId: string; + source: { + columnId: string; + index: number; + }; + destination: { + columnId: string; + index: number; + }; +} + +// Filter and sort types +export interface KanbanFilters { + assignee?: string; + priority?: Priority[]; + type?: TaskType[]; + labels?: string[]; + search?: string; +} + +export interface KanbanSort { + field: "priority" | "created_at" | "updated_at" | "due_date" | "story_points"; + direction: "asc" | "desc"; +} + +// User type for assignee selection +export interface KanbanUser { + id: string; + name: string; + email: string; + avatar_url?: string; +}