NeahStable/components/calendar/calendar-widget.tsx
2026-01-17 13:44:45 +01:00

259 lines
8.8 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: string | Date;
end: string | Date;
isAllDay: boolean;
calendarId: string;
calendarName?: string;
calendarColor?: string;
};
export function CalendarWidget() {
const { data: session } = 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(() => {
// Ne charger les événements que si l'utilisateur est connecté
if (!session) return;
const fetchUpcomingEvents = async () => {
try {
setLoading(true);
// Récupérer d'abord les calendriers de l'utilisateur
const calendarsRes = await fetch("/api/calendars");
if (!calendarsRes.ok) {
throw new Error("Impossible de charger les calendriers");
}
const calendars = await calendarsRes.json();
if (calendars.length === 0) {
setEvents([]);
setLoading(false);
return;
}
// Date actuelle et date dans 7 jours
const now = new Date();
const nextWeek = addDays(now, 7);
// 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;
};
// Récupérer les événements pour chaque calendrier
const allEventsPromises = calendars.map(async (calendar: any) => {
const eventsRes = await fetch(
`/api/calendars/${
calendar.id
}/events?start=${now.toISOString()}&end=${nextWeek.toISOString()}`
);
if (!eventsRes.ok) {
console.warn(
`Impossible de charger les événements du calendrier ${calendar.id}`
);
return [];
}
const events = await eventsRes.json();
// Ajouter les informations du calendrier à chaque événement
return events.map((event: any) => ({
...event,
calendarName: getCalendarDisplayName(calendar),
calendarColor: calendar.color,
}));
});
// Attendre toutes les requêtes d'événements
const allEventsArrays = await Promise.all(allEventsPromises);
// Fusionner tous les événements en un seul tableau
const allEvents = allEventsArrays.flat();
// Trier par date de début
const sortedEvents = allEvents.sort(
(a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()
);
// Limiter à 5 événements pour l'affichage
const upcomingEvents = sortedEvents.slice(0, 5);
// Calculate current event count (all events, not just displayed)
const currentEventCount = sortedEvents.length;
// Prepare notification items for all upcoming events
const notificationItems = sortedEvents.map(event => ({
id: event.id,
title: event.title,
message: event.isAllDay
? `Aujourd'hui (toute la journée)`
: `Le ${format(new Date(event.start), 'dd/MM à HH:mm', { locale: fr })}`,
link: '/agenda',
timestamp: new Date(event.start),
metadata: {
calendarId: event.calendarId,
calendarName: event.calendarName,
isAllDay: event.isAllDay,
},
}));
// Always trigger notification update to keep count fresh in Redis
await triggerNotification({
source: 'calendar',
count: currentEventCount,
items: notificationItems,
});
// Update last count reference
if (currentEventCount !== lastEventCountRef.current) {
lastEventCountRef.current = currentEventCount;
}
setEvents(upcomingEvents);
// Dispatch event for Outlook-style notifications
const eventsForNotification = sortedEvents.map(evt => ({
id: evt.id,
title: evt.title,
start: new Date(evt.start),
end: new Date(evt.end),
isAllDay: evt.isAllDay,
calendarName: evt.calendarName,
calendarColor: evt.calendarColor,
}));
try {
window.dispatchEvent(new CustomEvent('calendar-events-updated', {
detail: {
events: eventsForNotification,
}
}));
} catch (error) {
console.error('[Calendar Widget] Error dispatching event', error);
}
setError(null);
} catch (err) {
console.error("Calendar Widget - Error in fetchUpcomingEvents:", err);
setError("Impossible de charger les événements à venir");
} finally {
setLoading(false);
}
};
fetchUpcomingEvents();
}, [session]);
const formatEventDate = (date: string | Date, isAllDay: boolean) => {
const eventDate = new Date(date);
let dateString = "";
if (isToday(eventDate)) {
dateString = "Aujourd'hui";
} else if (isTomorrow(eventDate)) {
dateString = "Demain";
} else {
dateString = format(eventDate, "EEEE d MMMM", { locale: fr });
}
if (!isAllDay) {
dateString += ` · ${format(eventDate, "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>
);
}