Compare commits
3 Commits
901c766ced
...
c5adc316ef
| Author | SHA1 | Date | |
|---|---|---|---|
| c5adc316ef | |||
| d5080f59af | |||
| a352a50b63 |
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 || '',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user