cfb2b59584
* [#523] added way to manage attendance for delegated calendars * [DELEGATION] #523 & #524 changed eventURL calculation for delegated events to have the rigth one * [#524] added drag and drop authorisation to delegated event * [#524] added tests * [#524] support for the permission access * [#524] no edit when delegated event is not public
852 lines
26 KiB
TypeScript
852 lines
26 KiB
TypeScript
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
|
import { getCalendarDetailAsync } from "@/features/Calendars/services";
|
|
import EventPreviewModal from "@/features/Events/EventDisplayPreview";
|
|
import EventPopover from "@/features/Events/EventModal";
|
|
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
|
import ImportAlert from "@/features/Events/ImportAlert";
|
|
import SearchResultsPage from "@/features/Search/SearchResultsPage";
|
|
import { setTimeZone } from "@/features/Settings/SettingsSlice";
|
|
import { setDisplayedDateAndRange } from "@/utils/CalendarRangeManager";
|
|
import {
|
|
formatDateToYYYYMMDDTHHMMSS,
|
|
getCalendarRange,
|
|
} from "@/utils/dateUtils";
|
|
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
|
import { setSelectedCalendars as setSelectedCalendarsToStorage } from "@/utils/storage/setSelectedCalendars";
|
|
import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars";
|
|
import { browserDefaultTimeZone } from "@/utils/timezone";
|
|
import type { EventApi, LocaleInput } from "@fullcalendar/core";
|
|
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
|
import frLocale from "@fullcalendar/core/locales/fr";
|
|
import ruLocale from "@fullcalendar/core/locales/ru";
|
|
import viLocale from "@fullcalendar/core/locales/vi";
|
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
|
import interactionPlugin from "@fullcalendar/interaction";
|
|
import momentTimezonePlugin from "@fullcalendar/moment-timezone";
|
|
import FullCalendar from "@fullcalendar/react";
|
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
|
import { Box, Button, radius, useTheme } from "@linagora/twake-mui";
|
|
import AddIcon from "@mui/icons-material/Add";
|
|
import { MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
|
|
import { useI18n } from "twake-i18n";
|
|
import { User } from "../Attendees/PeopleSearch";
|
|
import { EventErrorSnackbar } from "../Error/ErrorSnackbar";
|
|
import { EventErrorHandler } from "../Error/EventErrorHandler";
|
|
import { EditModeDialog } from "../Event/EditModeDialog";
|
|
import "./Calendar.styl";
|
|
import CalendarSelection from "./CalendarSelection";
|
|
import "./CustomCalendar.styl";
|
|
import { useCalendarEventHandlers } from "./hooks/useCalendarEventHandlers";
|
|
import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
|
|
import { MiniCalendar } from "./MiniCalendar";
|
|
import { TempCalendarsInput } from "./TempCalendarsInput";
|
|
import { TimezoneSelector } from "./TimezoneSelector";
|
|
import { updateDarkColor } from "./utils/calendarColorsUtils";
|
|
import {
|
|
eventToFullCalendarFormat,
|
|
extractEvents,
|
|
updateSlotLabelVisibility,
|
|
} from "./utils/calendarUtils";
|
|
|
|
const localeMap: Record<string, LocaleInput | undefined> = {
|
|
fr: frLocale,
|
|
ru: ruLocale,
|
|
vi: viLocale,
|
|
en: undefined,
|
|
};
|
|
|
|
interface CalendarAppProps {
|
|
calendarRef: MutableRefObject<CalendarApi | null>;
|
|
onDateChange?: (date: Date) => void;
|
|
onViewChange?: (view: string) => void;
|
|
}
|
|
|
|
export default function CalendarApp({
|
|
calendarRef,
|
|
onDateChange,
|
|
onViewChange,
|
|
}: CalendarAppProps) {
|
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
|
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
|
const userId =
|
|
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
|
const dispatch = useAppDispatch();
|
|
const theme = useTheme();
|
|
const view = useAppSelector((state) => state.settings.view);
|
|
const userData = useAppSelector((state) => state.user.userData);
|
|
const hideDeclinedEvents = useAppSelector(
|
|
(state) => state.settings.hideDeclinedEvents
|
|
);
|
|
const calendars = useAppSelector((state) => state.calendars.list);
|
|
const isPending = useAppSelector((state) => state.calendars.pending);
|
|
const displayWeekNumbers = useAppSelector(
|
|
(state) => state.settings.displayWeekNumbers
|
|
);
|
|
const tempcalendars = useAppSelector((state) => state.calendars.templist);
|
|
const storedCalendars = useSelectedCalendars();
|
|
const [selectedCalendars, setSelectedCalendars] =
|
|
useState<string[]>(storedCalendars);
|
|
|
|
const calendarLightSignature = useMemo(() => {
|
|
return Object.values(calendars || {})
|
|
.map((cal) => `${cal.id}:${cal.color?.light ?? ""}`)
|
|
.sort()
|
|
.join("|");
|
|
}, [calendars]);
|
|
|
|
const calendarIdsString = useMemo(
|
|
() =>
|
|
Object.keys(calendars || {})
|
|
.sort()
|
|
.join(","),
|
|
[calendars]
|
|
);
|
|
const calendarIds = useMemo(
|
|
() => (calendarIdsString ? calendarIdsString.split(",") : []),
|
|
[calendarIdsString]
|
|
);
|
|
|
|
const dottedEvents: CalendarEvent[] = selectedCalendars.flatMap((calId) => {
|
|
const calendar = calendars[calId];
|
|
if (!calendar?.events) return [];
|
|
return Object.values(calendar.events);
|
|
});
|
|
|
|
const [currentView, setCurrentView] = useState("timeGridWeek");
|
|
const timezone =
|
|
useAppSelector((state) => state.settings.timeZone) ??
|
|
browserDefaultTimeZone;
|
|
|
|
const fetchedRangesRef = useRef<Record<string, string>>({});
|
|
|
|
// Auto-select personal calendars when first loaded
|
|
const initialLoadRef = useRef(true);
|
|
const [eventErrors, setEventErrors] = useState<string[]>([]);
|
|
const errorHandler = useRef(new EventErrorHandler());
|
|
|
|
// useEffect(() => {
|
|
// if (view === "search") {
|
|
// document.body.classList.add("dialog-expanded");
|
|
// } else {
|
|
// document.body.classList.remove("dialog-expanded");
|
|
// }
|
|
// }, [view]);
|
|
|
|
useEffect(() => {
|
|
const handler = errorHandler.current;
|
|
handler.setErrorCallback(setEventErrors);
|
|
return () => handler.setErrorCallback(() => {});
|
|
}, []);
|
|
|
|
const handleErrorClose = () => {
|
|
setEventErrors([]);
|
|
errorHandler.current.clearAll();
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (initialLoadRef.current && calendarIds.length > 0 && userId) {
|
|
const cached = localStorage.getItem("selectedCalendars");
|
|
if (cached && cached.length > 0) {
|
|
const parsed = JSON.parse(cached) as string[];
|
|
const valid = parsed.filter((id) => calendars[id]);
|
|
setSelectedCalendars(valid);
|
|
} else {
|
|
const personalCalendarIds = calendarIds.filter(
|
|
(id) => extractEventBaseUuid(id) === userId
|
|
);
|
|
setSelectedCalendars(personalCalendarIds);
|
|
}
|
|
initialLoadRef.current = false;
|
|
}
|
|
}, [calendarIds, calendars, userId]);
|
|
|
|
// Save selected cals to cache
|
|
useEffect(() => {
|
|
if (calendarIds.length > 0) {
|
|
setSelectedCalendarsToStorage(selectedCalendars);
|
|
}
|
|
}, [selectedCalendars, calendarIds.length]);
|
|
|
|
const prevCalendarLightSignature = useRef<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!calendarLightSignature) {
|
|
prevCalendarLightSignature.current = calendarLightSignature;
|
|
return;
|
|
}
|
|
|
|
if (prevCalendarLightSignature.current === calendarLightSignature) {
|
|
return;
|
|
}
|
|
|
|
prevCalendarLightSignature.current = calendarLightSignature;
|
|
updateDarkColor(calendars || {}, theme, dispatch);
|
|
}, [calendarLightSignature, calendars, theme, dispatch]);
|
|
|
|
useEffect(() => {
|
|
if (calendarIds.length === 0) return;
|
|
const validCalendarIds = new Set(calendarIds);
|
|
setSelectedCalendars((prev) => {
|
|
const filtered = prev.filter((calId) => validCalendarIds.has(calId));
|
|
if (filtered.length === prev.length) {
|
|
const unchanged = filtered.every((id, index) => id === prev[index]);
|
|
if (unchanged) {
|
|
return prev;
|
|
}
|
|
}
|
|
return filtered;
|
|
});
|
|
}, [calendarIds]);
|
|
|
|
const calendarRange = useMemo(
|
|
() => getCalendarRange(selectedDate),
|
|
[selectedDate]
|
|
);
|
|
|
|
const calendarRangeStart = calendarRange.start.getTime();
|
|
const calendarRangeEnd = calendarRange.end.getTime();
|
|
|
|
const rangeStart = useMemo(
|
|
() => formatDateToYYYYMMDDTHHMMSS(new Date(calendarRangeStart)),
|
|
[calendarRangeStart]
|
|
);
|
|
|
|
const rangeEnd = useMemo(
|
|
() => formatDateToYYYYMMDDTHHMMSS(new Date(calendarRangeEnd)),
|
|
[calendarRangeEnd]
|
|
);
|
|
|
|
// Create a stable string key for the range
|
|
const rangeKey = useMemo(
|
|
() => `${rangeStart}_${rangeEnd}`,
|
|
[rangeStart, rangeEnd]
|
|
);
|
|
|
|
const filteredEvents: CalendarEvent[] = extractEvents(
|
|
selectedCalendars,
|
|
calendars || {},
|
|
userData?.email,
|
|
hideDeclinedEvents
|
|
);
|
|
|
|
const tempCalendarIds = useMemo(
|
|
() => Object.keys(tempcalendars || {}).sort(),
|
|
[tempcalendars]
|
|
);
|
|
|
|
const filteredTempEvents: CalendarEvent[] = extractEvents(
|
|
tempCalendarIds,
|
|
tempcalendars || {},
|
|
userData?.email,
|
|
hideDeclinedEvents
|
|
);
|
|
|
|
const sortedSelectedCalendars = useMemo(
|
|
() => [...selectedCalendars].sort(),
|
|
[selectedCalendars]
|
|
);
|
|
|
|
const prefetchedCalendarsRef = useRef<Record<string, string>>({});
|
|
const tempFetchedRangesRef = useRef<Record<string, string>>({});
|
|
const activeLoadCompletedRef = useRef<boolean>(false);
|
|
const [activeLoadCompleted, setActiveLoadCompleted] =
|
|
useState<boolean>(false);
|
|
|
|
useEffect(() => {
|
|
activeLoadCompletedRef.current = false;
|
|
setActiveLoadCompleted(false);
|
|
}, [rangeKey]);
|
|
|
|
useEffect(() => {
|
|
if (isPending) return;
|
|
|
|
if (!rangeKey || sortedSelectedCalendars.length === 0) {
|
|
activeLoadCompletedRef.current = true;
|
|
setActiveLoadCompleted(true);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
const ACTIVE_BATCH_SIZE = 5;
|
|
|
|
const loadCalendars = async () => {
|
|
try {
|
|
const pendingIds = sortedSelectedCalendars.filter(
|
|
(id) => fetchedRangesRef.current[id] !== rangeKey
|
|
);
|
|
|
|
if (pendingIds.length === 0) {
|
|
activeLoadCompletedRef.current = true;
|
|
setActiveLoadCompleted(true);
|
|
return;
|
|
}
|
|
|
|
for (
|
|
let i = 0;
|
|
i < pendingIds.length && !cancelled;
|
|
i += ACTIVE_BATCH_SIZE
|
|
) {
|
|
const chunk = pendingIds.slice(i, i + ACTIVE_BATCH_SIZE);
|
|
|
|
chunk.forEach((id) => {
|
|
fetchedRangesRef.current[id] = rangeKey;
|
|
prefetchedCalendarsRef.current[id] = "active";
|
|
});
|
|
|
|
const requests = chunk.map(async (id) => {
|
|
try {
|
|
await dispatch(
|
|
getCalendarDetailAsync({
|
|
calId: id,
|
|
match: {
|
|
start: rangeStart,
|
|
end: rangeEnd,
|
|
},
|
|
})
|
|
).unwrap();
|
|
} catch {
|
|
fetchedRangesRef.current[id] = "";
|
|
}
|
|
});
|
|
|
|
await Promise.all(requests);
|
|
}
|
|
} finally {
|
|
if (!cancelled) {
|
|
activeLoadCompletedRef.current = true;
|
|
setActiveLoadCompleted(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
loadCalendars();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [dispatch, rangeKey, sortedSelectedCalendars, rangeStart, rangeEnd]);
|
|
|
|
useEffect(() => {
|
|
if (!rangeKey || !activeLoadCompleted || isPending) return;
|
|
const hiddenCalendars = calendarIds
|
|
.filter((id) => !selectedCalendars.includes(id))
|
|
.filter((id) => {
|
|
const prefetched = prefetchedCalendarsRef.current[id];
|
|
return prefetched !== rangeKey && prefetched !== "active";
|
|
});
|
|
|
|
if (hiddenCalendars.length === 0) return;
|
|
|
|
hiddenCalendars.forEach((id) => {
|
|
prefetchedCalendarsRef.current[id] = rangeKey;
|
|
dispatch(
|
|
getCalendarDetailAsync({
|
|
calId: id,
|
|
match: {
|
|
start: rangeStart,
|
|
end: rangeEnd,
|
|
},
|
|
})
|
|
)
|
|
.unwrap()
|
|
.catch(() => {
|
|
prefetchedCalendarsRef.current[id] = "";
|
|
});
|
|
});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [
|
|
calendarIdsString,
|
|
selectedCalendars,
|
|
rangeKey,
|
|
dispatch,
|
|
rangeStart,
|
|
rangeEnd,
|
|
activeLoadCompleted,
|
|
isPending,
|
|
]);
|
|
|
|
const calendarsWithClearedCache = useMemo(() => {
|
|
return selectedCalendars
|
|
.map((id) => {
|
|
const cleared = calendars[id]?.lastCacheCleared;
|
|
return cleared ? { id, cleared } : null;
|
|
})
|
|
.filter(Boolean) as { id: string; cleared: number }[];
|
|
}, [selectedCalendars, calendars]);
|
|
|
|
const processedCacheClearRef = useRef<Record<string, number>>({});
|
|
|
|
useEffect(() => {
|
|
if (isPending) return;
|
|
calendarsWithClearedCache.forEach(({ id, cleared }) => {
|
|
if (processedCacheClearRef.current[id] === cleared) {
|
|
return;
|
|
}
|
|
|
|
processedCacheClearRef.current[id] = cleared;
|
|
delete fetchedRangesRef.current[id];
|
|
prefetchedCalendarsRef.current[id] = "";
|
|
|
|
dispatch(
|
|
getCalendarDetailAsync({
|
|
calId: id,
|
|
match: {
|
|
start: rangeStart,
|
|
end: rangeEnd,
|
|
},
|
|
})
|
|
)
|
|
.unwrap()
|
|
.then(() => {
|
|
fetchedRangesRef.current[id] = rangeKey;
|
|
prefetchedCalendarsRef.current[id] = rangeKey;
|
|
})
|
|
.catch(() => {
|
|
fetchedRangesRef.current[id] = "";
|
|
prefetchedCalendarsRef.current[id] = "";
|
|
});
|
|
});
|
|
}, [
|
|
calendarsWithClearedCache,
|
|
dispatch,
|
|
rangeKey,
|
|
rangeStart,
|
|
rangeEnd,
|
|
isPending,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
const currentIds = new Set(tempCalendarIds);
|
|
Object.keys(tempFetchedRangesRef.current).forEach((id) => {
|
|
if (!currentIds.has(id)) {
|
|
delete tempFetchedRangesRef.current[id];
|
|
}
|
|
});
|
|
}, [tempCalendarIds]);
|
|
|
|
useEffect(() => {
|
|
if (!rangeKey || tempCalendarIds.length === 0) return;
|
|
|
|
let cancelled = false;
|
|
const TEMP_BATCH_SIZE = 5;
|
|
const loadTempCalendars = async () => {
|
|
const pendingIds = tempCalendarIds.filter(
|
|
(id) => tempFetchedRangesRef.current[id] !== rangeKey
|
|
);
|
|
|
|
if (pendingIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
for (
|
|
let i = 0;
|
|
i < pendingIds.length && !cancelled;
|
|
i += TEMP_BATCH_SIZE
|
|
) {
|
|
const chunk = pendingIds.slice(i, i + TEMP_BATCH_SIZE);
|
|
chunk.forEach((id) => {
|
|
tempFetchedRangesRef.current[id] = rangeKey;
|
|
});
|
|
|
|
const requests = chunk.map(async (id) => {
|
|
try {
|
|
await dispatch(
|
|
getCalendarDetailAsync({
|
|
calId: id,
|
|
match: {
|
|
start: rangeStart,
|
|
end: rangeEnd,
|
|
},
|
|
calType: "temp",
|
|
})
|
|
).unwrap();
|
|
} catch {
|
|
tempFetchedRangesRef.current[id] = "";
|
|
}
|
|
});
|
|
|
|
await Promise.all(requests);
|
|
}
|
|
};
|
|
|
|
loadTempCalendars();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [dispatch, rangeKey, tempCalendarIds, rangeStart, rangeEnd]);
|
|
|
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
|
const [openEventDisplay, setOpenEventDisplay] = useState(false);
|
|
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
|
const [eventDisplayedTemp, setEventDisplayedTemp] = useState(false);
|
|
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
|
|
|
// Listen for eventModalError event to reopen modal on API failure
|
|
useEffect(() => {
|
|
const handleEventModalError = (event: CustomEvent) => {
|
|
if (event.detail?.type === "create") {
|
|
// Reopen create event modal
|
|
setAnchorEl(document.body);
|
|
} else if (event.detail?.type === "update") {
|
|
// Store update modal info to sessionStorage for EventDisplayPreview to pick up
|
|
try {
|
|
sessionStorage.setItem(
|
|
"eventUpdateModalReopen",
|
|
JSON.stringify({
|
|
eventId: event.detail.eventId,
|
|
calId: event.detail.calId,
|
|
typeOfAction: event.detail.typeOfAction,
|
|
timestamp: Date.now(),
|
|
})
|
|
);
|
|
|
|
// Open EventDisplayPreview if it's not already open with matching event, so it can pick up the sessionStorage
|
|
if (
|
|
!openEventDisplay ||
|
|
eventDisplayedId !== event.detail.eventId ||
|
|
eventDisplayedCalId !== event.detail.calId
|
|
) {
|
|
setEventDisplayedId(event.detail.eventId);
|
|
setEventDisplayedCalId(event.detail.calId);
|
|
setEventDisplayedTemp(false);
|
|
setOpenEventDisplay(true);
|
|
} else {
|
|
// If EventDisplayPreview is already open, trigger reopen by dispatching a custom event
|
|
window.dispatchEvent(
|
|
new CustomEvent("eventUpdateModalReopen", {
|
|
detail: {
|
|
eventId: event.detail.eventId,
|
|
calId: event.detail.calId,
|
|
typeOfAction: event.detail.typeOfAction,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
} catch {
|
|
// Ignore sessionStorage errors
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener(
|
|
"eventModalError",
|
|
handleEventModalError as EventListener
|
|
);
|
|
return () => {
|
|
window.removeEventListener(
|
|
"eventModalError",
|
|
handleEventModalError as EventListener
|
|
);
|
|
};
|
|
}, [openEventDisplay, eventDisplayedId, eventDisplayedCalId]);
|
|
|
|
const [openEditModePopup, setOpenEditModePopup] = useState<string | null>(
|
|
null
|
|
);
|
|
const [, setTypeOfAction] = useState<"solo" | "all" | undefined>(undefined);
|
|
const [afterChoiceFunc, setAfterChoiceFunc] = useState<
|
|
((type: "solo" | "all" | undefined) => void) | undefined
|
|
>();
|
|
const [, setSelectedEvent] = useState<CalendarEvent>({} as CalendarEvent);
|
|
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(
|
|
null
|
|
);
|
|
|
|
const [tempUsers, setTempUsers] = useState<User[]>([]);
|
|
const [tempEvent, setTempEvent] = useState<CalendarEvent>(
|
|
{} as CalendarEvent
|
|
);
|
|
|
|
// Event handlers
|
|
const eventHandlers = useCalendarEventHandlers({
|
|
setSelectedRange,
|
|
setAnchorEl,
|
|
calendarRef,
|
|
dispatch,
|
|
setOpenEventDisplay,
|
|
setEventDisplayedId,
|
|
setEventDisplayedCalId,
|
|
setEventDisplayedTemp,
|
|
calendars,
|
|
setSelectedEvent,
|
|
setAfterChoiceFunc,
|
|
setOpenEditModePopup,
|
|
tempUsers,
|
|
setTempEvent,
|
|
timezone,
|
|
});
|
|
|
|
// View handlers
|
|
const viewHandlers = useCalendarViewHandlers({
|
|
calendarRef,
|
|
setSelectedDate,
|
|
setSelectedMiniDate,
|
|
onViewChange,
|
|
calendars,
|
|
tempcalendars,
|
|
errorHandler: errorHandler.current,
|
|
});
|
|
|
|
if (process.env.NODE_ENV === "test") {
|
|
window.__calendarRef = calendarRef;
|
|
}
|
|
|
|
const { t, lang } = useI18n();
|
|
|
|
return (
|
|
<main className="main-layout calendar-layout">
|
|
<Box
|
|
className="sidebar"
|
|
sx={{
|
|
paddingTop: 0,
|
|
paddingBottom: 3,
|
|
paddingLeft: 3,
|
|
paddingRight: 2,
|
|
width: "270px",
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
position: "sticky",
|
|
top: 0,
|
|
zIndex: 10,
|
|
backgroundColor: "#fff",
|
|
paddingTop: 3,
|
|
}}
|
|
>
|
|
<Button
|
|
size="medium"
|
|
variant="contained"
|
|
fullWidth
|
|
onClick={() =>
|
|
eventHandlers.handleDateSelect(null as unknown as DateSelectArg)
|
|
}
|
|
sx={{
|
|
borderRadius: radius.lg,
|
|
fontSize: "16px",
|
|
fontWeight: 500,
|
|
lineHeight: "normal",
|
|
}}
|
|
>
|
|
<AddIcon sx={{ marginRight: 0.5, fontSize: "20px" }} />{" "}
|
|
{t("event.createEvent")}
|
|
</Button>
|
|
</Box>
|
|
|
|
<MiniCalendar
|
|
calendarRef={calendarRef}
|
|
selectedDate={selectedMiniDate}
|
|
setSelectedMiniDate={setSelectedMiniDate}
|
|
dottedEvents={dottedEvents}
|
|
/>
|
|
<Box sx={{ mb: 3, mt: 2 }}>
|
|
<TempCalendarsInput
|
|
tempUsers={tempUsers}
|
|
setTempUsers={setTempUsers}
|
|
handleToggleEventPreview={() => {
|
|
eventHandlers.handleDateSelect(null as unknown as DateSelectArg);
|
|
}}
|
|
selectedCalendars={selectedCalendars}
|
|
setSelectedCalendars={setSelectedCalendars}
|
|
/>
|
|
</Box>
|
|
<div className="calendarList">
|
|
<CalendarSelection
|
|
selectedCalendars={selectedCalendars}
|
|
setSelectedCalendars={setSelectedCalendars}
|
|
/>
|
|
</div>
|
|
</Box>
|
|
<div className="calendar">
|
|
<ImportAlert />
|
|
{view === "calendar" && (
|
|
<FullCalendar
|
|
ref={(ref) => {
|
|
if (ref) {
|
|
calendarRef.current = ref.getApi();
|
|
}
|
|
}}
|
|
plugins={[
|
|
dayGridPlugin,
|
|
timeGridPlugin,
|
|
interactionPlugin,
|
|
momentTimezonePlugin,
|
|
]}
|
|
initialView="timeGridWeek"
|
|
firstDay={1}
|
|
editable={true}
|
|
locale={localeMap[lang]}
|
|
selectable={true}
|
|
timeZone={timezone}
|
|
height={"100%"}
|
|
select={eventHandlers.handleDateSelect}
|
|
nowIndicator
|
|
slotLabelClassNames={(arg) => [
|
|
updateSlotLabelVisibility(new Date(), arg, timezone),
|
|
]}
|
|
nowIndicatorContent={viewHandlers.handleNowIndicatorContent}
|
|
headerToolbar={false}
|
|
views={{
|
|
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
|
}}
|
|
dayMaxEvents={true}
|
|
events={eventToFullCalendarFormat(
|
|
filteredEvents,
|
|
filteredTempEvents,
|
|
userId,
|
|
userData?.email,
|
|
isPending,
|
|
calendars
|
|
)}
|
|
eventOrder={(a: EventApi, b: EventApi) =>
|
|
a.extendedProps.priority - b.extendedProps.priority
|
|
}
|
|
weekNumbers={
|
|
currentView === "timeGridWeek" || currentView === "timeGridDay"
|
|
}
|
|
weekNumberFormat={{ week: "long" }}
|
|
weekNumberContent={(arg) => {
|
|
return (
|
|
<div className="weekSelector">
|
|
{displayWeekNumbers && (
|
|
<div>
|
|
{t("menubar.views.week")} {arg.num}
|
|
</div>
|
|
)}
|
|
<TimezoneSelector
|
|
value={timezone}
|
|
referenceDate={calendarRef.current?.getDate() ?? new Date()}
|
|
onChange={(newTimezone: string) =>
|
|
dispatch(setTimeZone(newTimezone))
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
}}
|
|
dayCellContent={(arg) => {
|
|
const month = arg.date.toLocaleDateString(t("locale"), {
|
|
month: "short",
|
|
});
|
|
if (arg.view.type === "dayGridMonth") {
|
|
return (
|
|
<span
|
|
className={`fc-daygrid-day-number ${
|
|
arg.isToday ? "current-date" : ""
|
|
}`}
|
|
>
|
|
{arg.dayNumberText === "1" ? month : ""} {arg.dayNumberText}
|
|
</span>
|
|
);
|
|
}
|
|
}}
|
|
slotDuration={"00:30:00"}
|
|
slotLabelInterval={"01:00:00"}
|
|
scrollTime="12:00:00"
|
|
unselectAuto={false}
|
|
allDayText=""
|
|
slotLabelFormat={{
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
}}
|
|
datesSet={(arg) => {
|
|
setCurrentView(arg.view.type);
|
|
const calendarCurrentDate =
|
|
calendarRef.current?.getDate() || new Date(arg.start);
|
|
setDisplayedDateAndRange(calendarCurrentDate);
|
|
|
|
if (arg.view.type === "dayGridMonth") {
|
|
const start = new Date(arg.start).getTime();
|
|
const end = new Date(arg.end).getTime();
|
|
const middle = start + (end - start) / 2;
|
|
setSelectedDate(new Date(middle));
|
|
setSelectedMiniDate(calendarCurrentDate);
|
|
} else {
|
|
setSelectedDate(calendarCurrentDate);
|
|
setSelectedMiniDate(calendarCurrentDate);
|
|
}
|
|
|
|
// Always use the calendar's current date for consistency
|
|
if (onDateChange) {
|
|
onDateChange(calendarCurrentDate);
|
|
}
|
|
|
|
// Notify parent about view change
|
|
if (onViewChange) {
|
|
onViewChange(arg.view.type);
|
|
}
|
|
|
|
// Update slot label visibility when view changes
|
|
setTimeout(() => {
|
|
updateSlotLabelVisibility(new Date());
|
|
}, 100);
|
|
}}
|
|
dayHeaderContent={(arg) => {
|
|
const date = arg.date.getDate();
|
|
const weekDay = arg.date
|
|
.toLocaleDateString(t("locale"), { weekday: "short" })
|
|
.toUpperCase();
|
|
return (
|
|
<div className="fc-daygrid-day-top">
|
|
<small>{weekDay}</small>
|
|
{arg.view.type !== "dayGridMonth" && (
|
|
<span
|
|
className={`fc-daygrid-day-number ${
|
|
arg.isToday ? "current-date" : ""
|
|
}`}
|
|
>
|
|
{date}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}}
|
|
dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount}
|
|
dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount}
|
|
viewDidMount={viewHandlers.handleViewDidMount}
|
|
viewWillUnmount={viewHandlers.handleViewWillUnmount}
|
|
eventClick={eventHandlers.handleEventClick}
|
|
eventAllow={eventHandlers.handleEventAllow}
|
|
eventDrop={eventHandlers.handleEventDrop}
|
|
eventResize={eventHandlers.handleEventResize}
|
|
eventContent={viewHandlers.handleEventContent}
|
|
eventDidMount={viewHandlers.handleEventDidMount}
|
|
/>
|
|
)}
|
|
{view === "search" && <SearchResultsPage />}
|
|
<EventPopover
|
|
anchorEl={anchorEl}
|
|
open={Boolean(anchorEl)}
|
|
onClose={eventHandlers.handleClosePopover}
|
|
selectedRange={selectedRange}
|
|
setSelectedRange={setSelectedRange}
|
|
calendarRef={calendarRef}
|
|
event={tempEvent}
|
|
/>
|
|
<EditModeDialog
|
|
type={openEditModePopup}
|
|
setOpen={setOpenEditModePopup}
|
|
eventAction={(type: "solo" | "all" | undefined) => {
|
|
setTypeOfAction(type);
|
|
if (afterChoiceFunc) {
|
|
afterChoiceFunc(type);
|
|
}
|
|
}}
|
|
/>
|
|
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
|
|
<EventPreviewModal
|
|
eventId={eventDisplayedId}
|
|
calId={eventDisplayedCalId}
|
|
tempEvent={eventDisplayedTemp}
|
|
open={openEventDisplay}
|
|
onClose={eventHandlers.handleCloseEventDisplay}
|
|
/>
|
|
)}
|
|
<EventErrorSnackbar messages={eventErrors} onClose={handleErrorClose} />
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|