Add kanban
This commit is contained in:
parent
fc4d9ca6de
commit
a54059bf12
4 changed files with 846 additions and 0 deletions
|
|
@ -49,6 +49,10 @@ export const App = () => {
|
|||
<Route path=":tablo_id" />
|
||||
<Route path="create" element={<CreateEventModal />} />
|
||||
</Route>
|
||||
<Route path="kanban" element={<NotFoundPage />}>
|
||||
<Route index />
|
||||
<Route path=":tablo_id" />
|
||||
</Route>
|
||||
<Route path="chantiers" element={<ChantiersPage />} />
|
||||
<Route
|
||||
path="chat"
|
||||
|
|
|
|||
359
ui/src/components/kanban/KanbanBoard.tsx
Normal file
359
ui/src/components/kanban/KanbanBoard.tsx
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
import React, { useState, useCallback } from "react";
|
||||
import { Plus, Filter } from "lucide-react";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { Icon } from "@ui/ui-library/icon";
|
||||
import { SearchField } from "@ui/ui-library/search-field";
|
||||
import { SearchInput } from "@ui/ui-library/search-field";
|
||||
import {
|
||||
KanbanBoard as KanbanBoardType,
|
||||
KanbanTask,
|
||||
KanbanFilters as KanbanFiltersType,
|
||||
} from "@ui/types/kanban.types";
|
||||
|
||||
interface KanbanBoardProps {
|
||||
board: KanbanBoardType;
|
||||
onTaskCreate: (
|
||||
task: Omit<KanbanTask, "id" | "created_at" | "updated_at">
|
||||
) => void;
|
||||
onTaskUpdate: (taskId: string, updates: Partial<KanbanTask>) => void;
|
||||
onTaskDelete: (taskId: string) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
||||
board,
|
||||
onTaskCreate,
|
||||
onTaskUpdate,
|
||||
onTaskDelete,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const [selectedTask, setSelectedTask] = useState<KanbanTask | null>(null);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [filters] = useState<KanbanFiltersType>({});
|
||||
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 (
|
||||
<div className="flex flex-col h-full bg-gray-50 dark:bg-gray-900">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{board.name}
|
||||
</h1>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{filteredTasks} of {totalTasks} tasks
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<SearchField
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
className="w-64"
|
||||
>
|
||||
<SearchInput placeholder="Search tasks..." />
|
||||
</SearchField>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onPress={() => setShowFilters(!showFilters)}
|
||||
className="relative"
|
||||
>
|
||||
<Icon>
|
||||
<Filter className="w-4 h-4" />
|
||||
</Icon>
|
||||
{Object.keys(filters).length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full"></span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onPress={() => handleCreateTask()}>
|
||||
<Icon>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Icon>
|
||||
Create Task
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Panel */}
|
||||
{showFilters && (
|
||||
<div className="p-4 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Filters will be implemented here
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Board Content */}
|
||||
<div className="flex-1 overflow-x-auto overflow-y-hidden">
|
||||
<div className="flex h-full p-4 space-x-4 min-w-max">
|
||||
{filteredBoard.columns.map((column) => (
|
||||
<div
|
||||
key={column.id}
|
||||
className="flex flex-col w-80 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
{/* Column Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||
{column.title}
|
||||
</h3>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
|
||||
{column.tasks.length}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onPress={() => handleCreateTask()}
|
||||
>
|
||||
<Icon>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Icon>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Column Tasks */}
|
||||
<div className="flex-1 p-4 space-y-3 overflow-y-auto">
|
||||
{column.tasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 hover:shadow-md transition-shadow cursor-pointer"
|
||||
onClick={() => handleTaskClick(task)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm">
|
||||
{getTypeIcon(task.type)}
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs font-medium ${getPriorityColor(
|
||||
task.priority
|
||||
)}`}
|
||||
>
|
||||
{task.priority.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
{task.assignee_name && (
|
||||
<div className="w-6 h-6 bg-gray-300 dark:bg-gray-600 rounded-full flex items-center justify-center text-xs font-medium">
|
||||
{task.assignee_name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-1 line-clamp-2">
|
||||
{task.title}
|
||||
</h4>
|
||||
|
||||
{task.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{task.labels && task.labels.length > 0 && (
|
||||
<div className="flex space-x-1">
|
||||
{task.labels.slice(0, 2).map((label, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
{task.labels.length > 2 && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
+{task.labels.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{task.story_points && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 bg-gray-200 dark:bg-gray-600 px-2 py-1 rounded">
|
||||
{task.story_points}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TODO: Add modals when components are created */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg max-w-md w-full mx-4">
|
||||
<h2 className="text-xl font-bold mb-4">Create Task</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Task creation modal will be implemented here
|
||||
</p>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onPress={() => setShowCreateModal(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTask && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg max-w-2xl w-full mx-4">
|
||||
<h2 className="text-xl font-bold mb-4">{selectedTask.title}</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
{selectedTask.description || "No description"}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<strong>Priority:</strong> {selectedTask.priority}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Type:</strong> {selectedTask.type}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Status:</strong> {selectedTask.status}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Assignee:</strong>{" "}
|
||||
{selectedTask.assignee_name || "Unassigned"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" onPress={() => setSelectedTask(null)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
330
ui/src/pages/kanban.tsx
Normal file
330
ui/src/pages/kanban.tsx
Normal file
|
|
@ -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<string>(
|
||||
tablo_id || "all"
|
||||
);
|
||||
const [kanbanBoard, setKanbanBoard] = useState<KanbanBoardType | null>(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<KanbanTask, "id" | "created_at" | "updated_at">
|
||||
) => {
|
||||
// TODO: Implement task creation
|
||||
console.log("Create task:", task);
|
||||
};
|
||||
|
||||
const handleTaskUpdate = (taskId: string, updates: Partial<KanbanTask>) => {
|
||||
// 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 (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!kanbanBoard) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
No Kanban Board
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
Select a tablo to view its kanban board
|
||||
</p>
|
||||
{tablos && tablos.length > 0 && (
|
||||
<Select
|
||||
selectedKey={selectedTabloId}
|
||||
onSelectionChange={(key) => setSelectedTabloId(key as string)}
|
||||
className="w-64"
|
||||
>
|
||||
<SelectButton className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white text-left" />
|
||||
<SelectPopover>
|
||||
<SelectListBox>
|
||||
<SelectListItem id="all">All Projects</SelectListItem>
|
||||
{tablos.map((tablo) => (
|
||||
<SelectListItem key={tablo.id} id={tablo.id}>
|
||||
{tablo.name}
|
||||
</SelectListItem>
|
||||
))}
|
||||
</SelectListBox>
|
||||
</SelectPopover>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
{/* Tablo Selector */}
|
||||
<div className="flex items-center justify-between p-4 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center space-x-4">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Project:
|
||||
</label>
|
||||
<Select
|
||||
selectedKey={selectedTabloId}
|
||||
onSelectionChange={(key) => setSelectedTabloId(key as string)}
|
||||
className="w-64"
|
||||
>
|
||||
<SelectButton className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white text-left" />
|
||||
<SelectPopover>
|
||||
<SelectListBox>
|
||||
<SelectListItem id="all">All Projects</SelectListItem>
|
||||
{tablos?.map((tablo) => (
|
||||
<SelectListItem key={tablo.id} id={tablo.id}>
|
||||
{tablo.name}
|
||||
</SelectListItem>
|
||||
))}
|
||||
</SelectListBox>
|
||||
</SelectPopover>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kanban Board */}
|
||||
<div className="flex-1">
|
||||
<KanbanBoard
|
||||
board={kanbanBoard}
|
||||
onTaskCreate={handleTaskCreate}
|
||||
onTaskUpdate={handleTaskUpdate}
|
||||
onTaskDelete={handleTaskDelete}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
153
ui/src/types/kanban.types.ts
Normal file
153
ui/src/types/kanban.types.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue