'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-[#C97A7A] shadow-[0_0_8px_rgba(201,122,122,0.45)]', in_progress: 'bg-[#D4A574] shadow-[0_0_8px_rgba(212,165,116,0.45)]', ready: 'bg-[#7CB97A] shadow-[0_0_8px_rgba(124,185,122,0.45)]', done: 'bg-[var(--status-closed)]', 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 featuredEpics = useMemo(() => epicTree.slice(0, 2), [epicTree]); const standardEpics = useMemo(() => epicTree.slice(2, 6), [epicTree]); const compactEpics = useMemo(() => epicTree.slice(6), [epicTree]); 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 */}
{[ { label: 'Featured', items: featuredEpics, tier: 'featured' as const }, { label: 'Active', items: standardEpics, tier: 'standard' as const }, { label: 'Queue', items: compactEpics, tier: 'compact' as const }, ].map((section) => (

{section.label}

{section.items.map(({ epic, children, stats, status }) => { const isExpanded = expandedEpics.has(epic.id); const isSelected = selectedEpicId === epic.id; const statusStyle = { blocked: 'bg-[radial-gradient(circle_at_100%_50%,rgba(201,122,122,0.3),transparent_58%),rgba(92,58,58,0.8)] hover:bg-[radial-gradient(circle_at_100%_50%,rgba(201,122,122,0.38),transparent_58%),rgba(106,64,64,0.85)]', in_progress: 'bg-[radial-gradient(circle_at_100%_50%,rgba(212,165,116,0.34),transparent_58%),rgba(92,70,45,0.82)] hover:bg-[radial-gradient(circle_at_100%_50%,rgba(212,165,116,0.44),transparent_58%),rgba(108,82,51,0.88)]', ready: 'bg-[radial-gradient(circle_at_100%_50%,rgba(124,185,122,0.34),transparent_60%),rgba(54,84,55,0.84)] hover:bg-[radial-gradient(circle_at_100%_50%,rgba(124,185,122,0.44),transparent_60%),rgba(61,95,61,0.9)]', done: 'bg-[radial-gradient(circle_at_100%_50%,rgba(91,168,160,0.3),transparent_58%),rgba(52,72,77,0.78)] hover:bg-[radial-gradient(circle_at_100%_50%,rgba(91,168,160,0.38),transparent_58%),rgba(59,82,87,0.84)]', empty: 'bg-[radial-gradient(circle_at_100%_50%,rgba(74,104,130,0.2),transparent_58%),rgba(44,49,65,0.76)] hover:bg-[radial-gradient(circle_at_100%_50%,rgba(74,104,130,0.28),transparent_58%),rgba(49,56,74,0.82)]', }[status]; if (section.tier === 'compact') { return ( ); } const isFeatured = section.tier === 'featured'; const cardPadding = isFeatured ? 'p-4' : 'p-3'; const titleClass = isFeatured ? 'text-base' : 'text-sm'; const activeStyle = isSelected ? 'shadow-[0_24px_34px_-16px_rgba(0,0,0,0.9),0_0_0_1px_rgba(255,255,255,0.08)_inset] scale-[1.01]' : 'shadow-[0_10px_20px_-14px_rgba(0,0,0,0.85)]'; return (
{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;