diff --git a/app/api/twenty-crm/tasks/route.ts b/app/api/twenty-crm/tasks/route.ts index 9cc7f2c..05f4a29 100644 --- a/app/api/twenty-crm/tasks/route.ts +++ b/app/api/twenty-crm/tasks/route.ts @@ -11,6 +11,7 @@ interface TwentyTask { markdown?: string; }; dueAt?: string; + startAt?: string; // Start date/time for Twenty CRM tasks status?: string; // e.g., "Done", "Todo", etc. type?: string; assigneeId?: string; @@ -172,6 +173,7 @@ async function fetchTwentyTasks(userId?: string): Promise { markdown } dueAt + startAt status assigneeId assignee { @@ -303,6 +305,7 @@ async function fetchTwentyTasks(userId?: string): Promise { title: node.title || 'Untitled Task', bodyV2: node.bodyV2 || null, dueAt: node.dueAt || null, + startAt: node.startAt || null, // Start date for Twenty CRM tasks status: node.status || null, type: node.type || 'Task', assigneeId: node.assigneeId || null, @@ -475,6 +478,7 @@ export async function GET(request: NextRequest) { headline: task.title, description: (task as any)._bodyText || null, // Use extracted body text dateToFinish: task.dueAt || null, + startDate: task.startAt || null, // Start date for Twenty CRM tasks (for notifications) projectName: 'Médiation', projectId: 0, status: task.status === 'Done' ? 5 : 1, // 5 = Done, 1 = New (or other status) diff --git a/components/flow.tsx b/components/flow.tsx index c96e84b..930d246 100644 --- a/components/flow.tsx +++ b/components/flow.tsx @@ -251,6 +251,39 @@ export function Duties() { } setTasks(sortedTasks); + + // Dispatch event for Outlook-style notifications (when tasks are due/starting) + const tasksForNotification = sortedTasks.map(task => ({ + id: task.id.toString(), + headline: task.headline, + dateToFinish: task.dateToFinish, + source: (task as any).source || 'leantime', + projectName: task.projectName, + url: (task as any).url || null, + startDate: (task as any).startDate || null, // For Twenty CRM tasks + })); + + console.log('[Devoirs Widget] 📋 Dispatching tasks update', { + tasksCount: tasksForNotification.length, + tasks: tasksForNotification.map(t => ({ + id: t.id, + title: t.headline, + dateToFinish: t.dateToFinish, + source: t.source, + startDate: t.startDate, + })), + }); + + try { + window.dispatchEvent(new CustomEvent('tasks-updated', { + detail: { + tasks: tasksForNotification, + } + })); + console.log('[Devoirs Widget] ✅ Event dispatched successfully'); + } catch (error) { + console.error('[Devoirs Widget] ❌ Error dispatching event', error); + } } catch (error) { console.error('Error fetching tasks:', error); setError(error instanceof Error ? error.message : 'Failed to fetch tasks'); diff --git a/components/layout/layout-wrapper.tsx b/components/layout/layout-wrapper.tsx index 6e3600d..4e5fc51 100644 --- a/components/layout/layout-wrapper.tsx +++ b/components/layout/layout-wrapper.tsx @@ -13,6 +13,7 @@ import { IncomingCallNotification } from "@/components/incoming-call-notificatio import { useEmailNotifications } from "@/hooks/use-email-notifications"; import { useRocketChatMessageNotifications } from "@/hooks/use-rocketchat-message-notifications"; import { useCalendarEventNotifications } from "@/hooks/use-calendar-event-notifications"; +import { useTaskNotifications } from "@/hooks/use-task-notifications"; import { OutlookNotification } from "@/components/outlook-notification"; import { NotificationStack } from "@/components/notification-stack"; @@ -38,6 +39,9 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou // Listen for calendar event notifications const { eventNotification, setEventNotification } = useCalendarEventNotifications(); + // Listen for task notifications + const { taskNotification, setTaskNotification } = useTaskNotifications(); + // Also listen for RocketChat iframe events (fallback) useEffect(() => { if (isSignInPage) return; @@ -270,6 +274,15 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou }} /> )} + {taskNotification && ( + { + console.log('[LayoutWrapper] Task notification dismissed'); + setTaskNotification(null); + }} + /> + )} )} diff --git a/hooks/use-task-notifications.ts b/hooks/use-task-notifications.ts new file mode 100644 index 0000000..172651a --- /dev/null +++ b/hooks/use-task-notifications.ts @@ -0,0 +1,229 @@ +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; + startDate?: string | null; // For Twenty CRM tasks +} + +/** + * 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, + startDate: t.startDate, + })), + }); + + 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, + startDate: task.startDate || 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 Leantime tasks: notification at due date time (dateToFinish) + // For Twenty CRM tasks: notification at start date time (startDate) if available, otherwise due date + let notificationDate: Date | null = null; + + if (task.source === 'twenty-crm' && task.startDate) { + // Twenty CRM: use start date for notification + notificationDate = new Date(task.startDate); + } else if (task.dateToFinish) { + // Leantime: use due date for notification + notificationDate = new Date(task.dateToFinish); + } + + 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 1 minute of the notification time (30 seconds before to 30 seconds after) + const notificationWindow = 30 * 1000; // 30 seconds + const inNotificationWindow = + timeUntilNotification >= -notificationWindow && + timeUntilNotification <= notificationWindow; + + console.log('[useTaskNotifications] ⏰ Checking task', { + id: task.id, + title: task.headline, + source: task.source, + notificationDate: notificationDate.toISOString(), + now: 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.source === 'twenty-crm' && a.startDate + ? new Date(a.startDate).getTime() + : a.dateToFinish ? new Date(a.dateToFinish).getTime() : Infinity; + const dateB = b.source === 'twenty-crm' && b.startDate + ? new Date(b.startDate).getTime() + : b.dateToFinish ? new Date(b.dateToFinish).getTime() : Infinity; + return dateA - dateB; + }); + + // Show notification for the first task + const task = dueTasks[0]; + + const notificationDate = task.source === 'twenty-crm' && task.startDate + ? new Date(task.startDate) + : 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/starting 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} - ${task.source === 'twenty-crm' ? 'Début' : 'É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, + }; +}