import React, { useState, useEffect } from 'react'; import { Handle, Position, useReactFlow, useUpdateNodeInternals } from '@xyflow/react'; import { Timer, Settings, Check, RefreshCw, Flame, Sparkles, AlertCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { isDefaultLabel } from '../../utils'; const HabitNode = ({ id, data, selected }) => { const { t } = useTranslation(); const { setNodes, setEdges } = useReactFlow(); const updateNodeInternals = useUpdateNodeInternals(); const [countdownText, setCountdownText] = useState(''); // Re-calculate handles when node size might change useEffect(() => { updateNodeInternals(id); }, [id, data.label, data.description, data.resetRule, updateNodeInternals]); // Downstream chain reset logic (BFS) const triggerManualReset = (e) => { e.stopPropagation(); const event = new CustomEvent('trigger-habit-reset', { detail: { nodeId: id } }); window.dispatchEvent(event); if (data.updateNodeData) { data.updateNodeData(id, { status: 'Todo', lastResetTime: new Date().toISOString() }); } else { setNodes((currentNodes) => { return currentNodes.map(node => { if (node.id === id) { return { ...node, data: { ...node.data, status: 'Todo', lastResetTime: new Date().toISOString() } }; } return node; }); }); } }; const toggleDone = (e) => { e.stopPropagation(); const newStatus = data.status === 'Done' ? 'Todo' : 'Done'; if (data.updateNodeData) { data.updateNodeData(id, { status: newStatus }); } else { setNodes((nds) => nds.map((node) => node.id === id ? { ...node, data: { ...node.data, status: newStatus } } : node ) ); } }; const handleConfigure = (e) => { e.stopPropagation(); const event = new CustomEvent('configure-habit-node', { detail: { nodeId: id, type: 'habit' } }); window.dispatchEvent(event); }; // Helper to format reset rule const getResetRuleText = () => { const rule = data.resetRule; if (!rule) return t('canvas.habit_rule_daily', { time: '00:00' }); if (rule.type === 'daily') { return t('canvas.habit_rule_daily', { time: rule.time || '00:00' }); } if (rule.type === 'weekly') { const daysKeys = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; const dayKey = daysKeys[rule.day !== undefined ? Number(rule.day) : 1]; const dayName = t(`common.days_short.${dayKey}`); return t('canvas.habit_rule_weekly', { day: dayName, time: rule.time || '00:00' }); } if (rule.type === 'hourly') { const totalHours = Number(rule.hours || 24); const hours = Math.floor(totalHours); const minutes = Math.round((totalHours - hours) * 60); if (hours > 0 && minutes > 0) { return t('canvas.habit_rule_mixed', { hours, minutes }); } if (hours % 24 === 0) { return t('canvas.habit_rule_days', { days: hours / 24 }); } return t('canvas.habit_rule_hourly', { hours }); } if (rule.type === 'minutely') { const totalMinutes = Number(rule.minutes || 5); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; if (hours > 0 && minutes > 0) { return t('canvas.habit_rule_mixed', { hours, minutes }); } if (hours > 0 && minutes === 0) { if (hours % 24 === 0) { return t('canvas.habit_rule_days', { days: hours / 24 }); } return t('canvas.habit_rule_hourly', { hours }); } return t('canvas.habit_rule_minutely', { minutes }); } return t('canvas.habit_rule_daily', { time: '00:00' }); }; // Calculate next reset countdown text useEffect(() => { const updateCountdown = () => { const lastResetTime = data.lastResetTime || data.createdAt || new Date().toISOString(); const rule = data.resetRule || { type: 'daily', time: '00:00' }; const lastReset = new Date(lastResetTime); const now = new Date(); let nextReset; if (rule.type === 'daily') { const [hour, minute] = (rule.time || '00:00').split(':').map(Number); const rDate = new Date(lastReset); rDate.setHours(hour, minute, 0, 0); if (lastReset.getTime() < rDate.getTime()) { nextReset = rDate; } else { nextReset = new Date(rDate); nextReset.setDate(nextReset.getDate() + 1); } } else if (rule.type === 'weekly') { const dayOfWeek = rule.day !== undefined ? Number(rule.day) : 1; const [hour, minute] = (rule.time || '00:00').split(':').map(Number); const rDate = new Date(lastReset); rDate.setHours(hour, minute, 0, 0); nextReset = rDate; while (nextReset.getDay() !== dayOfWeek || nextReset.getTime() <= lastReset.getTime()) { nextReset.setDate(nextReset.setDate() + 1); } } else if (rule.type === 'hourly') { const hours = Number(rule.hours || 24); nextReset = new Date(lastReset.getTime() + hours * 60 * 60 * 1000); } else if (rule.type === 'minutely') { const minutes = Number(rule.minutes || 5); nextReset = new Date(lastReset.getTime() + minutes * 60 * 1000); } else { const rDate = new Date(lastReset); rDate.setHours(0, 0, 0, 0); if (lastReset.getTime() < rDate.getTime()) { nextReset = rDate; } else { nextReset = new Date(rDate); nextReset.setDate(nextReset.getDate() + 1); } } const diffMs = nextReset.getTime() - now.getTime(); if (diffMs <= 0) { setCountdownText(t('canvas.habit_resetting')); return; } const diffHours = Math.floor(diffMs / (60 * 60 * 1000)); const diffMins = Math.floor((diffMs % (60 * 60 * 1000)) / (60 * 1000)); const diffSecs = Math.floor((diffMs % (60 * 1000)) / 1000); if (diffHours > 0) { setCountdownText(`${diffHours}${t('common.hours_short')} ${diffMins}${t('common.minutes_short')}`); } else { setCountdownText(`${diffMins}${t('common.minutes_short')} ${diffSecs}${t('common.seconds_short')}`); } }; updateCountdown(); const rule = data.resetRule || { type: 'daily', time: '00:00' }; const intervalTime = (rule.type === 'minutely' || rule.type === 'hourly') ? 1000 : 60000; const interval = setInterval(updateCountdown, intervalTime); return () => clearInterval(interval); }, [data.lastResetTime, data.createdAt, data.resetRule, t]); const isDone = data.status === 'Done'; return (
{/* Input Target Handle */}
{/* Header with Title and Settings */}
{isDefaultLabel(data.label) ? t('canvas.new_habit', 'New Habit') : data.label}
{/* Description if present */} {data.description && (

{data.description}

)} {/* Status Badge & Actions */}
{t('canvas.habit_status')} {isDone ? t('canvas.habit_status_done', 'Done') : t('canvas.habit_status_todo', 'Todo')}
{/* Toggle Done Button */}
{/* Time-Reset Countdown Block */}
{t('canvas.habit_reset_rule')} {getResetRuleText()}
{t('canvas.habit_countdown_label')} {countdownText}
{/* Signal state & Test buttons */}
{/* Signal Indicator */} {!isDone ? (
{t('canvas.habit_signal_active')}
) : (
{t('canvas.habit_signal_inactive')}
)} {/* Test Manual Reset Button */}
{/* Output Source Handle */}
); }; export default HabitNode;