import { useState, useEffect, useRef, useCallback } from 'react'; import { Calendar } from 'lucide-react'; import { OutlookNotificationData } from '@/components/outlook-notification'; import { format } from 'date-fns'; import { fr } from 'date-fns/locale'; interface CalendarEvent { id: string; title: string; start: Date; end: Date; isAllDay: boolean; calendarName?: string; calendarColor?: string; } /** * Hook to manage calendar event notifications and show Outlook-style notifications * when an event's start time arrives */ export function useCalendarEventNotifications() { const [eventNotification, setEventNotification] = useState(null); const notifiedEventIdsRef = useRef>(new Set()); const eventsRef = useRef([]); const checkIntervalRef = useRef(null); // Load notified event IDs from localStorage on mount useEffect(() => { try { const stored = localStorage.getItem('notified-event-ids'); if (stored) { const ids = JSON.parse(stored); notifiedEventIdsRef.current = new Set(ids); console.log('[useCalendarEventNotifications] 📦 Loaded notified event IDs from localStorage', { count: ids.length, }); } } catch (error) { console.error('[useCalendarEventNotifications] ❌ Error loading notified event IDs from localStorage', error); } }, []); // Save notified event IDs to localStorage whenever it changes const saveNotifiedEventIds = useCallback(() => { try { const ids = Array.from(notifiedEventIdsRef.current); localStorage.setItem('notified-event-ids', JSON.stringify(ids)); console.log('[useCalendarEventNotifications] 💾 Saved notified event IDs to localStorage', { count: ids.length, }); } catch (error) { console.error('[useCalendarEventNotifications] ❌ Error saving notified event IDs to localStorage', error); } }, []); useEffect(() => { console.log('[useCalendarEventNotifications] 🎧 Hook initialized, listening for calendar-events-updated'); // Listen for calendar events updates const handleEventsUpdate = (event: CustomEvent) => { const events = event.detail?.events || []; console.log('[useCalendarEventNotifications] 📅 Received calendar events update', { eventsCount: events.length, events: events.map((e: any) => ({ id: e.id, title: e.title, start: e.start, isAllDay: e.isAllDay, })), }); if (!events || events.length === 0) { eventsRef.current = []; return; } // Convert events to CalendarEvent format const calendarEvents: CalendarEvent[] = events.map((evt: any) => ({ id: evt.id, title: evt.title, start: new Date(evt.start), end: new Date(evt.end), isAllDay: evt.isAllDay || false, calendarName: evt.calendarName, calendarColor: evt.calendarColor, })); eventsRef.current = calendarEvents; console.log('[useCalendarEventNotifications] Events stored', { count: calendarEvents.length, events: calendarEvents.map(e => ({ id: e.id, title: e.title, start: e.start.toISOString(), })), }); }; window.addEventListener('calendar-events-updated', handleEventsUpdate as EventListener); // Check for events that are starting soon (every 10 seconds) const checkForStartingEvents = () => { const now = new Date(); const currentTime = now.getTime(); console.log('[useCalendarEventNotifications] 🔍 Checking for starting events', { now: now.toISOString(), eventsCount: eventsRef.current.length, notifiedCount: notifiedEventIdsRef.current.size, }); // Show notification 3 minutes before event starts // Window: between 3 minutes 30 seconds and 2 minutes 30 seconds before start // This gives a 1-minute window to catch the notification const notificationTimeBefore = 3 * 60 * 1000; // 3 minutes before const windowStart = notificationTimeBefore + 30 * 1000; // 3 min 30 sec before (start of window) const windowEnd = notificationTimeBefore - 30 * 1000; // 2 min 30 sec before (end of window) const startingEvents = eventsRef.current.filter((event) => { // Skip if already notified if (notifiedEventIdsRef.current.has(event.id)) { console.log('[useCalendarEventNotifications] ⏭️ Event already notified', { id: event.id, title: event.title, }); return false; } const eventStartTime = event.start.getTime(); const timeUntilStart = eventStartTime - currentTime; const timeUntilStartMinutes = Math.round(timeUntilStart / 1000 / 60); const timeUntilStartSeconds = Math.round(timeUntilStart / 1000); // Check if event is in the notification window (3 minutes before, with 1-minute tolerance) const inNotificationWindow = timeUntilStart <= windowStart && timeUntilStart >= windowEnd; console.log('[useCalendarEventNotifications] ⏰ Checking event', { id: event.id, title: event.title, start: event.start.toISOString(), now: now.toISOString(), timeUntilStartMinutes, timeUntilStartSeconds, inWindow: inNotificationWindow, windowStart: Math.round(windowStart / 1000), windowEnd: Math.round(windowEnd / 1000), }); // Event should be notified 3 minutes before start (with 1-minute window for detection) return inNotificationWindow; }); if (startingEvents.length > 0) { // Sort by start time (earliest first) startingEvents.sort((a, b) => a.start.getTime() - b.start.getTime()); // Show notification for the first event starting const event = startingEvents[0]; console.log('[useCalendarEventNotifications] 📅 Event starting detected!', { title: event.title, start: event.start.toISOString(), now: now.toISOString(), timeUntilStart: Math.round((event.start.getTime() - currentTime) / 1000 / 60), }); const timeStr = event.isAllDay ? 'Toute la journée' : format(event.start, 'HH:mm', { locale: fr }); const timeUntilStart = event.start.getTime() - now.getTime(); const minutesUntilStart = Math.round(timeUntilStart / 1000 / 60); const notification: OutlookNotificationData = { id: `calendar-${event.id}-${Date.now()}`, source: 'calendar', title: 'Agenda', subtitle: event.isAllDay ? 'Événement dans 3 minutes' : `Événement dans ${minutesUntilStart} min`, message: `${event.title}${event.isAllDay ? '' : ` à ${timeStr}`}${event.calendarName ? ` (${event.calendarName})` : ''}`, icon: Calendar, iconColor: 'text-orange-600', iconBgColor: 'bg-orange-100', borderColor: 'border-orange-500', link: '/agenda', timestamp: event.start, autoDismiss: 30000, // 30 seconds for calendar events actions: [ { label: 'Ouvrir', onClick: () => { window.location.href = '/agenda'; }, variant: 'default', className: 'bg-orange-600 hover:bg-orange-700 text-white', }, ], }; setEventNotification(notification); notifiedEventIdsRef.current.add(event.id); saveNotifiedEventIds(); // Persist to localStorage // Clean up old notified events (older than 1 hour) to allow re-notification if needed setTimeout(() => { notifiedEventIdsRef.current.delete(event.id); saveNotifiedEventIds(); // Update localStorage after cleanup }, 60 * 60 * 1000); // 1 hour } }; // Check immediately and then every 10 seconds for more responsive notifications checkForStartingEvents(); checkIntervalRef.current = setInterval(checkForStartingEvents, 10000); // Every 10 seconds return () => { window.removeEventListener('calendar-events-updated', handleEventsUpdate as EventListener); if (checkIntervalRef.current) { clearInterval(checkIntervalRef.current); } }; }, []); const handleDismiss = () => { setEventNotification(null); }; return { eventNotification, setEventNotification: handleDismiss, }; }