diff --git a/frontend/src/components/calendar/CalendarPage.tsx b/frontend/src/components/calendar/CalendarPage.tsx index 5190351..609bdfe 100644 --- a/frontend/src/components/calendar/CalendarPage.tsx +++ b/frontend/src/components/calendar/CalendarPage.tsx @@ -14,7 +14,9 @@ export default function CalendarPage() { const queryClient = useQueryClient(); const [showForm, setShowForm] = useState(false); const [editingEvent, setEditingEvent] = useState(null); - const [selectedDate, setSelectedDate] = useState(null); + const [selectedStart, setSelectedStart] = useState(null); + const [selectedEnd, setSelectedEnd] = useState(null); + const [selectedAllDay, setSelectedAllDay] = useState(false); const { data: events = [] } = useQuery({ queryKey: ['calendar-events'], @@ -24,8 +26,43 @@ export default function CalendarPage() { }, }); - const eventDropMutation = useMutation({ - mutationFn: async ({ id, start, end, allDay }: { id: number; start: string; end: string; allDay: boolean }) => { + 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 updateEventTimes = ( + id: number, + start: string, + end: string, + allDay: boolean, + revert: () => void, + ) => { + // Optimistically update cache so re-renders don't snap back + queryClient.setQueryData(['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}`, { start_datetime: start, end_datetime: end, @@ -35,11 +72,12 @@ export default function CalendarPage() { }, onSuccess: () => { 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'] }); - toast.error(getErrorMessage(error, 'Failed to move event')); + toast.error(getErrorMessage(error, 'Failed to update event')); }, }); @@ -61,31 +99,49 @@ 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 id = parseInt(info.event.id); const start = info.event.allDay ? 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 ? info.event.endStr || info.event.startStr - : info.event.end ? toLocalDatetime(info.event.end) : start; - eventDropMutation.mutate({ id, start, end, allDay: info.event.allDay }); + : info.event.end + ? 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) => { - setSelectedDate(selectInfo.startStr); + setSelectedStart(selectInfo.startStr); + setSelectedEnd(selectInfo.endStr); + setSelectedAllDay(selectInfo.allDay); setShowForm(true); }; const handleCloseForm = () => { setShowForm(false); setEditingEvent(null); - setSelectedDate(null); + setSelectedStart(null); + setSelectedEnd(null); + setSelectedAllDay(false); }; return ( @@ -112,6 +168,7 @@ export default function CalendarPage() { weekends={true} eventClick={handleEventClick} eventDrop={handleEventDrop} + eventResize={handleEventResize} select={handleDateSelect} height="auto" /> @@ -119,7 +176,13 @@ export default function CalendarPage() { {showForm && ( - + )} ); diff --git a/frontend/src/components/calendar/EventForm.tsx b/frontend/src/components/calendar/EventForm.tsx index da552f0..0d37c96 100644 --- a/frontend/src/components/calendar/EventForm.tsx +++ b/frontend/src/components/calendar/EventForm.tsx @@ -20,7 +20,9 @@ import { Checkbox } from '@/components/ui/checkbox'; interface EventFormProps { event: CalendarEvent | null; - initialDate?: string | null; + initialStart?: string | null; + initialEnd?: string | null; + initialAllDay?: boolean; onClose: () => void; } @@ -54,11 +56,11 @@ function formatForInput(dt: string, allDay: boolean, fallbackTime: string = '09: 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 isAllDay = event?.all_day ?? false; - const rawStart = event?.start_datetime || initialDate || ''; - const rawEnd = event?.end_datetime || initialDate || ''; + const isAllDay = event?.all_day ?? initialAllDay ?? false; + const rawStart = event?.start_datetime || initialStart || ''; + const rawEnd = event?.end_datetime || initialEnd || ''; const [formData, setFormData] = useState({ title: event?.title || '', description: event?.description || '',