UMBRA/frontend/src/components/calendar/CalendarPage.tsx
2026-02-15 16:13:41 +08:00

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>
);
}