363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef } from "react";
|
|
import FullCalendar from "@fullcalendar/react";
|
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
|
import interactionPlugin from "@fullcalendar/interaction";
|
|
import frLocale from "@fullcalendar/core/locales/fr";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Loader2, Plus } from "lucide-react";
|
|
import { Calendar, Event } from "@prisma/client";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
|
|
interface CalendarClientProps {
|
|
initialCalendars: (Calendar & { events: Event[] })[];
|
|
userId: string;
|
|
}
|
|
|
|
interface EventFormData {
|
|
title: string;
|
|
description: string | null;
|
|
start: string;
|
|
end: string;
|
|
allDay: boolean;
|
|
location: string | null;
|
|
calendarId?: string;
|
|
}
|
|
|
|
export function CalendarClient({ initialCalendars, userId }: CalendarClientProps) {
|
|
const [calendars, setCalendars] = useState(initialCalendars);
|
|
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
|
|
initialCalendars[0]?.id || ""
|
|
);
|
|
const [view, setView] = useState<"dayGridMonth" | "timeGridWeek" | "timeGridDay">("dayGridMonth");
|
|
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
|
|
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [eventForm, setEventForm] = useState<EventFormData>({
|
|
title: "",
|
|
description: null,
|
|
start: "",
|
|
end: "",
|
|
allDay: false,
|
|
location: null,
|
|
});
|
|
|
|
const calendarRef = useRef<any>(null);
|
|
|
|
const handleDateSelect = (selectInfo: any) => {
|
|
const startDate = new Date(selectInfo.start);
|
|
const endDate = new Date(selectInfo.end);
|
|
|
|
setEventForm({
|
|
title: "",
|
|
description: null,
|
|
start: startDate.toISOString().slice(0, 16),
|
|
end: endDate.toISOString().slice(0, 16),
|
|
allDay: selectInfo.allDay,
|
|
location: null,
|
|
calendarId: selectedCalendarId,
|
|
});
|
|
setIsEventModalOpen(true);
|
|
};
|
|
|
|
const handleEventClick = (clickInfo: any) => {
|
|
const event = clickInfo.event;
|
|
const startDate = new Date(event.start);
|
|
const endDate = new Date(event.end || event.start);
|
|
|
|
setSelectedEvent(event.extendedProps.originalEvent);
|
|
setEventForm({
|
|
title: event.title,
|
|
description: event.extendedProps.description,
|
|
start: startDate.toISOString().slice(0, 16),
|
|
end: endDate.toISOString().slice(0, 16),
|
|
allDay: event.isAllDay,
|
|
location: event.extendedProps.location,
|
|
calendarId: event.extendedProps.calendarId,
|
|
});
|
|
setIsEventModalOpen(true);
|
|
};
|
|
|
|
const handleEventSubmit = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const method = selectedEvent ? "PUT" : "POST";
|
|
const url = selectedEvent
|
|
? `/api/calendar?id=${selectedEvent.id}`
|
|
: "/api/calendar";
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
...eventForm,
|
|
id: selectedEvent?.id,
|
|
start: new Date(eventForm.start),
|
|
end: new Date(eventForm.end),
|
|
userId,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Erreur lors de la sauvegarde de l'événement");
|
|
}
|
|
|
|
// Refresh calendar data
|
|
const eventsResponse = await fetch("/api/calendar");
|
|
const updatedEvents = await eventsResponse.json();
|
|
setCalendars(calendars.map(cal => ({
|
|
...cal,
|
|
events: updatedEvents.filter((event: Event) => event.calendarId === cal.id)
|
|
})));
|
|
|
|
setIsEventModalOpen(false);
|
|
setSelectedEvent(null);
|
|
setEventForm({
|
|
title: "",
|
|
description: null,
|
|
start: "",
|
|
end: "",
|
|
allDay: false,
|
|
location: null,
|
|
});
|
|
} catch (error) {
|
|
setError((error as Error).message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleEventDelete = async () => {
|
|
if (!selectedEvent) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch(`/api/calendar?id=${selectedEvent.id}`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Erreur lors de la suppression de l'événement");
|
|
}
|
|
|
|
// Refresh calendar data
|
|
const eventsResponse = await fetch("/api/calendar");
|
|
const updatedEvents = await eventsResponse.json();
|
|
setCalendars(calendars.map(cal => ({
|
|
...cal,
|
|
events: updatedEvents.filter((event: Event) => event.calendarId === cal.id)
|
|
})));
|
|
|
|
setIsEventModalOpen(false);
|
|
setSelectedEvent(null);
|
|
} catch (error) {
|
|
setError((error as Error).message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleViewChange = (newView: "dayGridMonth" | "timeGridWeek" | "timeGridDay") => {
|
|
setView(newView);
|
|
if (calendarRef.current) {
|
|
const calendarApi = calendarRef.current.getApi();
|
|
calendarApi.changeView(newView);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Calendar filters and options */}
|
|
<div className="flex flex-wrap justify-between items-center gap-4 mb-4">
|
|
<div className="flex flex-wrap gap-2">
|
|
{calendars.map((calendar) => (
|
|
<Button
|
|
key={calendar.id}
|
|
variant={calendar.id === selectedCalendarId ? "default" : "outline"}
|
|
onClick={() => setSelectedCalendarId(calendar.id)}
|
|
>
|
|
{calendar.name}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
<Button
|
|
onClick={() => {
|
|
setSelectedEvent(null);
|
|
setIsEventModalOpen(true);
|
|
}}
|
|
>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Nouvel événement
|
|
</Button>
|
|
</div>
|
|
|
|
{/* View selector */}
|
|
<Tabs value={view} className="w-full">
|
|
<TabsList className="mb-4">
|
|
<TabsTrigger
|
|
value="dayGridMonth"
|
|
onClick={() => handleViewChange("dayGridMonth")}
|
|
>
|
|
Mois
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="timeGridWeek"
|
|
onClick={() => handleViewChange("timeGridWeek")}
|
|
>
|
|
Semaine
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="timeGridDay"
|
|
onClick={() => handleViewChange("timeGridDay")}
|
|
>
|
|
Jour
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* Calendar display */}
|
|
<Card className="p-4">
|
|
{error && (
|
|
<div className="p-4 mb-4 text-red-500 bg-red-50 rounded-md">
|
|
Erreur: {error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="h-96 flex items-center justify-center">
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
<span className="ml-2">Chargement des événements...</span>
|
|
</div>
|
|
) : (
|
|
<FullCalendar
|
|
ref={calendarRef}
|
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
|
initialView={view}
|
|
headerToolbar={{
|
|
left: "prev,next today",
|
|
center: "title",
|
|
right: "",
|
|
}}
|
|
events={calendars.flatMap(cal =>
|
|
cal.events.map(event => ({
|
|
id: event.id,
|
|
title: event.title,
|
|
start: event.start,
|
|
end: event.end,
|
|
allDay: event.isAllDay,
|
|
description: event.description,
|
|
location: event.location,
|
|
calendarId: event.calendarId,
|
|
originalEvent: event,
|
|
}))
|
|
)}
|
|
locale={frLocale}
|
|
selectable={true}
|
|
selectMirror={true}
|
|
dayMaxEvents={true}
|
|
weekends={true}
|
|
select={handleDateSelect}
|
|
eventClick={handleEventClick}
|
|
height="auto"
|
|
aspectRatio={1.8}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</Tabs>
|
|
|
|
{/* Event creation/edit dialog */}
|
|
<Dialog open={isEventModalOpen} onOpenChange={setIsEventModalOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{selectedEvent ? "Modifier l'événement" : "Nouvel événement"}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<label>Titre</label>
|
|
<Input
|
|
value={eventForm.title}
|
|
onChange={(e) =>
|
|
setEventForm({ ...eventForm, title: e.target.value })
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label>Description</label>
|
|
<Textarea
|
|
value={eventForm.description || ""}
|
|
onChange={(e) =>
|
|
setEventForm({ ...eventForm, description: e.target.value || null })
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<label>Début</label>
|
|
<Input
|
|
type="datetime-local"
|
|
value={eventForm.start}
|
|
onChange={(e) =>
|
|
setEventForm({ ...eventForm, start: e.target.value })
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label>Fin</label>
|
|
<Input
|
|
type="datetime-local"
|
|
value={eventForm.end}
|
|
onChange={(e) =>
|
|
setEventForm({ ...eventForm, end: e.target.value })
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label>Lieu</label>
|
|
<Input
|
|
value={eventForm.location || ""}
|
|
onChange={(e) =>
|
|
setEventForm({ ...eventForm, location: e.target.value || null })
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter className="flex justify-between">
|
|
{selectedEvent && (
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleEventDelete}
|
|
disabled={loading}
|
|
>
|
|
Supprimer
|
|
</Button>
|
|
)}
|
|
<div className="space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsEventModalOpen(false)}
|
|
disabled={loading}
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button onClick={handleEventSubmit} disabled={loading}>
|
|
{selectedEvent ? "Modifier" : "Créer"}
|
|
</Button>
|
|
</div>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|