import { useMemo } from 'react'; import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers'; import { linkifyTaskIdsInMarkdown } from '@renderer/utils/taskReferenceUtils'; import { getTaskKanbanColumn } from '@shared/utils/reviewState'; import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity'; import type { TeamTaskWithKanban } from '@shared/types'; /** * Status/kanban-column display colors. * Matches the kanban column palette from KanbanBoard.tsx. */ const STATUS_COLORS: Record = { pending: { text: '#60a5fa', bg: 'rgba(59, 130, 246, 0.15)' }, // blue todo: { text: '#60a5fa', bg: 'rgba(59, 130, 246, 0.15)' }, in_progress: { text: '#facc15', bg: 'rgba(234, 179, 8, 0.15)' }, // yellow completed: { text: '#4ade80', bg: 'rgba(34, 197, 94, 0.15)' }, // green done: { text: '#4ade80', bg: 'rgba(34, 197, 94, 0.15)' }, review: { text: '#a78bfa', bg: 'rgba(139, 92, 246, 0.15)' }, // purple approved: { text: '#34d399', bg: 'rgba(34, 197, 94, 0.25)' }, // bright green deleted: { text: '#f87171', bg: 'rgba(239, 68, 68, 0.15)' }, // red }; function getEffectiveColumn(task: TeamTaskWithKanban): string { const reviewColumn = getTaskKanbanColumn(task); if (reviewColumn) return reviewColumn; if (task.status === 'pending') return 'todo'; if (task.status === 'completed') return 'done'; return task.status; } function getStatusLabel(column: string): string { const labels: Record = { todo: 'To Do', pending: 'To Do', in_progress: 'In Progress', done: 'Done', completed: 'Done', review: 'Review', approved: 'Approved', deleted: 'Deleted', }; return labels[column] ?? column; } interface TaskTooltipProps { /** Canonical task id or short display id reference. */ taskId: string; /** Optional owning team for cross-team task references. */ teamName?: string; /** Rendered trigger element. */ children: React.ReactElement; /** Tooltip placement. */ side?: 'top' | 'bottom' | 'left' | 'right'; } /** * Tooltip that shows task summary on hover over any #taskId link. * Reads task data from the current team in the store. */ export const TaskTooltip = ({ taskId, teamName, children, side = 'top', }: TaskTooltipProps): React.JSX.Element => { const selectedTeamName = useStore((s) => s.selectedTeamName); const selectedTeamData = useStore((s) => s.selectedTeamData); const globalTasks = useStore((s) => s.globalTasks); const teamByName = useStore((s) => s.teamByName); const task = useMemo(() => { if (teamName && selectedTeamName === teamName) { return ( (selectedTeamData?.tasks ?? []).find((candidate) => taskMatchesRef(candidate, taskId)) ?? null ); } if (teamName) { return ( globalTasks.find( (candidate) => candidate.teamName === teamName && taskMatchesRef(candidate, taskId) ) ?? null ); } const currentTasks = selectedTeamData?.tasks ?? []; const currentMatch = currentTasks.find((task) => taskMatchesRef(task, taskId)); if (currentMatch) return currentMatch; const globalMatches = globalTasks.filter((candidate) => taskMatchesRef(candidate, taskId)); return globalMatches.length === 1 ? globalMatches[0] : null; }, [globalTasks, selectedTeamData, selectedTeamName, teamName, taskId]); const members = useMemo(() => { if (teamName && selectedTeamName === teamName) { return selectedTeamData?.members ?? []; } if (!teamName && task && selectedTeamName === (task as { teamName?: string }).teamName) { return selectedTeamData?.members ?? []; } return []; }, [selectedTeamData, selectedTeamName, teamName, task]); const colorMap = useMemo( () => (members ? buildMemberColorMap(members) : new Map()), [members] ); // If task not found, render children without tooltip if (!task) return children; const column = getEffectiveColumn(task); const statusColor = STATUS_COLORS[column] ?? STATUS_COLORS.pending; const label = getStatusLabel(column); const taskTeamName = typeof (task as unknown as { teamName?: unknown }).teamName === 'string' ? (task as unknown as { teamName: string }).teamName : undefined; const resolvedTeamName = teamName ?? taskTeamName; const resolvedTeamDisplayName = resolvedTeamName ? teamByName[resolvedTeamName]?.displayName : null; return ( {children} {resolvedTeamName ? (
{resolvedTeamDisplayName || resolvedTeamName}
) : null} {/* Subject */}
{formatTaskDisplayLabel(task)}{' '} {task.subject}
{/* Status badge */}
{label} {task.reviewState === 'needsFix' ? ( {REVIEW_STATE_DISPLAY.needsFix.label} ) : null} {/* Owner */} {task.owner && members.length > 0 ? ( ) : task.owner ? ( {task.owner} ) : ( Unassigned )}
{/* Description — full markdown with scroll */} {task.description ? (
) : null}
); };