feat(frontend): Refactor Kanban board to focus on Story management

Refactored the Kanban board from a mixed Epic/Story/Task view to focus exclusively on Stories, which are the right granularity for Kanban management.

Changes:
- Created StoryCard component with Epic breadcrumb, priority badges, and estimated hours display
- Updated KanbanColumn to use Story type and display epic names
- Created CreateStoryDialog for story creation with epic selection
- Added useProjectStories hook to fetch all stories across epics for a project
- Refactored Kanban page to show Stories only with drag-and-drop status updates
- Updated SignalR event handlers to focus on Story events only
- Changed UI text from 'New Issue' to 'New Story' and 'update issue status' to 'update story status'
- Implemented story status change via useChangeStoryStatus hook

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2025-11-05 15:03:12 +01:00
parent 2a0394b5ab
commit 90e3d2416c
6 changed files with 493 additions and 127 deletions

View File

@@ -0,0 +1,143 @@
'use client';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Story } from '@/types/project';
import { FileText, FolderKanban, Clock, CheckSquare } from 'lucide-react';
import { useMemo } from 'react';
interface StoryCardProps {
story: Story;
epicName?: string;
taskCount?: number;
}
export function StoryCard({ story, epicName, taskCount }: StoryCardProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: story.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const priorityColors = {
Low: 'bg-gray-100 text-gray-700 border-gray-300',
Medium: 'bg-blue-100 text-blue-700 border-blue-300',
High: 'bg-orange-100 text-orange-700 border-orange-300',
Critical: 'bg-red-100 text-red-700 border-red-300',
};
const statusColors = {
Backlog: 'bg-gray-100 text-gray-600',
Todo: 'bg-blue-100 text-blue-600',
InProgress: 'bg-yellow-100 text-yellow-700',
Done: 'bg-green-100 text-green-700',
};
// Get assignee initials
const assigneeInitials = useMemo(() => {
if (!story.assigneeId) return null;
// For now, just use first two letters. In real app, fetch user data
return story.assigneeId.substring(0, 2).toUpperCase();
}, [story.assigneeId]);
// Calculate progress (if both estimated and actual hours exist)
const hoursDisplay = useMemo(() => {
if (story.estimatedHours) {
if (story.actualHours) {
return `${story.actualHours}/${story.estimatedHours}h`;
}
return `0/${story.estimatedHours}h`;
}
return null;
}, [story.estimatedHours, story.actualHours]);
return (
<Card
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow"
>
<CardContent className="p-3 space-y-2">
{/* Header: Story icon + Task count */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-green-600" />
<span className="text-xs font-medium text-gray-600">Story</span>
</div>
{taskCount !== undefined && taskCount > 0 && (
<Badge variant="secondary" className="text-xs">
<CheckSquare className="w-3 h-3 mr-1" />
{taskCount} {taskCount === 1 ? 'task' : 'tasks'}
</Badge>
)}
</div>
{/* Epic breadcrumb */}
{epicName && (
<div className="flex items-center gap-1 text-xs text-gray-500">
<FolderKanban className="w-3 h-3" />
<span className="truncate max-w-[200px]" title={epicName}>
{epicName}
</span>
</div>
)}
{/* Title */}
<h3 className="text-sm font-medium line-clamp-2" title={story.title}>
{story.title}
</h3>
{/* Description (if available) */}
{story.description && (
<p className="text-xs text-gray-600 line-clamp-2" title={story.description}>
{story.description}
</p>
)}
{/* Footer: Priority, Hours, Assignee */}
<div className="flex items-center justify-between pt-2 border-t">
<div className="flex items-center gap-2">
<Badge
variant="outline"
className={`${priorityColors[story.priority]} text-xs`}
>
{story.priority}
</Badge>
<Badge
variant="outline"
className={`${statusColors[story.status]} text-xs`}
>
{story.status}
</Badge>
</div>
<div className="flex items-center gap-2">
{/* Hours display */}
{hoursDisplay && (
<div className="flex items-center gap-1 text-xs text-gray-500">
<Clock className="w-3 h-3" />
<span>{hoursDisplay}</span>
</div>
)}
{/* Assignee avatar */}
{assigneeInitials && (
<Avatar className="h-6 w-6">
<AvatarFallback className="text-xs">
{assigneeInitials}
</AvatarFallback>
</Avatar>
)}
</div>
</div>
</CardContent>
</Card>
);
}