264 lines
9.3 KiB
TypeScript
264 lines
9.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
import { format, isToday, isTomorrow, addDays } from "date-fns";
|
|
import { fr } from "date-fns/locale";
|
|
import { CalendarIcon, ChevronRight } from "lucide-react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import Link from "next/link";
|
|
import { useSession } from "next-auth/react";
|
|
import { useWidgetNotification } from "@/hooks/use-widget-notification";
|
|
|
|
type Event = {
|
|
id: string;
|
|
title: string;
|
|
start: Date;
|
|
end: Date;
|
|
isAllDay: boolean;
|
|
calendarId: string;
|
|
calendarName?: string;
|
|
calendarColor?: string;
|
|
};
|
|
|
|
export function CalendarWidget() {
|
|
const { data: session, status } = useSession();
|
|
const [events, setEvents] = useState<Event[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { triggerNotification } = useWidgetNotification();
|
|
const lastEventCountRef = useRef<number>(-1);
|
|
|
|
useEffect(() => {
|
|
console.log("Calendar Widget - Session Status:", status);
|
|
console.log("Calendar Widget - Session Data:", session);
|
|
|
|
if (status === "loading") {
|
|
console.log("Calendar Widget - Session is loading");
|
|
return;
|
|
}
|
|
|
|
if (status !== "authenticated" || !session) {
|
|
console.log("Calendar Widget - Not authenticated, skipping fetch");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const fetchUpcomingEvents = async () => {
|
|
try {
|
|
console.log("Calendar Widget - Starting to fetch events");
|
|
setLoading(true);
|
|
|
|
// Fetch calendars with events
|
|
console.log("Calendar Widget - Making API request to /api/calendars");
|
|
const response = await fetch('/api/calendars');
|
|
|
|
if (!response.ok) {
|
|
console.error("Calendar Widget - API response not OK:", response.status, response.statusText);
|
|
throw new Error("Impossible de charger les événements");
|
|
}
|
|
|
|
const calendarsData = await response.json();
|
|
console.log("Calendar Widget - Raw calendars data:", calendarsData);
|
|
|
|
if (!Array.isArray(calendarsData)) {
|
|
console.error("Calendar Widget - Calendars data is not an array:", calendarsData);
|
|
throw new Error("Format de données invalide");
|
|
}
|
|
|
|
// 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 all events and add calendar info
|
|
const allEvents = calendarsData.flatMap((calendar) => {
|
|
const displayName = getCalendarDisplayName(calendar);
|
|
console.log("Calendar Widget - Processing calendar:", displayName, "Events:", calendar.events?.length || 0);
|
|
return (calendar.events || []).map((event: { id: string; title: string; start: string | Date; end: string | Date; isAllDay: boolean; calendarId: string }) => {
|
|
const startDate = new Date(event.start);
|
|
const endDate = new Date(event.end);
|
|
return {
|
|
id: event.id,
|
|
title: event.title,
|
|
start: startDate,
|
|
end: endDate,
|
|
isAllDay: event.isAllDay,
|
|
calendarId: event.calendarId,
|
|
calendarColor: calendar.color,
|
|
calendarName: displayName
|
|
};
|
|
});
|
|
});
|
|
|
|
// Filter for upcoming events (today and tomorrow)
|
|
const tomorrow = addDays(now, 1);
|
|
const upcomingEvents = allEvents
|
|
.filter(event => event.start >= now && event.start <= tomorrow)
|
|
.sort((a, b) => a.start.getTime() - b.start.getTime())
|
|
.slice(0, 10);
|
|
|
|
console.log("Calendar Widget - Final upcoming events:", upcomingEvents);
|
|
|
|
// Calculate current event count
|
|
const currentEventCount = upcomingEvents.length;
|
|
|
|
// Trigger notification if count changed
|
|
if (currentEventCount !== lastEventCountRef.current) {
|
|
lastEventCountRef.current = currentEventCount;
|
|
|
|
// Prepare notification items
|
|
const notificationItems = upcomingEvents.map(event => ({
|
|
id: event.id,
|
|
title: event.title,
|
|
message: event.isAllDay
|
|
? `Aujourd'hui (toute la journée)`
|
|
: `Le ${format(event.start, 'dd/MM à HH:mm', { locale: fr })}`,
|
|
link: '/agenda',
|
|
timestamp: event.start,
|
|
metadata: {
|
|
calendarId: event.calendarId,
|
|
calendarName: event.calendarName,
|
|
isAllDay: event.isAllDay,
|
|
},
|
|
}));
|
|
|
|
// Trigger notification update
|
|
await triggerNotification({
|
|
source: 'calendar',
|
|
count: currentEventCount,
|
|
items: notificationItems,
|
|
});
|
|
}
|
|
|
|
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);
|
|
setError("Impossible de charger les événements à venir");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Initial fetch
|
|
fetchUpcomingEvents();
|
|
|
|
// Set up an interval to refresh events every 5 minutes
|
|
const intervalId = setInterval(() => {
|
|
fetchUpcomingEvents();
|
|
}, 300000);
|
|
|
|
return () => clearInterval(intervalId);
|
|
}, [session, status, triggerNotification]);
|
|
|
|
const formatEventDate = (date: Date, isAllDay: boolean) => {
|
|
let dateString = "";
|
|
|
|
if (isToday(date)) {
|
|
dateString = "Aujourd'hui";
|
|
} else if (isTomorrow(date)) {
|
|
dateString = "Demain";
|
|
} else {
|
|
dateString = format(date, "EEEE d MMMM", { locale: fr });
|
|
}
|
|
|
|
if (!isAllDay) {
|
|
dateString += ` · ${format(date, "HH:mm", { locale: fr })}`;
|
|
}
|
|
|
|
return dateString;
|
|
};
|
|
|
|
return (
|
|
<Card className='transition-transform duration-500 ease-in-out transform hover:scale-105'>
|
|
<CardHeader className='flex flex-row items-center justify-between pb-2'>
|
|
<CardTitle className='text-lg font-medium'>
|
|
Événements à venir
|
|
</CardTitle>
|
|
<Link href='/calendar' passHref>
|
|
<Button variant='ghost' size='sm' className='h-8 w-8 p-0'>
|
|
<ChevronRight className='h-4 w-4' />
|
|
<span className='sr-only'>Voir le calendrier</span>
|
|
</Button>
|
|
</Link>
|
|
</CardHeader>
|
|
<CardContent className='pb-3'>
|
|
{loading ? (
|
|
<div className='flex items-center justify-center py-4'>
|
|
<div className='h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent' />
|
|
<span className='ml-2 text-sm text-muted-foreground'>
|
|
Chargement...
|
|
</span>
|
|
</div>
|
|
) : error ? (
|
|
<p className='text-sm text-red-500'>{error}</p>
|
|
) : events.length === 0 ? (
|
|
<p className='text-sm text-muted-foreground py-2'>
|
|
Aucun événement à venir cette semaine
|
|
</p>
|
|
) : (
|
|
<div className='space-y-3'>
|
|
{events.map((event) => (
|
|
<div
|
|
key={event.id}
|
|
className='flex items-start space-x-3 rounded-md border border-muted p-2'
|
|
>
|
|
<div
|
|
className='h-3 w-3 flex-shrink-0 rounded-full mt-1'
|
|
style={{ backgroundColor: event.calendarColor || "#0082c9" }}
|
|
/>
|
|
<div className='flex-1 min-w-0'>
|
|
<h5
|
|
className='text-sm font-medium truncate'
|
|
title={event.title}
|
|
>
|
|
{event.title}
|
|
</h5>
|
|
<div className='flex items-center text-xs text-muted-foreground mt-1'>
|
|
<CalendarIcon className='h-3 w-3 mr-1' />
|
|
<span>{formatEventDate(event.start, event.isAllDay)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<Link href='/calendar' passHref>
|
|
<Button
|
|
size='sm'
|
|
className='w-full transition-all ease-in-out duration-500 bg-muted text-black hover:text-white hover:bg-primary'
|
|
>
|
|
Voir tous les événements
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
} |