Compare commits

...

3 Commits

Author SHA1 Message Date
c5adc316ef resolved event create bug 2026-02-19 20:44:14 +08:00
d5080f59af fixed day view visual bug 2026-02-19 20:38:11 +08:00
a352a50b63 resolved click & drag bug 2026-02-19 20:24:41 +08:00
2 changed files with 92 additions and 23 deletions

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
import FullCalendar from '@fullcalendar/react'; import FullCalendar from '@fullcalendar/react';
@ -12,9 +12,12 @@ import EventForm from './EventForm';
export default function CalendarPage() { export default function CalendarPage() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const calendarRef = useRef<FullCalendar>(null);
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null); const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null);
const [selectedDate, setSelectedDate] = useState<string | null>(null); const [selectedStart, setSelectedStart] = useState<string | null>(null);
const [selectedEnd, setSelectedEnd] = useState<string | null>(null);
const [selectedAllDay, setSelectedAllDay] = useState(false);
const { data: events = [] } = useQuery({ const { data: events = [] } = useQuery({
queryKey: ['calendar-events'], queryKey: ['calendar-events'],
@ -24,8 +27,43 @@ export default function CalendarPage() {
}, },
}); });
const eventDropMutation = useMutation({ const toLocalDatetime = (d: Date): string => {
mutationFn: async ({ id, start, end, allDay }: { id: number; start: string; end: string; allDay: boolean }) => { 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 updateEventTimes = (
id: number,
start: string,
end: string,
allDay: boolean,
revert: () => void,
) => {
// Optimistically update cache so re-renders don't snap back
queryClient.setQueryData<CalendarEvent[]>(['calendar-events'], (old) =>
old?.map((e) =>
e.id === id
? { ...e, start_datetime: start, end_datetime: end, all_day: allDay }
: e,
),
);
eventMutation.mutate({ id, start, end, allDay, revert });
};
const eventMutation = useMutation({
mutationFn: async ({
id,
start,
end,
allDay,
}: {
id: number;
start: string;
end: string;
allDay: boolean;
revert: () => void;
}) => {
const response = await api.put(`/events/${id}`, { const response = await api.put(`/events/${id}`, {
start_datetime: start, start_datetime: start,
end_datetime: end, end_datetime: end,
@ -35,11 +73,12 @@ export default function CalendarPage() {
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['calendar-events'] }); queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
toast.success('Event moved'); toast.success('Event updated');
}, },
onError: (error) => { onError: (error, variables) => {
variables.revert();
queryClient.invalidateQueries({ queryKey: ['calendar-events'] }); queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
toast.error(getErrorMessage(error, 'Failed to move event')); toast.error(getErrorMessage(error, 'Failed to update event'));
}, },
}); });
@ -61,31 +100,50 @@ export default function CalendarPage() {
} }
}; };
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 handleEventDrop = (info: EventDropArg) => {
const id = parseInt(info.event.id); const id = parseInt(info.event.id);
const start = info.event.allDay const start = info.event.allDay
? info.event.startStr ? info.event.startStr
: info.event.start ? toLocalDatetime(info.event.start) : info.event.startStr; : info.event.start
? toLocalDatetime(info.event.start)
: info.event.startStr;
const end = info.event.allDay const end = info.event.allDay
? info.event.endStr || info.event.startStr ? info.event.endStr || info.event.startStr
: info.event.end ? toLocalDatetime(info.event.end) : start; : info.event.end
eventDropMutation.mutate({ id, start, end, allDay: info.event.allDay }); ? toLocalDatetime(info.event.end)
: start;
updateEventTimes(id, start, end, info.event.allDay, info.revert);
};
const handleEventResize = (info: { event: EventDropArg['event']; revert: () => void }) => {
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;
updateEventTimes(id, start, end, info.event.allDay, info.revert);
}; };
const handleDateSelect = (selectInfo: DateSelectArg) => { const handleDateSelect = (selectInfo: DateSelectArg) => {
setSelectedDate(selectInfo.startStr); setSelectedStart(selectInfo.startStr);
setSelectedEnd(selectInfo.endStr);
setSelectedAllDay(selectInfo.allDay);
setShowForm(true); setShowForm(true);
}; };
const handleCloseForm = () => { const handleCloseForm = () => {
calendarRef.current?.getApi().unselect();
setShowForm(false); setShowForm(false);
setEditingEvent(null); setEditingEvent(null);
setSelectedDate(null); setSelectedStart(null);
setSelectedEnd(null);
setSelectedAllDay(false);
}; };
return ( return (
@ -97,6 +155,7 @@ export default function CalendarPage() {
<div className="flex-1 overflow-y-auto p-6"> <div className="flex-1 overflow-y-auto p-6">
<div className="bg-card rounded-lg border p-4"> <div className="bg-card rounded-lg border p-4">
<FullCalendar <FullCalendar
ref={calendarRef}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]} plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth" initialView="dayGridMonth"
headerToolbar={{ headerToolbar={{
@ -108,10 +167,12 @@ export default function CalendarPage() {
editable={true} editable={true}
selectable={true} selectable={true}
selectMirror={true} selectMirror={true}
unselectAuto={false}
dayMaxEvents={true} dayMaxEvents={true}
weekends={true} weekends={true}
eventClick={handleEventClick} eventClick={handleEventClick}
eventDrop={handleEventDrop} eventDrop={handleEventDrop}
eventResize={handleEventResize}
select={handleDateSelect} select={handleDateSelect}
height="auto" height="auto"
/> />
@ -119,7 +180,13 @@ export default function CalendarPage() {
</div> </div>
{showForm && ( {showForm && (
<EventForm event={editingEvent} initialDate={selectedDate} onClose={handleCloseForm} /> <EventForm
event={editingEvent}
initialStart={selectedStart}
initialEnd={selectedEnd}
initialAllDay={selectedAllDay}
onClose={handleCloseForm}
/>
)} )}
</div> </div>
); );

View File

@ -20,7 +20,9 @@ import { Checkbox } from '@/components/ui/checkbox';
interface EventFormProps { interface EventFormProps {
event: CalendarEvent | null; event: CalendarEvent | null;
initialDate?: string | null; initialStart?: string | null;
initialEnd?: string | null;
initialAllDay?: boolean;
onClose: () => void; onClose: () => void;
} }
@ -54,11 +56,11 @@ function formatForInput(dt: string, allDay: boolean, fallbackTime: string = '09:
return allDay ? toDateOnly(dt) : toDatetimeLocal(dt, fallbackTime); return allDay ? toDateOnly(dt) : toDatetimeLocal(dt, fallbackTime);
} }
export default function EventForm({ event, initialDate, onClose }: EventFormProps) { export default function EventForm({ event, initialStart, initialEnd, initialAllDay, onClose }: EventFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isAllDay = event?.all_day ?? false; const isAllDay = event?.all_day ?? initialAllDay ?? false;
const rawStart = event?.start_datetime || initialDate || ''; const rawStart = event?.start_datetime || initialStart || '';
const rawEnd = event?.end_datetime || initialDate || ''; const rawEnd = event?.end_datetime || initialEnd || '';
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
title: event?.title || '', title: event?.title || '',
description: event?.description || '', description: event?.description || '',