import { useState, useEffect, useRef } from 'react'; import { CheckSquare } from 'lucide-react'; import { OutlookNotificationData } from '@/components/outlook-notification'; import { format } from 'date-fns'; import { fr } from 'date-fns/locale'; interface Task { id: string; headline: string; dateToFinish: string | null; source: 'leantime' | 'twenty-crm'; projectName: string; url?: string; } /** * Hook to manage task notifications and show Outlook-style notifications * - Leantime tasks: notification at due date time * - Twenty CRM tasks: notification at start date time (or due date if no start date) */ export function useTaskNotifications() { const [taskNotification, setTaskNotification] = useState(null); const notifiedTaskIdsRef = useRef>(new Set()); const tasksRef = useRef([]); const checkIntervalRef = useRef(null); useEffect(() => { console.log('[useTaskNotifications] 🎧 Hook initialized, listening for tasks-updated'); // Listen for tasks updates const handleTasksUpdate = (event: CustomEvent) => { const tasks = event.detail?.tasks || []; console.log('[useTaskNotifications] 📋 Received tasks update', { tasksCount: tasks.length, tasks: tasks.map((t: any) => ({ id: t.id, title: t.headline, dateToFinish: t.dateToFinish, source: t.source, })), }); if (!tasks || tasks.length === 0) { tasksRef.current = []; return; } // Convert tasks to Task format const formattedTasks: Task[] = tasks.map((task: any) => ({ id: task.id?.toString() || '', headline: task.headline || task.title || '', dateToFinish: task.dateToFinish || null, source: task.source || 'leantime', projectName: task.projectName || '', url: task.url || null, })); tasksRef.current = formattedTasks; console.log('[useTaskNotifications] Tasks stored', { count: formattedTasks.length, tasks: formattedTasks.map(t => ({ id: t.id, title: t.headline, dateToFinish: t.dateToFinish, source: t.source, })), }); }; window.addEventListener('tasks-updated', handleTasksUpdate as EventListener); // Check for tasks that are due/starting now (every 10 seconds) const checkForDueTasks = () => { const now = new Date(); const currentTime = now.getTime(); console.log('[useTaskNotifications] 🔍 Checking for due tasks', { now: now.toISOString(), tasksCount: tasksRef.current.length, notifiedCount: notifiedTaskIdsRef.current.size, }); const dueTasks = tasksRef.current.filter((task) => { // Skip if already notified if (notifiedTaskIdsRef.current.has(task.id)) { console.log('[useTaskNotifications] ⏭️ Task already notified', { id: task.id, title: task.headline, }); return false; } // For both Leantime and Twenty CRM tasks: notification at due date time (dateToFinish) // Note: Leantime dates might be in MySQL format (YYYY-MM-DD HH:MM:SS) without timezone // We need to parse them correctly to avoid timezone issues let notificationDate: Date | null = null; if (task.dateToFinish) { const dateStr = task.dateToFinish; // Leantime dates with 'Z' are actually local time stored as UTC // We need to extract the time components and create a local date if (dateStr.endsWith('Z')) { // Parse as UTC first to get the components, then create local date // Example: "2026-01-16T01:13:00.000Z" -> treat 01:13 as local time, not UTC const utcDate = new Date(dateStr); // Extract components from UTC date and create local date // This assumes the UTC date actually represents local time notificationDate = new Date( utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate(), utcDate.getUTCHours(), utcDate.getUTCMinutes(), utcDate.getUTCSeconds(), utcDate.getUTCMilliseconds() ); console.log('[useTaskNotifications] 📅 Parsing Leantime date (Z -> local)', { id: task.id, title: task.headline, rawDate: dateStr, utcComponents: { year: utcDate.getUTCFullYear(), month: utcDate.getUTCMonth(), day: utcDate.getUTCDate(), hour: utcDate.getUTCHours(), minute: utcDate.getUTCMinutes(), }, localDate: notificationDate.toLocaleString('fr-FR'), localISO: notificationDate.toISOString(), }); } else if (dateStr.includes('T')) { // ISO format without Z - treat as local time notificationDate = new Date(dateStr); } else if (dateStr.includes(' ')) { // MySQL format: "YYYY-MM-DD HH:MM:SS" - parse as local time const isoLike = dateStr.replace(' ', 'T'); notificationDate = new Date(isoLike); } else { // Try direct parsing notificationDate = new Date(dateStr); } if (!notificationDate || isNaN(notificationDate.getTime())) { console.error('[useTaskNotifications] ❌ Invalid date', { id: task.id, rawDate: dateStr, }); return false; } } if (!notificationDate || isNaN(notificationDate.getTime())) { return false; } const notificationTime = notificationDate.getTime(); const timeUntilNotification = notificationTime - currentTime; const timeUntilNotificationMinutes = Math.round(timeUntilNotification / 1000 / 60); const timeUntilNotificationSeconds = Math.round(timeUntilNotification / 1000); // Notification window: within 2 minutes of the notification time (1 minute before to 1 minute after) // This allows catching tasks that are due now or just became due const notificationWindow = 60 * 1000; // 1 minute const inNotificationWindow = timeUntilNotification >= -notificationWindow && timeUntilNotification <= notificationWindow; console.log('[useTaskNotifications] ⏰ Checking task', { id: task.id, title: task.headline, source: task.source, rawDate: task.dateToFinish, notificationDateLocal: notificationDate.toLocaleString('fr-FR'), notificationDateISO: notificationDate.toISOString(), nowLocal: now.toLocaleString('fr-FR'), nowISO: now.toISOString(), timeUntilNotificationMinutes, timeUntilNotificationSeconds, inWindow: inNotificationWindow, }); return inNotificationWindow; }); if (dueTasks.length > 0) { // Sort by notification time (earliest first) dueTasks.sort((a, b) => { const dateA = a.dateToFinish ? new Date(a.dateToFinish).getTime() : Infinity; const dateB = b.dateToFinish ? new Date(b.dateToFinish).getTime() : Infinity; return dateA - dateB; }); // Show notification for the first task const task = dueTasks[0]; const notificationDate = task.dateToFinish ? new Date(task.dateToFinish) : new Date(); const timeStr = format(notificationDate, 'HH:mm', { locale: fr }); const sourceLabel = task.source === 'twenty-crm' ? 'Médiation' : 'Agilité'; console.log('[useTaskNotifications] ✅ Task due detected!', { id: task.id, title: task.headline, source: task.source, notificationDate: notificationDate.toISOString(), now: now.toISOString(), }); const notification: OutlookNotificationData = { id: `task-${task.id}-${Date.now()}`, source: 'leantime', title: 'Devoirs', subtitle: task.source === 'twenty-crm' ? 'Tâche Médiation' : 'Tâche Agilité', message: `${task.headline} - Échéance à ${timeStr} (${sourceLabel})`, icon: CheckSquare, iconColor: 'text-blue-600', iconBgColor: 'bg-blue-100', borderColor: 'border-blue-500', link: task.url || '/agilite', timestamp: notificationDate, autoDismiss: 30000, // 30 seconds actions: [ { label: 'Ouvrir', onClick: () => { if (task.url) { window.open(task.url, '_blank'); } else { window.location.href = '/agilite'; } }, variant: 'default', className: 'bg-blue-600 hover:bg-blue-700 text-white', }, ], }; setTaskNotification(notification); notifiedTaskIdsRef.current.add(task.id); // Clean up old notified tasks (older than 1 hour) to allow re-notification if needed setTimeout(() => { notifiedTaskIdsRef.current.delete(task.id); }, 60 * 60 * 1000); // 1 hour } }; // Check immediately and then every 10 seconds for more responsive notifications checkForDueTasks(); checkIntervalRef.current = setInterval(checkForDueTasks, 10000); // Every 10 seconds return () => { window.removeEventListener('tasks-updated', handleTasksUpdate as EventListener); if (checkIntervalRef.current) { clearInterval(checkIntervalRef.current); } }; }, []); const handleDismiss = () => { setTaskNotification(null); }; return { taskNotification, setTaskNotification: handleDismiss, }; }