NeahFront7/components/calendar/calendar-client.tsx
2025-04-13 14:27:41 +02:00

994 lines
32 KiB
TypeScript

"use client";
import { useState, useRef, useEffect } 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,
Calendar as CalendarIcon,
Check,
X,
User,
Clock,
BarChart2,
Settings,
ChevronRight,
ChevronLeft,
Bell,
Users,
MapPin,
Tag,
ChevronDown,
ChevronUp
} 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";
import { Label } from "@/components/ui/label";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import DatePicker, { registerLocale } from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { fr } from 'date-fns/locale';
// Register French locale
registerLocale('fr', fr);
// Predefined professional color palette
const colorPalette = [
"#4f46e5", // Indigo
"#0891b2", // Cyan
"#0e7490", // Teal
"#16a34a", // Green
"#65a30d", // Lime
"#ca8a04", // Amber
"#d97706", // Orange
"#dc2626", // Red
"#e11d48", // Rose
"#9333ea", // Purple
"#7c3aed", // Violet
"#2563eb", // Blue
];
interface CalendarClientProps {
initialCalendars: (Calendar & { events: Event[] })[];
userId: string;
userProfile: {
name: string;
email: string;
avatar?: string;
};
}
interface EventFormData {
title: string;
description: string | null;
start: string;
end: string;
allDay: boolean;
location: string | null;
calendarId?: string;
}
interface CalendarDialogProps {
open: boolean;
onClose: () => void;
onSave: (calendarData: Partial<Calendar>) => Promise<void>;
onDelete?: (calendarId: string) => Promise<void>;
initialData?: Partial<Calendar>;
}
function CalendarDialog({ open, onClose, onSave, onDelete, initialData }: CalendarDialogProps) {
const [name, setName] = useState(initialData?.name || "");
const [color, setColor] = useState(initialData?.color || "#4f46e5");
const [description, setDescription] = useState(initialData?.description || "");
const [isSubmitting, setIsSubmitting] = useState(false);
const [customColorMode, setCustomColorMode] = useState(false);
const isMainCalendar = initialData?.name === "Calendrier principal";
useEffect(() => {
if (open) {
setName(initialData?.name || "");
setColor(initialData?.color || "#4f46e5");
setDescription(initialData?.description || "");
setCustomColorMode(!colorPalette.includes(initialData?.color || "#4f46e5"));
}
}, [open, initialData]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitting(true);
try {
await onSave({
id: initialData?.id,
name,
color,
description
});
resetForm();
} catch (error) {
console.error("Erreur lors de la création du calendrier:", error);
} finally {
setIsSubmitting(false);
}
};
const handleDelete = async () => {
if (!initialData?.id || !onDelete || isMainCalendar) return;
if (!confirm("Êtes-vous sûr de vouloir supprimer ce calendrier ? Tous les événements associés seront également supprimés.")) {
return;
}
setIsSubmitting(true);
try {
await onDelete(initialData.id);
resetForm();
} catch (error) {
console.error("Erreur lors de la suppression du calendrier:", error);
} finally {
setIsSubmitting(false);
}
};
const resetForm = () => {
setName("");
setColor("#4f46e5");
setDescription("");
setCustomColorMode(false);
onClose();
};
return (
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-md rounded-xl">
<DialogHeader>
<DialogTitle className="flex items-center text-xl font-semibold text-gray-900">
<CalendarIcon className="w-5 h-5 mr-2 text-indigo-600" />
{initialData?.id ? "Modifier le calendrier" : "Créer un nouveau calendrier"}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="space-y-5 py-4">
<div className="space-y-2">
<Label htmlFor="calendar-name" className="text-gray-700">Nom</Label>
<Input
id="calendar-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Nom du calendrier"
required
disabled={isMainCalendar}
className="rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div className="space-y-3">
<Label className="text-gray-700">Couleur</Label>
<div className="flex flex-wrap gap-3 mb-3">
{colorPalette.map((paletteColor) => (
<button
key={paletteColor}
type="button"
className={`w-8 h-8 rounded-full flex items-center justify-center transition-all ${
color === paletteColor && !customColorMode
? 'ring-2 ring-offset-2 ring-gray-400'
: 'hover:scale-110'
}`}
style={{ backgroundColor: paletteColor }}
onClick={() => {
setColor(paletteColor);
setCustomColorMode(false);
}}
>
{color === paletteColor && !customColorMode && (
<Check className="w-4 h-4 text-white" />
)}
</button>
))}
<button
type="button"
className={`w-8 h-8 rounded-full flex items-center justify-center bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 transition-all ${
customColorMode
? 'ring-2 ring-offset-2 ring-gray-400'
: 'hover:scale-110'
}`}
onClick={() => setCustomColorMode(true)}
>
{customColorMode && <Check className="w-4 h-4 text-white" />}
</button>
</div>
{customColorMode && (
<div className="flex items-center gap-4 mt-2 p-3 bg-gray-50 rounded-lg">
<div className="flex flex-1 items-center gap-3">
<Input
id="calendar-color"
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
className="w-10 h-10 p-1 cursor-pointer rounded border-gray-300"
/>
<Input
type="text"
value={color}
onChange={(e) => setColor(e.target.value)}
placeholder="#RRGGBB"
className="w-28 rounded-lg"
/>
</div>
<div
className="w-8 h-8 rounded-lg shadow-sm"
style={{ backgroundColor: color }}
></div>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="calendar-description" className="text-gray-700">
Description (optionnelle)
</Label>
<Textarea
id="calendar-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description du calendrier"
rows={3}
className="rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
</div>
<DialogFooter className="mt-6 border-t border-gray-100 pt-4">
<div className="flex justify-between w-full">
{initialData?.id && !isMainCalendar && (
<Button
type="button"
variant="destructive"
onClick={handleDelete}
disabled={isSubmitting || isMainCalendar}
>
<X className="w-4 h-4 mr-2" />
Supprimer
</Button>
)}
<div className="flex gap-3">
<Button
type="button"
variant="outline"
onClick={onClose}
disabled={isSubmitting}
className="rounded-lg border-gray-300 text-gray-700 hover:bg-gray-50"
>
Annuler
</Button>
<Button
type="submit"
disabled={!name || isSubmitting || isMainCalendar}
className="rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white"
>
{isSubmitting
? "Enregistrement..."
: initialData?.id
? "Mettre à jour"
: "Créer"
}
</Button>
</div>
</div>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
function StatisticsPanel({ statistics }: {
statistics: {
totalEvents: number;
upcomingEvents: number;
completedEvents: number;
meetingHours: number;
};
}) {
return (
<div className="grid grid-cols-2 gap-4 p-4">
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-full">
<CalendarIcon className="h-5 w-5 text-primary" />
</div>
<div>
<p className="text-sm font-medium">Total Événements</p>
<p className="text-2xl font-bold">{statistics.totalEvents}</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-full">
<Clock className="h-5 w-5 text-primary" />
</div>
<div>
<p className="text-sm font-medium">Heures de Réunion</p>
<p className="text-2xl font-bold">{statistics.meetingHours}h</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-full">
<Bell className="h-5 w-5 text-primary" />
</div>
<div>
<p className="text-sm font-medium">Prochains Événements</p>
<p className="text-2xl font-bold">{statistics.upcomingEvents}</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-full">
<Check className="h-5 w-5 text-primary" />
</div>
<div>
<p className="text-sm font-medium">Événements Terminés</p>
<p className="text-2xl font-bold">{statistics.completedEvents}</p>
</div>
</div>
</Card>
</div>
);
}
function EventPreview({ event, calendar }: { event: Event; calendar: Calendar }) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<Card className="p-4 space-y-4">
<div className="flex items-start justify-between">
<div className="space-y-1">
<h3 className="font-medium">{event.title}</h3>
<div className="flex items-center gap-2 text-sm text-gray-500">
<Clock className="h-4 w-4" />
<span>
{new Date(event.start).toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: calendar.color }}
/>
<Button
variant="ghost"
size="icon"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</div>
</div>
{isExpanded && (
<div className="space-y-4">
{event.description && (
<p className="text-sm text-gray-600">{event.description}</p>
)}
<div className="space-y-2">
{event.location && (
<div className="flex items-center gap-2 text-sm">
<MapPin className="h-4 w-4 text-gray-500" />
<span>{event.location}</span>
</div>
)}
{event.calendarId && (
<div className="flex items-center gap-2 text-sm">
<Tag className="h-4 w-4 text-gray-500" />
<span>{calendar.name}</span>
</div>
)}
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="flex-1">
Modifier
</Button>
<Button variant="outline" size="sm" className="flex-1">
Supprimer
</Button>
</div>
</div>
)}
</Card>
);
}
export function CalendarClient({ initialCalendars, userId, userProfile }: CalendarClientProps) {
const [calendars, setCalendars] = useState(initialCalendars.map(cal => ({
...cal,
events: cal.events || []
})));
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
initialCalendars[0]?.id || ""
);
const [view, setView] = useState<"dayGridMonth" | "timeGridWeek" | "timeGridDay">("dayGridMonth");
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
const [isCalendarModalOpen, setIsCalendarModalOpen] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [selectedCalendar, setSelectedCalendar] = useState<Calendar | 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,
calendarId: selectedCalendarId
});
const [selectedEventPreview, setSelectedEventPreview] = useState<Event | null>(null);
const [statistics, setStatistics] = useState({
totalEvents: initialCalendars.reduce((acc, cal) => acc + cal.events.length, 0),
upcomingEvents: initialCalendars.reduce((acc, cal) =>
acc + cal.events.filter(e => new Date(e.start) > new Date()).length, 0
),
completedEvents: initialCalendars.reduce((acc, cal) =>
acc + cal.events.filter(e => new Date(e.end) < new Date()).length, 0
),
meetingHours: initialCalendars.reduce((acc, cal) =>
acc + cal.events.reduce((hours, e) => {
const duration = (new Date(e.end).getTime() - new Date(e.start).getTime()) / (1000 * 60 * 60);
return hours + duration;
}, 0)
, 0)
});
const upcomingEvents = initialCalendars
.flatMap(cal => cal.events)
.filter(event => new Date(event.start) > new Date())
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
.slice(0, 5);
const calendarRef = useRef<any>(null);
const fetchCalendars = async () => {
try {
setLoading(true);
const response = await fetch("/api/calendars");
if (!response.ok) throw new Error("Failed to fetch calendars");
const data = await response.json();
console.log("Fetched calendars:", data); // Debug log
setCalendars(data.map((cal: Calendar & { events: Event[] }) => ({
...cal,
events: cal.events || []
})));
if (data.length > 0 && !selectedCalendarId) {
setSelectedCalendarId(data[0].id);
}
} catch (error) {
console.error("Error fetching calendars:", error);
setError(error instanceof Error ? error.message : "Failed to fetch calendars");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCalendars();
}, []);
const handleCalendarSave = async (calendarData: Partial<Calendar>) => {
try {
setLoading(true);
const response = await fetch("/api/calendars", {
method: calendarData.id ? "PUT" : "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...calendarData,
userId,
}),
});
if (!response.ok) {
throw new Error("Failed to save calendar");
}
await fetchCalendars();
setIsCalendarModalOpen(false);
} catch (error) {
console.error("Error saving calendar:", error);
setError(error instanceof Error ? error.message : "Failed to save calendar");
} finally {
setLoading(false);
}
};
const handleCalendarDelete = async (calendarId: string) => {
try {
setLoading(true);
const response = await fetch(`/api/calendars/${calendarId}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Failed to delete calendar");
}
await fetchCalendars();
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) {
console.error("Error deleting calendar:", error);
setError(error instanceof Error ? error.message : "Failed to delete calendar");
} finally {
setLoading(false);
}
};
const handleDateSelect = (selectInfo: any) => {
const startDate = new Date(selectInfo.start);
const endDate = new Date(selectInfo.end);
setEventForm({
title: "",
description: null,
start: startDate.toISOString(),
end: endDate.toISOString(),
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 response = await fetch("/api/events", {
method: selectedEvent ? "PUT" : "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...eventForm,
calendarId: selectedCalendarId,
userId,
}),
});
if (!response.ok) {
throw new Error("Failed to save event");
}
await fetchCalendars();
setIsEventModalOpen(false);
setEventForm({
title: "",
description: null,
start: "",
end: "",
allDay: false,
location: null,
calendarId: selectedCalendarId
});
} catch (error) {
console.error("Error saving event:", error);
setError(error instanceof Error ? error.message : "Failed to save event");
} finally {
setLoading(false);
}
};
const handleEventDelete = async () => {
if (!selectedEvent) return;
try {
setLoading(true);
const response = await fetch(`/api/events/${selectedEvent.id}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Failed to delete event");
}
await fetchCalendars();
setIsEventModalOpen(false);
setSelectedEvent(null);
} catch (error) {
console.error("Error deleting event:", error);
setError(error instanceof Error ? error.message : "Failed to delete event");
} finally {
setLoading(false);
}
};
const handleViewChange = (newView: "dayGridMonth" | "timeGridWeek" | "timeGridDay") => {
setView(newView);
if (calendarRef.current) {
const calendarApi = calendarRef.current.getApi();
calendarApi.changeView(newView);
}
};
// Add a calendar selector component
const CalendarSelector = () => (
<div className="flex flex-wrap items-center gap-2 mb-4">
{calendars.map((calendar) => (
<div key={calendar.id} className="relative group">
<Button
variant={calendar.id === selectedCalendarId ? "secondary" : "ghost"}
className="flex items-center gap-2 pr-8"
onClick={() => setSelectedCalendarId(calendar.id)}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: calendar.color }}
/>
<span>{calendar.name}</span>
</Button>
{calendar.name !== "Calendrier principal" && (
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation();
setSelectedCalendar(calendar);
setIsCalendarModalOpen(true);
}}
>
<Settings className="h-4 w-4" />
</Button>
)}
</div>
))}
</div>
);
// Add these helper functions for date handling
const getDateFromString = (dateString: string) => {
return dateString ? new Date(dateString) : new Date();
};
const handleStartDateChange = (date: Date | null) => {
if (!date) return;
const endDate = getDateFromString(eventForm.end);
if (date > endDate) {
// If start date is after end date, set end date to start date + 1 hour
const newEndDate = new Date(date);
newEndDate.setHours(date.getHours() + 1);
setEventForm({
...eventForm,
start: date.toISOString(),
end: newEndDate.toISOString(),
});
} else {
setEventForm({
...eventForm,
start: date.toISOString(),
});
}
};
const handleEndDateChange = (date: Date | null) => {
if (!date) return;
const startDate = getDateFromString(eventForm.start);
if (date < startDate) {
// If end date is before start date, set start date to end date - 1 hour
const newStartDate = new Date(date);
newStartDate.setHours(date.getHours() - 1);
setEventForm({
...eventForm,
start: newStartDate.toISOString(),
end: date.toISOString(),
});
} else {
setEventForm({
...eventForm,
end: date.toISOString(),
});
}
};
return (
<div className="space-y-4">
<Card className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={() => {
setSelectedCalendar(null);
setIsCalendarModalOpen(true);
}}
className="bg-white hover:bg-gray-50 text-gray-900 border-gray-200"
>
<Plus className="mr-2 h-4 w-4" />
<span className="font-medium">Nouveau calendrier</span>
</Button>
<Button
onClick={() => {
setSelectedEvent(null);
setEventForm({
title: "",
description: null,
start: "",
end: "",
allDay: false,
location: null,
calendarId: selectedCalendarId
});
setIsEventModalOpen(true);
}}
className="bg-primary hover:bg-primary/90 text-white"
>
<Plus className="mr-2 h-4 w-4" />
<span className="font-medium">Nouvel événement</span>
</Button>
</div>
<Tabs value={view} className="w-auto">
<TabsList>
<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>
</Tabs>
</div>
<CalendarSelector />
{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,
backgroundColor: cal.color,
textColor: '#ffffff',
borderColor: cal.color,
}))
)}
locale={frLocale}
selectable={true}
selectMirror={true}
dayMaxEvents={true}
weekends={true}
select={handleDateSelect}
eventClick={handleEventClick}
height="auto"
aspectRatio={1.8}
/>
)}
</Card>
{/* Calendar dialog */}
<CalendarDialog
open={isCalendarModalOpen}
onClose={() => setIsCalendarModalOpen(false)}
onSave={handleCalendarSave}
onDelete={handleCalendarDelete}
initialData={selectedCalendar || undefined}
/>
{/* Event dialog */}
<Dialog open={isEventModalOpen} onOpenChange={setIsEventModalOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
{selectedEvent ? "Modifier l'événement" : "Nouvel événement"}
</DialogTitle>
</DialogHeader>
<div className="space-y-6 py-4">
<div className="space-y-2">
<Label htmlFor="event-title">Titre</Label>
<Input
id="event-title"
value={eventForm.title}
onChange={(e) =>
setEventForm({ ...eventForm, title: e.target.value })
}
placeholder="Titre de l'événement"
className="w-full"
/>
</div>
<div className="space-y-2">
<Label htmlFor="event-description">Description</Label>
<Textarea
id="event-description"
value={eventForm.description || ""}
onChange={(e) =>
setEventForm({ ...eventForm, description: e.target.value || null })
}
placeholder="Description de l'événement"
className="w-full"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Début</Label>
<div className="relative">
<DatePicker
selected={getDateFromString(eventForm.start)}
onChange={handleStartDateChange}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="Pp"
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="Sélectionner la date et l'heure"
/>
</div>
</div>
<div className="space-y-2">
<Label>Fin</Label>
<div className="relative">
<DatePicker
selected={getDateFromString(eventForm.end)}
onChange={handleEndDateChange}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="Pp"
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="Sélectionner la date et l'heure"
minDate={getDateFromString(eventForm.start)}
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="event-location">Lieu</Label>
<Input
id="event-location"
value={eventForm.location || ""}
onChange={(e) =>
setEventForm({ ...eventForm, location: e.target.value || null })
}
placeholder="Lieu de l'événement"
className="w-full"
/>
</div>
</div>
<DialogFooter className="flex justify-between">
{selectedEvent && (
<Button
variant="destructive"
onClick={handleEventDelete}
disabled={loading}
>
<X className="w-4 h-4 mr-2" />
Supprimer
</Button>
)}
<div className="space-x-2">
<Button
variant="outline"
onClick={() => setIsEventModalOpen(false)}
disabled={loading}
>
Annuler
</Button>
<Button
onClick={handleEventSubmit}
disabled={loading || !eventForm.title}
className="bg-primary hover:bg-primary/90"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{selectedEvent ? "Modification..." : "Création..."}
</>
) : (
<>
<Check className="w-4 h-4 mr-2" />
{selectedEvent ? "Modifier" : "Créer"}
</>
)}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}