127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'sonner';
|
|
import FullCalendar from '@fullcalendar/react';
|
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
import interactionPlugin from '@fullcalendar/interaction';
|
|
import type { EventClickArg, DateSelectArg, EventDropArg } from '@fullcalendar/core';
|
|
import api, { getErrorMessage } from '@/lib/api';
|
|
import type { CalendarEvent } from '@/types';
|
|
import EventForm from './EventForm';
|
|
|
|
export default function CalendarPage() {
|
|
const queryClient = useQueryClient();
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null);
|
|
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
|
|
|
const { data: events = [] } = useQuery({
|
|
queryKey: ['calendar-events'],
|
|
queryFn: async () => {
|
|
const { data } = await api.get<CalendarEvent[]>('/events');
|
|
return data;
|
|
},
|
|
});
|
|
|
|
const eventDropMutation = useMutation({
|
|
mutationFn: async ({ id, start, end, allDay }: { id: number; start: string; end: string; allDay: boolean }) => {
|
|
const response = await api.put(`/events/${id}`, {
|
|
start_datetime: start,
|
|
end_datetime: end,
|
|
all_day: allDay,
|
|
});
|
|
return response.data;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
|
|
toast.success('Event moved');
|
|
},
|
|
onError: (error) => {
|
|
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
|
|
toast.error(getErrorMessage(error, 'Failed to move event'));
|
|
},
|
|
});
|
|
|
|
const calendarEvents = events.map((event) => ({
|
|
id: event.id.toString(),
|
|
title: event.title,
|
|
start: event.start_datetime,
|
|
end: event.end_datetime || undefined,
|
|
allDay: event.all_day,
|
|
backgroundColor: event.color || 'hsl(var(--accent-color))',
|
|
borderColor: event.color || 'hsl(var(--accent-color))',
|
|
}));
|
|
|
|
const handleEventClick = (info: EventClickArg) => {
|
|
const event = events.find((e) => e.id.toString() === info.event.id);
|
|
if (event) {
|
|
setEditingEvent(event);
|
|
setShowForm(true);
|
|
}
|
|
};
|
|
|
|
const toLocalDatetime = (d: Date): string => {
|
|
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
};
|
|
|
|
const handleEventDrop = (info: EventDropArg) => {
|
|
const id = parseInt(info.event.id);
|
|
const start = info.event.allDay
|
|
? info.event.startStr
|
|
: info.event.start ? toLocalDatetime(info.event.start) : info.event.startStr;
|
|
const end = info.event.allDay
|
|
? info.event.endStr || info.event.startStr
|
|
: info.event.end ? toLocalDatetime(info.event.end) : start;
|
|
eventDropMutation.mutate({ id, start, end, allDay: info.event.allDay });
|
|
};
|
|
|
|
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
|
setSelectedDate(selectInfo.startStr);
|
|
setShowForm(true);
|
|
};
|
|
|
|
const handleCloseForm = () => {
|
|
setShowForm(false);
|
|
setEditingEvent(null);
|
|
setSelectedDate(null);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<div className="border-b bg-card px-6 py-4">
|
|
<h1 className="text-3xl font-bold">Calendar</h1>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<div className="bg-card rounded-lg border p-4">
|
|
<FullCalendar
|
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
|
initialView="dayGridMonth"
|
|
headerToolbar={{
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,timeGridWeek,timeGridDay',
|
|
}}
|
|
events={calendarEvents}
|
|
editable={true}
|
|
selectable={true}
|
|
selectMirror={true}
|
|
dayMaxEvents={true}
|
|
weekends={true}
|
|
eventClick={handleEventClick}
|
|
eventDrop={handleEventDrop}
|
|
select={handleDateSelect}
|
|
height="auto"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{showForm && (
|
|
<EventForm event={editingEvent} initialDate={selectedDate} onClose={handleCloseForm} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|