agenda page
This commit is contained in:
parent
dcc7f71bfb
commit
42233f3141
@ -106,11 +106,7 @@ export default async function CalendarPage() {
|
|||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)]">
|
<div className="container mx-auto py-10">
|
||||||
<div className="flex-1 flex flex-col">
|
|
||||||
<div className="flex-1 p-6">
|
|
||||||
<div className="h-full flex flex-col">
|
|
||||||
<div className="flex-1">
|
|
||||||
<CalendarClient
|
<CalendarClient
|
||||||
initialCalendars={calendars}
|
initialCalendars={calendars}
|
||||||
userId={session.user.id}
|
userId={session.user.id}
|
||||||
@ -121,9 +117,5 @@ export default async function CalendarPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect, useMemo } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import FullCalendar from "@fullcalendar/react";
|
import FullCalendar from "@fullcalendar/react";
|
||||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
import frLocale from "@fullcalendar/core/locales/fr";
|
import frLocale from "@fullcalendar/core/locales/fr";
|
||||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
@ -435,7 +435,9 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
|
|||||||
...cal,
|
...cal,
|
||||||
events: cal.events || []
|
events: cal.events || []
|
||||||
})));
|
})));
|
||||||
const [selectedCalendarId, setSelectedCalendarId] = useState<string | undefined>(undefined);
|
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
|
||||||
|
initialCalendars[0]?.id || ""
|
||||||
|
);
|
||||||
const [view, setView] = useState<"dayGridMonth" | "timeGridWeek" | "timeGridDay">("dayGridMonth");
|
const [view, setView] = useState<"dayGridMonth" | "timeGridWeek" | "timeGridDay">("dayGridMonth");
|
||||||
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
|
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
|
||||||
const [isCalendarModalOpen, setIsCalendarModalOpen] = useState(false);
|
const [isCalendarModalOpen, setIsCalendarModalOpen] = useState(false);
|
||||||
@ -464,81 +466,6 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
|
|||||||
|
|
||||||
const [visibleCalendarIds, setVisibleCalendarIds] = useState<string[]>([]);
|
const [visibleCalendarIds, setVisibleCalendarIds] = useState<string[]>([]);
|
||||||
|
|
||||||
// Calculate events based on visible calendars
|
|
||||||
const events = useMemo(() => {
|
|
||||||
return calendars
|
|
||||||
.filter(cal => visibleCalendarIds.includes(cal.id))
|
|
||||||
.flatMap(cal =>
|
|
||||||
(cal.events || []).map(event => ({
|
|
||||||
id: event.id,
|
|
||||||
title: event.title,
|
|
||||||
start: new Date(event.start),
|
|
||||||
end: new Date(event.end),
|
|
||||||
allDay: event.isAllDay,
|
|
||||||
description: event.description,
|
|
||||||
location: event.location,
|
|
||||||
calendarId: event.calendarId,
|
|
||||||
backgroundColor: `${cal.color}dd`,
|
|
||||||
borderColor: cal.color,
|
|
||||||
textColor: '#ffffff',
|
|
||||||
extendedProps: {
|
|
||||||
calendarName: cal.name,
|
|
||||||
location: event.location,
|
|
||||||
description: event.description,
|
|
||||||
calendarId: event.calendarId,
|
|
||||||
originalEvent: event,
|
|
||||||
color: cal.color
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}, [calendars, visibleCalendarIds]);
|
|
||||||
|
|
||||||
// Event content renderer
|
|
||||||
const renderEventContent = (arg: any) => {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div
|
|
||||||
className="px-2 py-1 overflow-hidden w-full transition-all rounded-sm hover:brightness-110"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${arg.event.backgroundColor}`,
|
|
||||||
boxShadow: `inset 0 0 0 1px ${arg.event.borderColor}, 0 2px 4px ${arg.event.borderColor}40`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-white">
|
|
||||||
{!arg.event.allDay && (
|
|
||||||
<span className="font-medium whitespace-nowrap shrink-0">
|
|
||||||
{new Date(arg.event.start).toLocaleTimeString('fr-FR', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="font-medium truncate max-w-[calc(100%-4.5rem)]">
|
|
||||||
{arg.event.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<div className="text-sm">
|
|
||||||
<p className="font-medium">{arg.event.title}</p>
|
|
||||||
{!arg.event.allDay && (
|
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
|
||||||
{new Date(arg.event.start).toLocaleTimeString('fr-FR', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update useEffect to initialize visible calendars and fetch events
|
// Update useEffect to initialize visible calendars and fetch events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (calendars.length > 0) {
|
if (calendars.length > 0) {
|
||||||
@ -625,19 +552,24 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const calendarRef = useRef<FullCalendar>(null);
|
const calendarRef = useRef<any>(null);
|
||||||
|
|
||||||
const handleCalendarSelect = (calendarId: string) => {
|
const handleCalendarSelect = (calendarId: string) => {
|
||||||
|
console.log("Calendar selected:", calendarId);
|
||||||
setSelectedCalendarId(calendarId);
|
setSelectedCalendarId(calendarId);
|
||||||
|
setEventForm(prev => ({
|
||||||
|
...prev,
|
||||||
|
calendarId: calendarId
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCalendarSave = async (calendarData: Partial<Calendar>) => {
|
const handleCalendarSave = async (calendarData: Partial<Calendar>) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch('/api/calendars', {
|
const response = await fetch("/api/calendars", {
|
||||||
method: 'POST',
|
method: calendarData.id ? "PUT" : "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
...calendarData,
|
...calendarData,
|
||||||
@ -646,15 +578,14 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to save calendar');
|
throw new Error("Failed to save calendar");
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedCalendar = await response.json();
|
await fetchCalendars();
|
||||||
setCalendars(prev => [...prev, savedCalendar]);
|
|
||||||
setIsCalendarModalOpen(false);
|
setIsCalendarModalOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving calendar:', error);
|
console.error("Error saving calendar:", error);
|
||||||
setError(error instanceof Error ? error.message : 'Failed to save calendar');
|
setError(error instanceof Error ? error.message : "Failed to save calendar");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -664,98 +595,160 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(`/api/calendars/${calendarId}`, {
|
const response = await fetch(`/api/calendars/${calendarId}`, {
|
||||||
method: 'DELETE',
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to delete calendar');
|
throw new Error("Failed to delete calendar");
|
||||||
}
|
}
|
||||||
|
|
||||||
setCalendars(prev => prev.filter(cal => cal.id !== calendarId));
|
await fetchCalendars();
|
||||||
if (selectedCalendarId === calendarId) {
|
|
||||||
setSelectedCalendarId(undefined);
|
|
||||||
}
|
|
||||||
setIsCalendarModalOpen(false);
|
setIsCalendarModalOpen(false);
|
||||||
|
|
||||||
|
// If the deleted calendar was selected, select another one
|
||||||
|
if (selectedCalendarId === calendarId) {
|
||||||
|
const remainingCalendars = calendars.filter(cal => cal.id !== calendarId);
|
||||||
|
setSelectedCalendarId(remainingCalendars[0]?.id || "");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting calendar:', error);
|
console.error("Error deleting calendar:", error);
|
||||||
setError(error instanceof Error ? error.message : 'Failed to delete calendar');
|
setError(error instanceof Error ? error.message : "Failed to delete calendar");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateSelect = (selectInfo: any) => {
|
const handleDateSelect = (selectInfo: any) => {
|
||||||
setSelectedEvent(null);
|
const startDate = new Date(selectInfo.start);
|
||||||
|
const endDate = new Date(selectInfo.end);
|
||||||
|
|
||||||
|
console.log("Date select handler - Current state:", {
|
||||||
|
calendars: calendars.map(c => ({ id: c.id, name: c.name })),
|
||||||
|
selectedCalendarId,
|
||||||
|
availableCalendars: calendars.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no calendar is selected, use the first available calendar
|
||||||
|
if (!selectedCalendarId && calendars.length > 0) {
|
||||||
|
const firstCalendar = calendars[0];
|
||||||
|
console.log("No calendar selected, selecting first calendar:", firstCalendar);
|
||||||
|
setSelectedCalendarId(firstCalendar.id);
|
||||||
|
|
||||||
setEventForm({
|
setEventForm({
|
||||||
title: "",
|
title: "",
|
||||||
description: null,
|
description: null,
|
||||||
start: selectInfo.startStr,
|
start: startDate.toISOString(),
|
||||||
end: selectInfo.endStr,
|
end: endDate.toISOString(),
|
||||||
|
allDay: selectInfo.allDay,
|
||||||
|
location: null,
|
||||||
|
calendarId: firstCalendar.id
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setEventForm({
|
||||||
|
title: "",
|
||||||
|
description: null,
|
||||||
|
start: startDate.toISOString(),
|
||||||
|
end: endDate.toISOString(),
|
||||||
allDay: selectInfo.allDay,
|
allDay: selectInfo.allDay,
|
||||||
location: null,
|
location: null,
|
||||||
calendarId: selectedCalendarId
|
calendarId: selectedCalendarId
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setIsEventModalOpen(true);
|
setIsEventModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEventClick = (clickInfo: any) => {
|
const handleEventClick = (clickInfo: any) => {
|
||||||
setSelectedEvent(clickInfo.event.extendedProps.originalEvent);
|
const event = clickInfo.event;
|
||||||
|
const startDate = new Date(event.start);
|
||||||
|
const endDate = new Date(event.end || event.start);
|
||||||
|
|
||||||
|
setSelectedEvent(event.extendedProps.originalEvent);
|
||||||
setEventForm({
|
setEventForm({
|
||||||
title: clickInfo.event.title,
|
title: event.title,
|
||||||
description: clickInfo.event.extendedProps.description,
|
description: event.extendedProps.description,
|
||||||
start: clickInfo.event.startStr,
|
start: startDate.toISOString().slice(0, 16),
|
||||||
end: clickInfo.event.endStr,
|
end: endDate.toISOString().slice(0, 16),
|
||||||
allDay: clickInfo.event.allDay,
|
allDay: event.isAllDay,
|
||||||
location: clickInfo.event.extendedProps.location,
|
location: event.extendedProps.location,
|
||||||
calendarId: clickInfo.event.extendedProps.calendarId
|
calendarId: event.extendedProps.calendarId,
|
||||||
});
|
});
|
||||||
setIsEventModalOpen(true);
|
setIsEventModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEventSubmit = async () => {
|
const handleEventSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
// Validate required fields including calendar
|
||||||
setError(null);
|
if (!eventForm.title || !eventForm.start || !eventForm.end || !eventForm.calendarId) {
|
||||||
|
console.log("Form validation failed:", {
|
||||||
|
title: eventForm.title,
|
||||||
|
start: eventForm.start,
|
||||||
|
end: eventForm.end,
|
||||||
|
calendarId: eventForm.calendarId
|
||||||
|
});
|
||||||
|
setError("Veuillez remplir tous les champs obligatoires et sélectionner un calendrier");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
const eventData = {
|
const eventData = {
|
||||||
...eventForm,
|
...eventForm,
|
||||||
calendarId: selectedCalendarId || calendars[0]?.id
|
start: new Date(eventForm.start).toISOString(),
|
||||||
|
end: new Date(eventForm.end).toISOString(),
|
||||||
|
userId,
|
||||||
|
...(selectedEvent ? { id: selectedEvent.id } : {}), // Include ID for updates
|
||||||
|
allDay: eventForm.allDay // Use allDay instead of isAllDay
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch('/api/events', {
|
console.log("Submitting event with data:", eventData);
|
||||||
method: selectedEvent ? 'PUT' : 'POST',
|
|
||||||
|
const response = await fetch("/api/events", {
|
||||||
|
method: selectedEvent ? "PUT" : "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(eventData),
|
body: JSON.stringify(eventData),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
console.log("Response from server:", responseData);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to save event');
|
console.error("Error response:", responseData);
|
||||||
}
|
throw new Error(responseData.error || "Failed to save event");
|
||||||
|
|
||||||
const savedEvent = await response.json();
|
|
||||||
|
|
||||||
if (selectedEvent) {
|
|
||||||
setCalendars(prev => prev.map(cal => ({
|
|
||||||
...cal,
|
|
||||||
events: cal.events.map(ev =>
|
|
||||||
ev.id === selectedEvent.id ? savedEvent : ev
|
|
||||||
)
|
|
||||||
})));
|
|
||||||
} else {
|
|
||||||
setCalendars(prev => prev.map(cal => ({
|
|
||||||
...cal,
|
|
||||||
events: cal.id === eventData.calendarId
|
|
||||||
? [...cal.events, savedEvent]
|
|
||||||
: cal.events
|
|
||||||
})));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset form and close modal first
|
||||||
setIsEventModalOpen(false);
|
setIsEventModalOpen(false);
|
||||||
|
setEventForm({
|
||||||
|
title: "",
|
||||||
|
description: null,
|
||||||
|
start: "",
|
||||||
|
end: "",
|
||||||
|
allDay: false,
|
||||||
|
location: null,
|
||||||
|
calendarId: selectedCalendarId
|
||||||
|
});
|
||||||
|
setSelectedEvent(null);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Update calendars state with the new event
|
||||||
|
const updatedCalendars = calendars.map(cal => {
|
||||||
|
if (cal.id === eventData.calendarId) {
|
||||||
|
return {
|
||||||
|
...cal,
|
||||||
|
events: [...cal.events, responseData]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return cal;
|
||||||
|
});
|
||||||
|
setCalendars(updatedCalendars);
|
||||||
|
|
||||||
|
// Fetch fresh data to ensure all calendars are up to date
|
||||||
|
await fetchCalendars();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving event:', error);
|
console.error("Error saving event:", error);
|
||||||
setError(error instanceof Error ? error.message : 'Failed to save event');
|
setError(error instanceof Error ? error.message : "Failed to save event");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -923,10 +916,9 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="space-y-4">
|
||||||
<Card className="flex-1">
|
<Card className="p-4">
|
||||||
<CardHeader className="pb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -983,39 +975,396 @@ export function CalendarClient({ initialCalendars, userId, userProfile }: Calend
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<CalendarSelector />
|
<CalendarSelector />
|
||||||
<div className="mt-4">
|
|
||||||
|
<style jsx global>{`
|
||||||
|
/* Fixed height and scrolling for day cells only */
|
||||||
|
.fc .fc-daygrid-day-frame {
|
||||||
|
min-height: 100px !important;
|
||||||
|
max-height: 100px !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep events container relative positioning */
|
||||||
|
.fc .fc-daygrid-day-events {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar styles */
|
||||||
|
.fc .fc-daygrid-day-frame::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-daygrid-day-frame::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-daygrid-day-frame::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-daygrid-day-frame::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar when not needed */
|
||||||
|
.fc .fc-daygrid-day-frame::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-daygrid-day-frame:hover::-webkit-scrollbar {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{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
|
<FullCalendar
|
||||||
ref={calendarRef}
|
ref={calendarRef}
|
||||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||||
initialView="dayGridMonth"
|
initialView={view}
|
||||||
headerToolbar={false}
|
headerToolbar={{
|
||||||
height="100%"
|
left: "prev,next today",
|
||||||
events={events}
|
center: "title",
|
||||||
|
right: "",
|
||||||
|
}}
|
||||||
|
events={calendars
|
||||||
|
.filter(cal => visibleCalendarIds.includes(cal.id))
|
||||||
|
.flatMap(cal =>
|
||||||
|
(cal.events || []).map(event => ({
|
||||||
|
id: event.id,
|
||||||
|
title: event.title,
|
||||||
|
start: new Date(event.start),
|
||||||
|
end: new Date(event.end),
|
||||||
|
allDay: event.isAllDay,
|
||||||
|
description: event.description,
|
||||||
|
location: event.location,
|
||||||
|
calendarId: event.calendarId,
|
||||||
|
backgroundColor: `${cal.color}dd`,
|
||||||
|
borderColor: cal.color,
|
||||||
|
textColor: '#ffffff',
|
||||||
|
extendedProps: {
|
||||||
|
calendarName: cal.name,
|
||||||
|
location: event.location,
|
||||||
|
description: event.description,
|
||||||
|
calendarId: event.calendarId,
|
||||||
|
originalEvent: event,
|
||||||
|
color: cal.color
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
)}
|
||||||
|
eventContent={(arg) => {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
className="px-2 py-1 overflow-hidden w-full transition-all rounded-sm hover:brightness-110"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${arg.event.backgroundColor}`,
|
||||||
|
boxShadow: `inset 0 0 0 1px ${arg.event.borderColor}, 0 2px 4px ${arg.event.borderColor}40`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-white">
|
||||||
|
{!arg.event.allDay && (
|
||||||
|
<span className="font-medium whitespace-nowrap shrink-0">
|
||||||
|
{new Date(arg.event.start).toLocaleTimeString('fr-FR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="font-medium truncate max-w-[calc(100%-4.5rem)]">
|
||||||
|
{arg.event.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-medium">{arg.event.title}</p>
|
||||||
|
{!arg.event.allDay && (
|
||||||
|
<p className="text-xs text-gray-400 mt-1">
|
||||||
|
{new Date(arg.event.start).toLocaleTimeString('fr-FR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
eventClassNames="rounded-md overflow-hidden"
|
||||||
|
dayCellContent={(arg) => {
|
||||||
|
return (
|
||||||
|
<div className="text-xs font-medium">
|
||||||
|
{arg.dayNumberText}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
locale={frLocale}
|
||||||
selectable={true}
|
selectable={true}
|
||||||
|
selectMirror={true}
|
||||||
|
dayMaxEventRows={false}
|
||||||
|
dayMaxEvents={false}
|
||||||
|
weekends={true}
|
||||||
select={handleDateSelect}
|
select={handleDateSelect}
|
||||||
eventClick={handleEventClick}
|
eventClick={handleEventClick}
|
||||||
eventContent={renderEventContent}
|
height="auto"
|
||||||
locale={frLocale}
|
aspectRatio={1.8}
|
||||||
firstDay={1}
|
slotMinTime="06:00:00"
|
||||||
weekends={true}
|
slotMaxTime="22:00:00"
|
||||||
allDaySlot={true}
|
allDaySlot={true}
|
||||||
slotMinTime="08:00:00"
|
allDayText=""
|
||||||
slotMaxTime="20:00:00"
|
views={{
|
||||||
expandRows={true}
|
timeGridWeek: {
|
||||||
stickyHeaderDates={true}
|
allDayText: "",
|
||||||
dayMaxEvents={true}
|
dayHeaderFormat: { weekday: 'long', day: 'numeric', month: 'numeric' }
|
||||||
eventTimeFormat={{
|
},
|
||||||
|
timeGridDay: {
|
||||||
|
allDayText: "",
|
||||||
|
dayHeaderFormat: { weekday: 'long', day: 'numeric', month: 'numeric' }
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
slotLabelFormat={{
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
hour12: false
|
hour12: false
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Calendar dialog */}
|
||||||
|
<CalendarDialog
|
||||||
|
open={isCalendarModalOpen}
|
||||||
|
onClose={() => setIsCalendarModalOpen(false)}
|
||||||
|
onSave={handleCalendarSave}
|
||||||
|
onDelete={handleCalendarDelete}
|
||||||
|
initialData={selectedCalendar || undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Event dialog */}
|
||||||
|
<Dialog open={isEventModalOpen} onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setIsEventModalOpen(false);
|
||||||
|
setEventForm({
|
||||||
|
title: "",
|
||||||
|
description: null,
|
||||||
|
start: "",
|
||||||
|
end: "",
|
||||||
|
allDay: false,
|
||||||
|
location: null,
|
||||||
|
calendarId: selectedCalendarId || calendars[0]?.id
|
||||||
|
});
|
||||||
|
setSelectedEvent(null);
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{selectedEvent ? "Modifier l'événement" : "Nouvel événement"}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-2 rounded-md text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="title" className="text-base font-semibold">Titre</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
placeholder="Titre de l'événement"
|
||||||
|
value={eventForm.title}
|
||||||
|
onChange={(e) => setEventForm({ ...eventForm, title: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-base font-semibold">Calendrier</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{calendars.map((cal) => (
|
||||||
|
<button
|
||||||
|
key={cal.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setEventForm(prev => ({
|
||||||
|
...prev,
|
||||||
|
calendarId: cal.id
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-all ${
|
||||||
|
eventForm.calendarId === cal.id
|
||||||
|
? 'bg-white ring-2 ring-primary'
|
||||||
|
: 'bg-gray-900 hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: cal.color }}
|
||||||
|
/>
|
||||||
|
<span className={`text-sm ${
|
||||||
|
eventForm.calendarId === cal.id
|
||||||
|
? 'font-medium text-gray-900'
|
||||||
|
: 'text-gray-100'
|
||||||
|
}`}>
|
||||||
|
{cal.name}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Début</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<DatePicker
|
||||||
|
selected={getDateFromString(eventForm.start)}
|
||||||
|
onChange={handleStartDateChange}
|
||||||
|
dateFormat="dd/MM/yyyy"
|
||||||
|
locale="fr"
|
||||||
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
placeholderText="Date"
|
||||||
|
customInput={<Input />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DatePicker
|
||||||
|
selected={getDateFromString(eventForm.start)}
|
||||||
|
onChange={handleStartDateChange}
|
||||||
|
showTimeSelect
|
||||||
|
showTimeSelectOnly
|
||||||
|
timeIntervals={15}
|
||||||
|
timeCaption="Heure"
|
||||||
|
dateFormat="HH:mm"
|
||||||
|
className="w-32"
|
||||||
|
customInput={<Input />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Fin</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<DatePicker
|
||||||
|
selected={getDateFromString(eventForm.end)}
|
||||||
|
onChange={handleEndDateChange}
|
||||||
|
dateFormat="dd/MM/yyyy"
|
||||||
|
locale="fr"
|
||||||
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
placeholderText="Date"
|
||||||
|
customInput={<Input />}
|
||||||
|
minDate={getDateFromString(eventForm.start)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DatePicker
|
||||||
|
selected={getDateFromString(eventForm.end)}
|
||||||
|
onChange={handleEndDateChange}
|
||||||
|
showTimeSelect
|
||||||
|
showTimeSelectOnly
|
||||||
|
timeIntervals={15}
|
||||||
|
timeCaption="Heure"
|
||||||
|
dateFormat="HH:mm"
|
||||||
|
className="w-32"
|
||||||
|
customInput={<Input />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="allDay"
|
||||||
|
checked={eventForm.allDay}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setEventForm({ ...eventForm, allDay: checked as boolean })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="allDay">Toute la journée</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Lieu</Label>
|
||||||
|
<Input
|
||||||
|
value={eventForm.location || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEventForm({ ...eventForm, location: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Ajouter un lieu"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Description</Label>
|
||||||
|
<Textarea
|
||||||
|
value={eventForm.description || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEventForm({ ...eventForm, description: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Ajouter une description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{selectedEvent && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleEventDelete}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Suppression...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Supprimer"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsEventModalOpen(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleEventSubmit} disabled={loading}>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Enregistrement...
|
||||||
|
</>
|
||||||
|
) : selectedEvent ? (
|
||||||
|
"Mettre à jour"
|
||||||
|
) : (
|
||||||
|
"Créer"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user