import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { cn } from '@renderer/lib/utils'; import { REVIEW_STATE_DISPLAY, TASK_STATUS_LABELS, TASK_STATUS_STYLES, } from '@renderer/utils/memberHelpers'; import { ArrowRight, Eye, MessageSquareX, Plus, ShieldCheck } from 'lucide-react'; import type { TaskHistoryEvent, TeamReviewState, TeamTaskStatus } from '@shared/types'; interface WorkflowTimelineProps { events: TaskHistoryEvent[]; /** Map of member name → color name for colored badges. */ memberColorMap?: Map; } export const WorkflowTimeline = ({ events, memberColorMap }: WorkflowTimelineProps) => { if (events.length === 0) { return (
No workflow history recorded
); } return (
{events.map((event, idx) => { const isLast = idx === events.length - 1; const time = formatTime(event.timestamp); return (
{/* Timeline line + dot */}
{!isLast &&
}
{/* Content */}
{time} {shouldShowTrailingActor(event) && event.actor ? ( ) : null}
{new Date(event.timestamp).toLocaleString()}
); })}
); }; /** Keep old name as re-export for backwards compatibility during migration. */ export const StatusHistoryTimeline = WorkflowTimeline; const EventContent = ({ event, memberColorMap, }: { event: TaskHistoryEvent; memberColorMap?: Map; }) => { switch (event.type) { case 'task_created': return ( Created as {event.actor ? ( <> by ) : null} ); case 'status_changed': return ( ); case 'review_requested': return ( Review requested {event.reviewer ? ( ) : null} ); case 'review_started': return ( Review started ); case 'review_changes_requested': return ( Changes requested ); case 'review_approved': return ( Approved ); default: return Unknown event; } }; const StatusBadge = ({ status }: { status: TeamTaskStatus }) => { const style = TASK_STATUS_STYLES[status] ?? TASK_STATUS_STYLES.pending; const label = TASK_STATUS_LABELS[status] ?? status; return ( {label} ); }; const ReviewStateBadge = ({ state }: { state: TeamReviewState }) => { if (state === 'none') return null; const display = REVIEW_STATE_DISPLAY[state]; if (!display) return null; return ( {display.label} ); }; function dotColor(event: TaskHistoryEvent): string { switch (event.type) { case 'task_created': return dotColorForStatus(event.status); case 'status_changed': return dotColorForStatus(event.to); case 'review_requested': return 'bg-purple-400'; case 'review_started': return 'bg-purple-400'; case 'review_changes_requested': return 'bg-amber-400'; case 'review_approved': return 'bg-emerald-400'; default: return 'bg-zinc-500'; } } function shouldShowTrailingActor(event: TaskHistoryEvent): boolean { return event.type !== 'task_created'; } function dotColorForStatus(status: TeamTaskStatus): string { switch (status) { case 'pending': return 'bg-zinc-500'; case 'in_progress': return 'bg-blue-400'; case 'completed': return 'bg-emerald-400'; case 'deleted': return 'bg-red-400'; default: return 'bg-zinc-500'; } } function formatTime(timestamp: string): string { try { const date = new Date(timestamp); if (isNaN(date.getTime())) return '??:??'; return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); } catch { return '??:??'; } }