233 lines
8.5 KiB
TypeScript
233 lines
8.5 KiB
TypeScript
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<OutlookNotificationData | null>(null);
|
|
const notifiedEventIdsRef = useRef<Set<string>>(new Set());
|
|
const eventsRef = useRef<CalendarEvent[]>([]);
|
|
const checkIntervalRef = useRef<NodeJS.Timeout | null>(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,
|
|
};
|
|
}
|