'use client';
import { useState } from 'react';
import { ChevronRight, ChevronDown, Folder, FileText, CheckSquare } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { useEpics } from '@/lib/hooks/use-epics';
import { useStories } from '@/lib/hooks/use-stories';
import { useTasks } from '@/lib/hooks/use-tasks';
import type { Epic, Story, Task, WorkItemStatus, WorkItemPriority } from '@/types/project';
interface HierarchyTreeProps {
projectId: string;
onEpicClick?: (epic: Epic) => void;
onStoryClick?: (story: Story) => void;
onTaskClick?: (task: Task) => void;
}
export function HierarchyTree({
projectId,
onEpicClick,
onStoryClick,
onTaskClick,
}: HierarchyTreeProps) {
const { data: epics = [], isLoading: epicsLoading } = useEpics(projectId);
if (epicsLoading) {
return ;
}
if (epics.length === 0) {
return (
No Epics Found
Create your first epic to start organizing work
);
}
return (
{epics.map((epic) => (
))}
);
}
interface EpicNodeProps {
epic: Epic;
onEpicClick?: (epic: Epic) => void;
onStoryClick?: (story: Story) => void;
onTaskClick?: (task: Task) => void;
}
function EpicNode({ epic, onEpicClick, onStoryClick, onTaskClick }: EpicNodeProps) {
const [isExpanded, setIsExpanded] = useState(false);
const { data: stories = [], isLoading: storiesLoading } = useStories(
isExpanded ? epic.id : undefined
);
return (
setIsExpanded(!isExpanded)}
>
{
e.stopPropagation();
onEpicClick?.(epic);
}}
>
{epic.name}
{epic.description && (
{epic.description}
)}
{epic.estimatedHours && (
{epic.estimatedHours}h
{epic.actualHours && ` / ${epic.actualHours}h`}
)}
{isExpanded && (
{storiesLoading ? (
) : stories.length === 0 ? (
No stories in this epic
) : (
stories.map((story) => (
))
)}
)}
);
}
interface StoryNodeProps {
story: Story;
onStoryClick?: (story: Story) => void;
onTaskClick?: (task: Task) => void;
}
function StoryNode({ story, onStoryClick, onTaskClick }: StoryNodeProps) {
const [isExpanded, setIsExpanded] = useState(false);
const { data: tasks = [], isLoading: tasksLoading } = useTasks(
isExpanded ? story.id : undefined
);
return (
setIsExpanded(!isExpanded)}
>
{
e.stopPropagation();
onStoryClick?.(story);
}}
>
{story.title}
{story.description && (
{story.description}
)}
{story.estimatedHours && (
{story.estimatedHours}h
{story.actualHours && ` / ${story.actualHours}h`}
)}
{isExpanded && (
{tasksLoading ? (
) : tasks.length === 0 ? (
No tasks in this story
) : (
tasks.map((task) =>
)
)}
)}
);
}
interface TaskNodeProps {
task: Task;
onTaskClick?: (task: Task) => void;
}
function TaskNode({ task, onTaskClick }: TaskNodeProps) {
return (
onTaskClick?.(task)}
>
{task.description && (
{task.description}
)}
{task.estimatedHours && (
{task.estimatedHours}h
{task.actualHours && ` / ${task.actualHours}h`}
)}
);
}
interface StatusBadgeProps {
status: WorkItemStatus;
size?: 'default' | 'sm' | 'xs';
}
function StatusBadge({ status, size = 'default' }: StatusBadgeProps) {
const variants: Record = {
Backlog: 'secondary',
Todo: 'outline',
InProgress: 'default',
Done: 'outline',
};
const sizeClasses = {
default: 'text-xs',
sm: 'text-xs px-1.5 py-0',
xs: 'text-[10px] px-1 py-0',
};
return (
{status}
);
}
interface PriorityBadgeProps {
priority: WorkItemPriority;
size?: 'default' | 'sm' | 'xs';
}
function PriorityBadge({ priority, size = 'default' }: PriorityBadgeProps) {
const colors: Record = {
Low: 'bg-gray-100 text-gray-700',
Medium: 'bg-blue-100 text-blue-700',
High: 'bg-orange-100 text-orange-700',
Critical: 'bg-red-100 text-red-700',
};
const sizeClasses = {
default: 'text-xs',
sm: 'text-xs px-1.5 py-0',
xs: 'text-[10px] px-1 py-0',
};
return {priority};
}
function HierarchyTreeSkeleton() {
return (
{[1, 2, 3].map((i) => (
))}
);
}