Initial commit: Agile Project Manager
- Complete React + TypeScript application with Vite - Dashboard with charts and metrics visualization - Kanban board with drag-and-drop functionality - Product backlog management with user stories - Sprint planning and tracking features - Comprehensive UI component library (shadcn/ui) - Tailwind CSS styling with dark mode support - Context-based state management - Mock data for immediate testing and demonstration
This commit is contained in:
223
components/KanbanBoard.tsx
Normal file
223
components/KanbanBoard.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useApp } from '../contexts/AppContext';
|
||||
import { Task, UserStory } from '../types';
|
||||
import { TaskCard } from './TaskCard';
|
||||
import { CreateTaskDialog } from './CreateTaskDialog';
|
||||
import { EditTaskDialog } from './EditTaskDialog';
|
||||
import { CommentDialog } from './CommentDialog';
|
||||
import { Card } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
interface DraggableTaskProps {
|
||||
task: Task;
|
||||
onEdit: (task: Task) => void;
|
||||
onComment: (task: Task) => void;
|
||||
}
|
||||
|
||||
const DraggableTask: React.FC<DraggableTaskProps> = ({ task, onEdit, onComment }) => {
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
type: 'TASK',
|
||||
item: { id: task.id },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={drag}
|
||||
style={{ opacity: isDragging ? 0.5 : 1 }}
|
||||
className="cursor-move"
|
||||
>
|
||||
<TaskCard task={task} onEdit={onEdit} onComment={onComment} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ColumnProps {
|
||||
title: string;
|
||||
status: Task['status'];
|
||||
tasks: Task[];
|
||||
onDrop: (taskId: string, newStatus: Task['status']) => void;
|
||||
onEdit: (task: Task) => void;
|
||||
onComment: (task: Task) => void;
|
||||
}
|
||||
|
||||
const Column: React.FC<ColumnProps> = ({ title, status, tasks, onDrop, onEdit, onComment }) => {
|
||||
const [{ isOver }, drop] = useDrop(() => ({
|
||||
accept: 'TASK',
|
||||
drop: (item: { id: string }) => onDrop(item.id, status),
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={drop}
|
||||
className={`flex-1 min-w-[280px] ${isOver ? 'opacity-50' : ''}`}
|
||||
>
|
||||
<Card className="p-4 h-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="flex items-center gap-2">
|
||||
{title}
|
||||
<Badge variant="secondary">{tasks.length}</Badge>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{tasks.map(task => (
|
||||
<DraggableTask
|
||||
key={task.id}
|
||||
task={task}
|
||||
onEdit={onEdit}
|
||||
onComment={onComment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const KanbanBoard: React.FC = () => {
|
||||
const { tasks, userStories, activeSprint, updateTask } = useApp();
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [selectedUserStoryId, setSelectedUserStoryId] = useState<string | null>(null);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||
const [commentDialogOpen, setCommentDialogOpen] = useState(false);
|
||||
|
||||
const sprintUserStories = userStories.filter(
|
||||
story => story.sprintId === activeSprint?.id
|
||||
);
|
||||
|
||||
const handleCreateTask = (userStoryId: string) => {
|
||||
setSelectedUserStoryId(userStoryId);
|
||||
setCreateDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditTask = (task: Task) => {
|
||||
setSelectedTask(task);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCommentTask = (task: Task) => {
|
||||
setSelectedTask(task);
|
||||
setCommentDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDrop = (taskId: string, newStatus: Task['status']) => {
|
||||
updateTask(taskId, { status: newStatus });
|
||||
};
|
||||
|
||||
if (!activeSprint) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<h2 className="mb-2">No Active Sprint</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Please activate a sprint from the Sprint Management page.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1>{activeSprint.name}</h1>
|
||||
<p className="text-muted-foreground">{activeSprint.goal}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{sprintUserStories.map(story => {
|
||||
const storyTasks = tasks.filter(task => task.userStoryId === story.id);
|
||||
|
||||
return (
|
||||
<div key={story.id} className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2>{story.title}</h2>
|
||||
<Badge>{story.storyPoints} pts</Badge>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleCreateTask(story.id)}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Task
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 overflow-x-auto pb-2">
|
||||
<Column
|
||||
title="To Do"
|
||||
status="todo"
|
||||
tasks={storyTasks.filter(t => t.status === 'todo')}
|
||||
onDrop={handleDrop}
|
||||
onEdit={handleEditTask}
|
||||
onComment={handleCommentTask}
|
||||
/>
|
||||
<Column
|
||||
title="In Progress"
|
||||
status="in-progress"
|
||||
tasks={storyTasks.filter(t => t.status === 'in-progress')}
|
||||
onDrop={handleDrop}
|
||||
onEdit={handleEditTask}
|
||||
onComment={handleCommentTask}
|
||||
/>
|
||||
<Column
|
||||
title="Blocked"
|
||||
status="blocked"
|
||||
tasks={storyTasks.filter(t => t.status === 'blocked')}
|
||||
onDrop={handleDrop}
|
||||
onEdit={handleEditTask}
|
||||
onComment={handleCommentTask}
|
||||
/>
|
||||
<Column
|
||||
title="Done"
|
||||
status="done"
|
||||
tasks={storyTasks.filter(t => t.status === 'done')}
|
||||
onDrop={handleDrop}
|
||||
onEdit={handleEditTask}
|
||||
onComment={handleCommentTask}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{sprintUserStories.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
No user stories in this sprint. Add user stories from the backlog.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CreateTaskDialog
|
||||
open={createDialogOpen}
|
||||
onOpenChange={setCreateDialogOpen}
|
||||
userStoryId={selectedUserStoryId || ''}
|
||||
/>
|
||||
|
||||
<EditTaskDialog
|
||||
open={editDialogOpen}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
task={selectedTask}
|
||||
/>
|
||||
|
||||
<CommentDialog
|
||||
open={commentDialogOpen}
|
||||
onOpenChange={setCommentDialogOpen}
|
||||
task={selectedTask}
|
||||
/>
|
||||
</div>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user