'use client'; import { useState, useMemo } from 'react'; import type { BeadIssue } from '../../lib/types'; import { useResponsive } from '../../hooks/use-responsive'; import { cn } from '../../lib/utils'; export interface LeftPanelProps { issues: BeadIssue[]; selectedEpicId?: string | null; onEpicSelect?: (epicId: string | null) => void; } interface EpicNode { epic: BeadIssue; children: BeadIssue[]; stats: { total: number; closed: number; in_progress: number; blocked: number; ready: number; lastActivity: number; }; status: 'blocked' | 'in_progress' | 'ready' | 'done' | 'empty'; } function buildEpicTree(issues: BeadIssue[]): EpicNode[] { const epics = issues.filter(issue => issue.issue_type === 'epic'); const epicMap = new Map(); for (const epic of epics) { epicMap.set(epic.id, { epic, children: [], stats: { total: 0, closed: 0, in_progress: 0, blocked: 0, ready: 0, lastActivity: new Date(epic.updated_at).getTime() }, status: 'empty' }); } for (const issue of issues) { if (issue.issue_type === 'epic') continue; const parentDep = issue.dependencies.find(dep => dep.type === 'parent'); if (parentDep && epicMap.has(parentDep.target)) { const node = epicMap.get(parentDep.target)!; node.children.push(issue); node.stats.total++; if (issue.status === 'closed') node.stats.closed++; else if (issue.status === 'blocked') node.stats.blocked++; else if (issue.status === 'in_progress') node.stats.in_progress++; else node.stats.ready++; // open/ready const issueTime = new Date(issue.updated_at).getTime(); if (issueTime > node.stats.lastActivity) node.stats.lastActivity = issueTime; } } // Determine Aggregate Status for (const node of epicMap.values()) { if (node.stats.blocked > 0) node.status = 'blocked'; else if (node.stats.in_progress > 0) node.status = 'in_progress'; else if (node.stats.ready > 0) node.status = 'ready'; else if (node.stats.total > 0 && node.stats.closed === node.stats.total) node.status = 'done'; else node.status = 'empty'; } return Array.from(epicMap.values()).sort((a, b) => { // Sort by status priority (Blocked > In Progress > Ready > Done) then Recency const statusScore = { blocked: 4, in_progress: 3, ready: 2, done: 1, empty: 0 }; const scoreDiff = statusScore[b.status] - statusScore[a.status]; if (scoreDiff !== 0) return scoreDiff; return b.stats.lastActivity - a.stats.lastActivity; }); } function StatusIndicator({ status }: { status: string }) { const styles = { blocked: 'bg-rose-500 shadow-[0_0_8px_#f43f5e]', in_progress: 'bg-amber-500 shadow-[0_0_8px_#f59e0b]', ready: 'bg-teal-500 shadow-[0_0_8px_#14b8a6]', done: 'bg-slate-500', empty: 'bg-white/10' }[status] || 'bg-slate-500'; return
; } export function LeftPanel({ issues, selectedEpicId, onEpicSelect, }: LeftPanelProps) { const [expandedEpics, setExpandedEpics] = useState>(new Set()); const { isDesktop, isTablet } = useResponsive(); const epicTree = useMemo(() => buildEpicTree(issues), [issues]); const toggleEpic = (epicId: string) => { setExpandedEpics(prev => { const next = new Set(prev); if (next.has(epicId)) { next.delete(epicId); } else { next.add(epicId); } return next; }); }; const handleEpicClick = (epicId: string) => { onEpicSelect?.(epicId === selectedEpicId ? null : epicId); toggleEpic(epicId); }; if (isTablet) { return (
{epicTree.map(({ epic, status }) => ( ))}
); } return (
{/* Header */}
Workstreams
{epicTree.length} ACTIVE
{/* Tree */}
{epicTree.map(({ epic, children, stats, status }) => { const isExpanded = expandedEpics.has(epic.id); const isSelected = selectedEpicId === epic.id; // Dynamic Styling based on Status const statusStyle = { blocked: 'border-rose-500/30 bg-rose-500/5 hover:bg-rose-500/10', in_progress: 'border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/10', ready: 'border-teal-500/30 bg-teal-500/5 hover:bg-teal-500/10', done: 'border-white/5 bg-white/[0.02] opacity-60', empty: 'border-white/5 bg-transparent opacity-40' }[status]; const activeStyle = isSelected ? 'ring-1 ring-white/20 shadow-lg scale-[1.02]' : ''; return (
{/* Sub-items (Tasks) */} {isExpanded && children.length > 0 && (
{children.slice(0, 5).map(child => (
{child.id}
{child.title}
))} {children.length > 5 && (
+{children.length - 5} more
)}
)}
); })}
{/* Footer */}
); } export default LeftPanel;