import React, { useState, useRef, useEffect } from 'react'; import { Handle, Position, useReactFlow, useUpdateNodeInternals } from '@xyflow/react'; import { CheckCircle2, Circle, X, ArrowRight, Sparkles, AlertCircle, CheckSquare, Settings } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useTheme } from '../../../../context/ThemeContext'; import { isDefaultLabel } from '../../utils'; const PASTEL_COLORS = { yellow: { light: '#fef9c3', dark: '#2e2b14', borderLight: '#eab308', borderDark: '#ca8a04' }, blue: { light: '#dbeafe', dark: '#111827', borderLight: '#3b82f6', borderDark: '#2563eb' }, green: { light: '#dcfce7', dark: '#062f17', borderLight: '#22c55e', borderDark: '#16a34a' }, pink: { light: '#fce7f3', dark: '#37061d', borderLight: '#ec4899', borderDark: '#db2777' }, purple: { light: '#f3e8ff', dark: '#210638', borderLight: '#a855f7', borderDark: '#9333ea' }, orange: { light: '#ffedd5', dark: '#2e1208', borderLight: '#f97316', borderDark: '#ea580c' }, white: { light: '#f5f5f4', dark: '#1c1917', borderLight: '#d6d3d1', borderDark: '#44403c' } }; const getInitials = (displayName, email) => { if (displayName && displayName.trim()) { const parts = displayName.trim().split(/\s+/); if (parts.length >= 2) { return (parts[0][0] + parts[1][0]).toUpperCase(); } return parts[0].substring(0, 2).toUpperCase(); } if (email && email.trim()) { return email.substring(0, 2).toUpperCase(); } return 'U'; }; const SketchNode = ({ id, data, selected }) => { const { setNodes } = useReactFlow(); const updateNodeInternals = useUpdateNodeInternals(); const { t } = useTranslation(); const { isDarkMode } = useTheme(); const [showWarning, setShowWarning] = useState(false); const warningTimeoutRef = useRef(null); // Re-calculate handles when node size might change useEffect(() => { updateNodeInternals(id); }, [id, data.subgoals, data.showProgress, data.description, data.status, showWarning, updateNodeInternals]); useEffect(() => { return () => { if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current); }; }, []); const toggleDone = (e) => { e.stopPropagation(); // Check if subgoals constraint is satisfied if (data.status !== 'Done' && data.subgoals && data.subgoals.done < data.subgoals.total) { setShowWarning(true); if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current); warningTimeoutRef.current = setTimeout(() => { setShowWarning(false); }, 3000); return; } const newStatus = data.status === 'Done' ? 'Todo' : 'Done'; const newProgress = newStatus === 'Done' ? 100 : 0; if (data.updateNodeData) { data.updateNodeData(id, { status: newStatus, progress: data.showProgress ? newProgress : (data.progress || 0) }); } else { setNodes((nds) => nds.map((node) => node.id === id ? { ...node, data: { ...node.data, status: newStatus, progress: data.showProgress ? newProgress : (data.progress || 0) } } : node ) ); } }; const handleProgressChange = (e) => { e.stopPropagation(); const val = parseInt(e.target.value, 10); // Check if subgoals constraint is satisfied if (val === 100 && data.subgoals && data.subgoals.done < data.subgoals.total) { setShowWarning(true); if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current); warningTimeoutRef.current = setTimeout(() => { setShowWarning(false); }, 3000); // Cap progress at 99% and status to In Progress updateProgress(99, 'In Progress'); return; } const newStatus = val === 100 ? 'Done' : (val === 0 ? 'Todo' : 'In Progress'); updateProgress(val, newStatus); }; const updateProgress = (progressVal, newStatus) => { if (data.updateNodeData) { data.updateNodeData(id, { progress: progressVal, status: newStatus }); } else { setNodes((nds) => nds.map((node) => node.id === id ? { ...node, data: { ...node.data, progress: progressVal, status: newStatus } } : node ) ); } }; const openConfigureModal = (e) => { e.stopPropagation(); window.dispatchEvent(new CustomEvent('configure-sketch-node', { detail: { nodeId: id } })); }; const getStatusColor = (status) => { switch (status) { case 'Done': return 'bg-green-500/10 text-success border-success/30'; case 'Blocked': return 'bg-red-500/10 text-error border-error/30'; case 'In Progress': return 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30'; case 'Review': return 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/30'; case 'Todo': case 'Pending': return 'bg-secondary/15 text-primary/70 border-primary/20'; default: return 'bg-surface-container-lowest text-primary border-primary'; } }; const progressValue = data.progress !== undefined ? data.progress : 0; const assignees = data.assignees && Array.isArray(data.assignees) ? data.assignees : (data.assignee ? [data.assignee] : []); const tooltipText = assignees.map(a => { const name = a.displayName || t('canvas.collaborator', 'Collaborator'); return a.email ? `${name} (${a.email})` : name; }).join(', '); const maxVisible = 3; const shouldTruncate = assignees.length > maxVisible; const visibleAssignees = shouldTruncate ? assignees.slice(0, 2) : assignees; const remainingCount = assignees.length - visibleAssignees.length; const selectedColor = data.color && data.color !== 'default' ? PASTEL_COLORS[data.color] : null; const isDone = data.status === 'Done'; const nodeBg = selectedColor ? (isDarkMode ? selectedColor.dark : selectedColor.light) : 'var(--color-surface-container-lowest)'; const nodeBorder = selectedColor ? (isDarkMode ? selectedColor.borderDark : selectedColor.borderLight) : 'var(--color-primary)'; return (
{isDefaultLabel(data.label) ? t('canvas.new_goal', 'New Goal') : data.label}
{data.isNextStep && }
{data.description && (

{data.description}

)} {data.subgoals && data.subgoals.total > 0 && (
{t('canvas.subgoals_progress', { done: data.subgoals.done, total: data.subgoals.total })}
)} {data.showProgress && (
{t('canvas.progress_readiness', 'Readiness')} {progressValue}%
)} {showWarning && (
{t('canvas.subgoal_warning')}
)}
{data.status}
{data.ai && } {assignees.length > 0 && (
{shouldTruncate && (
+{remainingCount}
)} {visibleAssignees.slice().reverse().map((assignee, index) => (
{getInitials(assignee.displayName, assignee.email)}
))}
)}
); }; export default SketchNode;