diff --git a/components/calendar/calendar-widget.tsx b/components/calendar/calendar-widget.tsx index 397bbcd..d8f9006 100644 --- a/components/calendar/calendar-widget.tsx +++ b/components/calendar/calendar-widget.tsx @@ -142,6 +142,22 @@ export function CalendarWidget() { } setEvents(upcomingEvents.slice(0, 5)); // Keep only 5 for display + + // Dispatch event for Outlook-style notifications (when events start) + window.dispatchEvent(new CustomEvent('calendar-events-updated', { + detail: { + events: upcomingEvents.map(evt => ({ + id: evt.id, + title: evt.title, + start: evt.start, + end: evt.end, + isAllDay: evt.isAllDay, + calendarName: evt.calendarName, + calendarColor: evt.calendarColor, + })), + } + })); + setError(null); } catch (err) { console.error("Calendar Widget - Error in fetchUpcomingEvents:", err); @@ -155,10 +171,12 @@ export function CalendarWidget() { fetchUpcomingEvents(); // Set up an interval to refresh events every 5 minutes - const intervalId = setInterval(fetchUpcomingEvents, 300000); + const intervalId = setInterval(() => { + fetchUpcomingEvents(); + }, 300000); return () => clearInterval(intervalId); - }, [session, status]); + }, [session, status, triggerNotification]); const formatEventDate = (date: Date, isAllDay: boolean) => { let dateString = ""; diff --git a/components/layout/layout-wrapper.tsx b/components/layout/layout-wrapper.tsx index 4760af9..61b5712 100644 --- a/components/layout/layout-wrapper.tsx +++ b/components/layout/layout-wrapper.tsx @@ -12,6 +12,7 @@ import { useRocketChatCalls } from "@/hooks/use-rocketchat-calls"; import { IncomingCallNotification } from "@/components/incoming-call-notification"; import { useEmailNotifications } from "@/hooks/use-email-notifications"; import { useRocketChatMessageNotifications } from "@/hooks/use-rocketchat-message-notifications"; +import { useCalendarEventNotifications } from "@/hooks/use-calendar-event-notifications"; import { OutlookNotification } from "@/components/outlook-notification"; import { NotificationStack } from "@/components/notification-stack"; @@ -257,6 +258,15 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou }} /> )} + {eventNotification && ( + { + console.log('[LayoutWrapper] Calendar event notification dismissed'); + setEventNotification(null); + }} + /> + )} )} diff --git a/hooks/use-calendar-event-notifications.ts b/hooks/use-calendar-event-notifications.ts new file mode 100644 index 0000000..b39d25f --- /dev/null +++ b/hooks/use-calendar-event-notifications.ts @@ -0,0 +1,153 @@ +import { useState, useEffect, useRef } 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); + + useEffect(() => { + // Listen for calendar events updates + const handleEventsUpdate = (event: CustomEvent) => { + const events = event.detail?.events || []; + + 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 updated', { + count: calendarEvents.length, + }); + }; + + window.addEventListener('calendar-events-updated', handleEventsUpdate as EventListener); + + // Check for events that are starting now (every minute) + const checkForStartingEvents = () => { + const now = new Date(); + const currentTime = now.getTime(); + + // Check events that start within the next 2 minutes (to catch events that just started) + const upcomingWindow = 2 * 60 * 1000; // 2 minutes in milliseconds + + const startingEvents = eventsRef.current.filter((event) => { + // Skip if already notified + if (notifiedEventIdsRef.current.has(event.id)) { + return false; + } + + const eventStartTime = event.start.getTime(); + const timeUntilStart = eventStartTime - currentTime; + + // Event is starting now or within the next 2 minutes + // And hasn't started more than 5 minutes ago (to avoid old notifications) + return ( + timeUntilStart >= -5 * 60 * 1000 && // Not more than 5 minutes ago + timeUntilStart <= upcomingWindow // Within next 2 minutes + ); + }); + + 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, + now: now, + }); + + const timeStr = event.isAllDay + ? 'Toute la journĂ©e' + : format(event.start, 'HH:mm', { locale: fr }); + + const notification: OutlookNotificationData = { + id: `calendar-${event.id}-${Date.now()}`, + source: 'calendar', + title: 'Agenda', + subtitle: event.isAllDay ? 'ÉvĂ©nement aujourd\'hui' : 'ÉvĂ©nement maintenant', + 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); + + // Clean up old notified events (older than 1 hour) to allow re-notification if needed + setTimeout(() => { + notifiedEventIdsRef.current.delete(event.id); + }, 60 * 60 * 1000); // 1 hour + } + }; + + // Check immediately and then every minute + checkForStartingEvents(); + checkIntervalRef.current = setInterval(checkForStartingEvents, 60000); // Every minute + + return () => { + window.removeEventListener('calendar-events-updated', handleEventsUpdate as EventListener); + if (checkIntervalRef.current) { + clearInterval(checkIntervalRef.current); + } + }; + }, []); + + const handleDismiss = () => { + setEventNotification(null); + }; + + return { + eventNotification, + setEventNotification: handleDismiss, + }; +}