"use client"; import { useEffect, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { RefreshCw, Calendar as CalendarIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { useSession } from "next-auth/react"; import { useUnifiedRefresh } from "@/hooks/use-unified-refresh"; import { REFRESH_INTERVALS } from "@/lib/constants/refresh-intervals"; interface Event { id: string; title: string; start: string; end: string; allDay: boolean; calendar: string; calendarColor: string; } export function Calendar() { const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const router = useRouter(); const { data: session, status } = useSession(); const fetchEvents = async (forceRefresh: boolean = false) => { // Only show loading spinner on initial load or manual refresh, not on auto-refresh if (forceRefresh || events.length === 0) { setLoading(true); } try { const url = forceRefresh ? '/api/calendars?refresh=true' : '/api/calendars'; const response = await fetch(url); if (!response.ok) { throw new Error('Failed to fetch events'); } const calendarsData = await response.json(); console.log('Calendar Widget - Fetched calendars:', calendarsData); // Get current date at the start of the day const now = new Date(); now.setHours(0, 0, 0, 0); // Helper function to get display name for calendar const getCalendarDisplayName = (calendar: any) => { // If calendar is synced to an external account, use the account name if (calendar.syncConfig?.syncEnabled && calendar.syncConfig?.mailCredential) { return calendar.syncConfig.mailCredential.display_name || calendar.syncConfig.mailCredential.email; } // For non-synced calendars, use the calendar name return calendar.name; }; // Extract and process events from all calendars const allEvents = calendarsData.flatMap((calendar: any) => (calendar.events || []).map((event: any) => ({ id: event.id, title: event.title, start: event.start, end: event.end, allDay: event.isAllDay, calendar: getCalendarDisplayName(calendar), calendarColor: calendar.color, externalEventId: event.externalEventId || null, // Add externalEventId for deduplication })) ); // Deduplicate events by externalEventId (if same event appears in multiple calendars) // Keep the first occurrence of each unique externalEventId const seenExternalIds = new Set(); const seenEventKeys = new Set(); // For events without externalEventId, use title+start+calendar as key const deduplicatedEvents = allEvents.filter((event: any) => { if (event.externalEventId) { // For events with externalEventId, deduplicate strictly by externalEventId if (seenExternalIds.has(event.externalEventId)) { console.log('Calendar Widget - Skipping duplicate by externalEventId:', { title: event.title, externalEventId: event.externalEventId, calendar: event.calendar, }); return false; // Skip duplicate } seenExternalIds.add(event.externalEventId); } else { // For events without externalEventId, use title + start date + calendar as key // This prevents false positives when same title/date appears in different calendars const eventKey = `${event.title}|${new Date(event.start).toISOString().split('T')[0]}|${event.calendar}`; if (seenEventKeys.has(eventKey)) { console.log('Calendar Widget - Skipping duplicate by title+date+calendar:', { title: event.title, start: event.start, calendar: event.calendar, }); return false; // Skip duplicate } seenEventKeys.add(eventKey); } return true; // Keep event }); console.log('Calendar Widget - Deduplication:', { totalBefore: allEvents.length, totalAfter: deduplicatedEvents.length, duplicatesRemoved: allEvents.length - deduplicatedEvents.length, }); // Filter for upcoming events const upcomingEvents = deduplicatedEvents .filter((event: any) => new Date(event.start) >= now) .sort((a: any, b: any) => new Date(a.start).getTime() - new Date(b.start).getTime()) .slice(0, 7); console.log('Calendar Widget - Processed events:', upcomingEvents); setEvents(upcomingEvents); // Dispatch event for Outlook-style notifications (when events start) const eventsForNotification = upcomingEvents.map((evt: Event) => ({ id: evt.id, title: evt.title, start: typeof evt.start === 'string' ? new Date(evt.start) : evt.start, end: typeof evt.end === 'string' ? new Date(evt.end) : evt.end, isAllDay: evt.allDay, calendarName: evt.calendar, calendarColor: evt.calendarColor, })); console.log('[Calendar Widget] 📅 Dispatching calendar events update', { eventsCount: eventsForNotification.length, events: eventsForNotification.map((e: { id: string; title: string; start: Date; end: Date; isAllDay: boolean; calendarName: string; calendarColor: string }) => ({ id: e.id, title: e.title, start: e.start instanceof Date ? e.start.toISOString() : e.start, isAllDay: e.isAllDay, })), }); try { window.dispatchEvent(new CustomEvent('calendar-events-updated', { detail: { events: eventsForNotification, } })); console.log('[Calendar Widget] ✅ Event dispatched successfully'); } catch (error) { console.error('[Calendar Widget] ❌ Error dispatching event', error); } setError(null); } catch (err) { console.error('Error fetching events:', err); setError('Failed to load events'); } finally { // Only hide loading if we showed it if (forceRefresh || events.length === 0) { setLoading(false); } } }; // Initial fetch on mount useEffect(() => { if (status === 'authenticated') { fetchEvents(true); // Force refresh on initial load } }, [status]); // Integrate unified refresh for automatic polling const { refresh } = useUnifiedRefresh({ resource: 'calendar', interval: REFRESH_INTERVALS.CALENDAR, // 30 seconds (harmonized) enabled: status === 'authenticated', onRefresh: async () => { await fetchEvents(false); // Use cache for auto-refresh }, priority: 'high', }); const formatDate = (dateString: string) => { const date = new Date(dateString); return new Intl.DateTimeFormat('fr-FR', { day: '2-digit', month: 'short' }).format(date); }; const formatTime = (dateString: string) => { const date = new Date(dateString); return new Intl.DateTimeFormat('fr-FR', { hour: '2-digit', minute: '2-digit', }).format(date); }; return ( Agenda {loading ? (
) : error ? (
{error}
) : events.length === 0 ? (
No upcoming events
) : (
{events.map((event) => (
{formatDate(event.start)} {formatTime(event.start)}

{event.title}

{!event.allDay && ( {formatTime(event.start)} - {formatTime(event.end)} )}
{event.calendar}
))}
)} ); }