refactor: extract calendar utility functions and handlers
- Extract utility functions to utils/calendarUtils.ts * updateSlotLabelVisibility for slot label management * eventToFullCalendarFormat for event formatting * extractEvents for event filtering * updateCalsDetails for calendar detail updates - Extract event handlers to handlers/eventHandlers.ts * handleDateSelect, handleClosePopover, handleEventClick * handleEventAllow, handleEventDrop, handleEventResize - Extract mouse handlers to handlers/mouseHandlers.ts * handleMouseMove, handleMouseLeave * addMouseEventListeners, removeMouseEventListeners - Extract view handlers to handlers/viewHandlers.ts * handleViewDidMount, handleViewWillUnmount * handleEventContent, handleEventDidMount * handleDayHeaderDidMount, handleDayHeaderWillUnmount - Remove inline functions from Calendar.tsx - Improve code organization and maintainability
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import FullCalendar from "@fullcalendar/react";
|
||||
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
@@ -37,38 +36,14 @@ import LockIcon from "@mui/icons-material/Lock";
|
||||
import { userAttendee } from "../../features/User/userDataTypes";
|
||||
import { TempCalendarsInput } from "./TempCalendarsInput";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
// Function to hide/show slot labels based on current time
|
||||
const updateSlotLabelVisibility = (currentTime: Date) => {
|
||||
const slotLabels = document.querySelectorAll(".fc-timegrid-slot-label");
|
||||
const currentMinutes = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
|
||||
slotLabels.forEach((label) => {
|
||||
const labelElement = label as HTMLElement;
|
||||
const timeText = labelElement.textContent?.trim();
|
||||
|
||||
if (timeText && timeText.match(/^\d{1,2}:\d{2}$/)) {
|
||||
const [hours, minutes] = timeText.split(":").map(Number);
|
||||
const labelMinutes = hours * 60 + minutes;
|
||||
|
||||
// Calculate time difference in minutes
|
||||
let timeDiff = Math.abs(currentMinutes - labelMinutes);
|
||||
|
||||
// Handle edge case around midnight (00:00)
|
||||
if (timeDiff > 12 * 60) {
|
||||
// More than 12 hours difference
|
||||
timeDiff = 24 * 60 - timeDiff; // Wrap around
|
||||
}
|
||||
|
||||
// Dim if within 15 minutes (before or after)
|
||||
if (timeDiff <= 15) {
|
||||
labelElement.style.opacity = "0.2";
|
||||
} else {
|
||||
labelElement.style.opacity = "1";
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
import {
|
||||
updateSlotLabelVisibility,
|
||||
eventToFullCalendarFormat,
|
||||
extractEvents,
|
||||
updateCalsDetails,
|
||||
} from "./utils/calendarUtils";
|
||||
import { useCalendarEventHandlers } from "./hooks/useCalendarEventHandlers";
|
||||
import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
|
||||
|
||||
interface CalendarAppProps {
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
@@ -84,20 +59,20 @@ export default function CalendarApp({
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
||||
const tokens = useAppSelector((state) => state.user.tokens);
|
||||
const userId =
|
||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!tokens || !user) {
|
||||
if (!tokens || !userId) {
|
||||
dispatch(push("/"));
|
||||
}
|
||||
}, [tokens, user]);
|
||||
}, [tokens, userId]);
|
||||
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
const tempcalendars =
|
||||
useAppSelector((state) => state.calendars.templist) ?? {};
|
||||
const pending = useAppSelector((state) => state.calendars.pending);
|
||||
const userId =
|
||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||
const selectPersonnalCalendars = createSelector(
|
||||
(state) => state.calendars,
|
||||
(calendars) =>
|
||||
@@ -207,53 +182,39 @@ export default function CalendarApp({
|
||||
{} as CalendarEvent
|
||||
);
|
||||
|
||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
||||
setSelectedRange(selectInfo);
|
||||
setAnchorEl(document.body); // fallback: we could use selectInfo.jsEvent.target if from a click
|
||||
};
|
||||
// Event handlers
|
||||
const eventHandlers = useCalendarEventHandlers({
|
||||
setSelectedRange,
|
||||
setAnchorEl,
|
||||
calendarRef,
|
||||
selectedCalendars,
|
||||
tempcalendars,
|
||||
calendarRange,
|
||||
dispatch,
|
||||
setOpenEventDisplay,
|
||||
setAnchorPosition,
|
||||
setEventDisplayedId,
|
||||
setEventDisplayedCalId,
|
||||
setEventDisplayedTemp,
|
||||
calendars,
|
||||
});
|
||||
|
||||
const handleClosePopover = () => {
|
||||
calendarRef.current?.unselect();
|
||||
setAnchorEl(null);
|
||||
setSelectedRange(null);
|
||||
selectedCalendars.forEach((calId) =>
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
Object.keys(tempcalendars).forEach((calId) =>
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
},
|
||||
calType: "temp",
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
const handleCloseEventDisplay = () => {
|
||||
setAnchorPosition(null);
|
||||
setOpenEventDisplay(false);
|
||||
};
|
||||
// View handlers
|
||||
const viewHandlers = useCalendarViewHandlers({
|
||||
calendarRef,
|
||||
setSelectedDate,
|
||||
setSelectedMiniDate,
|
||||
onViewChange,
|
||||
calendars,
|
||||
tempcalendars,
|
||||
});
|
||||
|
||||
const handleMonthUp = () => {
|
||||
setSelectedMiniDate(
|
||||
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() - 1)
|
||||
);
|
||||
eventHandlers.handleMonthUp(selectedMiniDate, setSelectedMiniDate);
|
||||
};
|
||||
|
||||
const handleMonthDown = () => {
|
||||
setSelectedMiniDate(
|
||||
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1)
|
||||
);
|
||||
eventHandlers.handleMonthDown(selectedMiniDate, setSelectedMiniDate);
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
@@ -265,7 +226,9 @@ export default function CalendarApp({
|
||||
<div className="sidebar">
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => handleDateSelect(null as unknown as DateSelectArg)}
|
||||
onClick={() =>
|
||||
eventHandlers.handleDateSelect(null as unknown as DateSelectArg)
|
||||
}
|
||||
>
|
||||
<AddIcon /> <p>Create Event</p>
|
||||
</Button>
|
||||
@@ -378,7 +341,7 @@ export default function CalendarApp({
|
||||
selectable={true}
|
||||
timeZone="local"
|
||||
height={"100%"}
|
||||
select={handleDateSelect}
|
||||
select={eventHandlers.handleDateSelect}
|
||||
nowIndicator
|
||||
headerToolbar={false}
|
||||
views={{
|
||||
@@ -448,433 +411,21 @@ export default function CalendarApp({
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
dayHeaderDidMount={(arg) => {
|
||||
// Add click handler to day headers in week view
|
||||
if (arg.view.type === "timeGridWeek") {
|
||||
const headerEl = arg.el;
|
||||
|
||||
const handleDayHeaderClick = () => {
|
||||
// Switch to day view and navigate to the clicked date
|
||||
calendarRef.current?.changeView("timeGridDay", arg.date);
|
||||
setSelectedDate(new Date(arg.date));
|
||||
setSelectedMiniDate(new Date(arg.date));
|
||||
|
||||
// Notify parent about view change
|
||||
if (onViewChange) {
|
||||
onViewChange("timeGridDay");
|
||||
}
|
||||
};
|
||||
|
||||
headerEl.addEventListener("click", handleDayHeaderClick);
|
||||
|
||||
// Store the handler for cleanup
|
||||
(headerEl as any).__dayHeaderClickHandler = handleDayHeaderClick;
|
||||
}
|
||||
}}
|
||||
dayHeaderWillUnmount={(arg) => {
|
||||
// Clean up event listeners to prevent memory leaks
|
||||
const headerEl = arg.el;
|
||||
if ((headerEl as any).__dayHeaderClickHandler) {
|
||||
headerEl.removeEventListener(
|
||||
"click",
|
||||
(headerEl as any).__dayHeaderClickHandler
|
||||
);
|
||||
delete (headerEl as any).__dayHeaderClickHandler;
|
||||
}
|
||||
}}
|
||||
viewDidMount={(arg) => {
|
||||
// Update now indicator arrow with current time and hide nearby slot labels
|
||||
const updateNowIndicator = () => {
|
||||
const nowIndicatorArrow = document.querySelector(
|
||||
".fc-timegrid-now-indicator-arrow"
|
||||
) as HTMLElement;
|
||||
if (nowIndicatorArrow) {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
nowIndicatorArrow.setAttribute("data-time", timeString);
|
||||
|
||||
// Hide slot labels that are too close to current time
|
||||
updateSlotLabelVisibility(now);
|
||||
}
|
||||
};
|
||||
|
||||
// Update immediately and then every minute
|
||||
updateNowIndicator();
|
||||
const timeInterval = setInterval(updateNowIndicator, 60000);
|
||||
|
||||
// Watch for now indicator arrow creation and slot label changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
if (
|
||||
element.classList?.contains(
|
||||
"fc-timegrid-now-indicator-arrow"
|
||||
) ||
|
||||
element.querySelector?.(
|
||||
".fc-timegrid-now-indicator-arrow"
|
||||
) ||
|
||||
element.classList?.contains("fc-timegrid-slot-label") ||
|
||||
element.querySelector?.(".fc-timegrid-slot-label")
|
||||
) {
|
||||
setTimeout(() => {
|
||||
updateNowIndicator();
|
||||
updateSlotLabelVisibility(new Date());
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
// Store interval and observer for cleanup
|
||||
(arg.el as any).__timeInterval = timeInterval;
|
||||
(arg.el as any).__timeObserver = observer;
|
||||
|
||||
// Add global hover effect for week and day views
|
||||
if (
|
||||
arg.view.type === "timeGridWeek" ||
|
||||
arg.view.type === "timeGridDay"
|
||||
) {
|
||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||
if (calendarEl) {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
// Find the timegrid container
|
||||
const timegridEl =
|
||||
calendarEl.querySelector(".fc-timegrid-body");
|
||||
if (!timegridEl) return;
|
||||
|
||||
// Check if mouse is over all-day events area (fc-scrollgrid-sync-table)
|
||||
const allDayTable = calendarEl.querySelector(
|
||||
".fc-scrollgrid-sync-table"
|
||||
);
|
||||
if (allDayTable) {
|
||||
const allDayRect = allDayTable.getBoundingClientRect();
|
||||
if (
|
||||
e.clientY >= allDayRect.top &&
|
||||
e.clientY <= allDayRect.bottom
|
||||
) {
|
||||
// Mouse is over all-day events area, don't show highlight
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if mouse is over time slot labels (left side with hours)
|
||||
const target = e.target as HTMLElement;
|
||||
if (target && target.closest(".fc-timegrid-slot-label")) {
|
||||
// Mouse is over time slot labels, don't show highlight
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all day columns
|
||||
const dayColumns =
|
||||
timegridEl.querySelectorAll(".fc-timegrid-col");
|
||||
if (dayColumns.length === 0) return;
|
||||
|
||||
// Remove previous highlights
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
|
||||
// Find which day column the mouse is over
|
||||
let targetColumn: Element | null = null;
|
||||
for (const column of dayColumns) {
|
||||
const rect = column.getBoundingClientRect();
|
||||
if (e.clientX >= rect.left && e.clientX <= rect.right) {
|
||||
targetColumn = column;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetColumn) {
|
||||
const rect = targetColumn.getBoundingClientRect();
|
||||
const relativeY = e.clientY - rect.top;
|
||||
const hourHeight = rect.height / 24; // Assuming 24 hours
|
||||
const hourIndex = Math.floor(relativeY / hourHeight);
|
||||
|
||||
// Only show highlight if mouse is actually over the timegrid area (not all-day events)
|
||||
if (relativeY >= 0 && relativeY <= rect.height) {
|
||||
// Create highlight for the specific hour in the specific day
|
||||
const highlight = document.createElement("div");
|
||||
highlight.className = "hour-highlight";
|
||||
highlight.style.top = `${hourIndex * hourHeight}px`;
|
||||
highlight.style.height = `${hourHeight}px`;
|
||||
|
||||
(targetColumn as HTMLElement).style.position = "relative";
|
||||
targetColumn.appendChild(highlight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
// Remove all hour highlights when mouse leaves calendar
|
||||
const timegridEl =
|
||||
calendarEl.querySelector(".fc-timegrid-body");
|
||||
if (timegridEl) {
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
}
|
||||
};
|
||||
|
||||
calendarEl.addEventListener("mousemove", handleMouseMove);
|
||||
calendarEl.addEventListener("mouseleave", handleMouseLeave);
|
||||
|
||||
// Store handlers for cleanup
|
||||
(calendarEl as any).__calendarMouseMoveHandler =
|
||||
handleMouseMove;
|
||||
(calendarEl as any).__calendarMouseLeaveHandler =
|
||||
handleMouseLeave;
|
||||
}
|
||||
}
|
||||
}}
|
||||
viewWillUnmount={(arg) => {
|
||||
// Clean up time interval
|
||||
if ((arg.el as any).__timeInterval) {
|
||||
clearInterval((arg.el as any).__timeInterval);
|
||||
delete (arg.el as any).__timeInterval;
|
||||
}
|
||||
|
||||
// Clean up observer
|
||||
if ((arg.el as any).__timeObserver) {
|
||||
(arg.el as any).__timeObserver.disconnect();
|
||||
delete (arg.el as any).__timeObserver;
|
||||
}
|
||||
|
||||
// Clean up event listeners to prevent memory leaks
|
||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||
if (calendarEl) {
|
||||
if ((calendarEl as any).__calendarMouseMoveHandler) {
|
||||
calendarEl.removeEventListener(
|
||||
"mousemove",
|
||||
(calendarEl as any).__calendarMouseMoveHandler
|
||||
);
|
||||
delete (calendarEl as any).__calendarMouseMoveHandler;
|
||||
}
|
||||
if ((calendarEl as any).__calendarMouseLeaveHandler) {
|
||||
calendarEl.removeEventListener(
|
||||
"mouseleave",
|
||||
(calendarEl as any).__calendarMouseLeaveHandler
|
||||
);
|
||||
delete (calendarEl as any).__calendarMouseLeaveHandler;
|
||||
}
|
||||
}
|
||||
}}
|
||||
eventClick={(info) => {
|
||||
info.jsEvent.preventDefault(); // don't let the browser navigate
|
||||
|
||||
if (info.event.url) {
|
||||
window.open(info.event.url);
|
||||
} else {
|
||||
console.log(info.event);
|
||||
setOpenEventDisplay(true);
|
||||
setAnchorPosition({
|
||||
top: info.jsEvent.clientY,
|
||||
left: info.jsEvent.clientX,
|
||||
});
|
||||
setEventDisplayedId(info.event.extendedProps.uid);
|
||||
setEventDisplayedCalId(info.event.extendedProps.calId);
|
||||
setEventDisplayedTemp(info.event._def.extendedProps.temp);
|
||||
}
|
||||
}}
|
||||
eventAllow={(dropInfo, draggedEvent) => {
|
||||
if (
|
||||
draggedEvent?.extendedProps.uid &&
|
||||
draggedEvent.extendedProps.uid.split("/")[1]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
eventDrop={(arg) => {
|
||||
if (
|
||||
!arg.event ||
|
||||
!arg.event._def ||
|
||||
!arg.event._def.extendedProps
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event =
|
||||
calendars[arg.event._def.extendedProps.calId].events[
|
||||
arg.event._def.extendedProps.uid
|
||||
];
|
||||
|
||||
const totalDeltaMs = getDeltaInMilliseconds(arg.delta);
|
||||
|
||||
const originalStart = new Date(event.start);
|
||||
const computedNewStart = new Date(
|
||||
originalStart.getTime() + totalDeltaMs
|
||||
);
|
||||
const originalEnd = new Date(event.end ?? "");
|
||||
const computedNewEnd = new Date(
|
||||
originalEnd.getTime() + totalDeltaMs
|
||||
);
|
||||
const newEvent = {
|
||||
...event,
|
||||
start: computedNewStart.toISOString(),
|
||||
end: computedNewEnd.toISOString(),
|
||||
} as CalendarEvent;
|
||||
dispatch(
|
||||
updateEventLocal({ calId: newEvent.calId, event: newEvent })
|
||||
);
|
||||
dispatch(
|
||||
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
||||
);
|
||||
}}
|
||||
eventResize={(arg) => {
|
||||
if (
|
||||
!arg.event ||
|
||||
!arg.event._def ||
|
||||
!arg.event._def.extendedProps
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event =
|
||||
calendars[arg.event._def.extendedProps.calId].events[
|
||||
arg.event._def.extendedProps.uid
|
||||
];
|
||||
if (event.uid.split("/")[1]) {
|
||||
dispatch(getEventAsync(event));
|
||||
}
|
||||
const originalStart = new Date(event.start);
|
||||
const computedNewStart = new Date(
|
||||
originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta)
|
||||
);
|
||||
const originalEnd = new Date(event.end ?? "");
|
||||
const computedNewEnd = new Date(
|
||||
originalEnd.getTime() + getDeltaInMilliseconds(arg.endDelta)
|
||||
);
|
||||
const newEvent = {
|
||||
...event,
|
||||
start: computedNewStart.toISOString(),
|
||||
end: computedNewEnd.toISOString(),
|
||||
} as CalendarEvent;
|
||||
|
||||
dispatch(
|
||||
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
||||
);
|
||||
}}
|
||||
eventContent={(arg) => {
|
||||
const event = arg.event;
|
||||
if (
|
||||
(!event._def.extendedProps.temp &&
|
||||
!calendars[arg.event._def.extendedProps.calId]) ||
|
||||
(event._def.extendedProps.temp &&
|
||||
!tempcalendars[arg.event._def.extendedProps.calId])
|
||||
)
|
||||
return;
|
||||
|
||||
const attendees = event._def.extendedProps.attendee || [];
|
||||
const isPrivate =
|
||||
event._def.extendedProps.class === "PRIVATE" ||
|
||||
event._def.extendedProps.class === "CONFIDENTIAL";
|
||||
let Icon = null;
|
||||
let titleStyle: React.CSSProperties = {};
|
||||
const ownerEmails = new Set(
|
||||
(event._def.extendedProps.temp ? tempcalendars : calendars)[
|
||||
arg.event._def.extendedProps.calId
|
||||
].ownerEmails?.map((email) => email.toLowerCase())
|
||||
);
|
||||
|
||||
const delegated = (
|
||||
event._def.extendedProps.temp ? tempcalendars : calendars
|
||||
)[arg.event._def.extendedProps.calId].delegated;
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
);
|
||||
if (!delegated && showSpecialDisplay.length === 0) return null;
|
||||
switch (showSpecialDisplay?.[0]?.partstat) {
|
||||
case "DECLINED":
|
||||
Icon = null;
|
||||
titleStyle.textDecoration = "line-through";
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
Icon = HelpOutlineIcon;
|
||||
break;
|
||||
case "NEEDS-ACTION":
|
||||
Icon = AccessTimeIcon;
|
||||
break;
|
||||
case "ACCEPTED":
|
||||
Icon = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
{isPrivate && (
|
||||
<LockIcon
|
||||
data-testid="lock-icon"
|
||||
fontSize="small"
|
||||
style={{ marginRight: "4px" }}
|
||||
/>
|
||||
)}
|
||||
{Icon && (
|
||||
<Icon fontSize="small" style={{ marginRight: "4px" }} />
|
||||
)}
|
||||
<span style={titleStyle}>{event.title}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
eventDidMount={(arg) => {
|
||||
const attendees = arg.event._def.extendedProps.attendee || [];
|
||||
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
||||
const ownerEmails = new Set(
|
||||
calendars[arg.event._def.extendedProps.calId].ownerEmails?.map(
|
||||
(email) => email.toLowerCase()
|
||||
)
|
||||
);
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
);
|
||||
|
||||
if (!showSpecialDisplay[0]) return;
|
||||
|
||||
// Clear previously applied classes
|
||||
arg.el.classList.remove(
|
||||
"declined-event",
|
||||
"tentative-event",
|
||||
"needs-action-event"
|
||||
);
|
||||
|
||||
switch (showSpecialDisplay[0].partstat) {
|
||||
case "DECLINED":
|
||||
arg.el.classList.add("declined-event");
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
arg.el.classList.add("tentative-event");
|
||||
break;
|
||||
case "NEEDS-ACTION":
|
||||
arg.el.classList.add("needs-action-event");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
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}
|
||||
/>
|
||||
<EventPopover
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClosePopover}
|
||||
onClose={eventHandlers.handleClosePopover}
|
||||
selectedRange={selectedRange}
|
||||
setSelectedRange={setSelectedRange}
|
||||
calendarRef={calendarRef}
|
||||
@@ -887,72 +438,10 @@ export default function CalendarApp({
|
||||
tempEvent={eventDisplayedTemp}
|
||||
anchorPosition={anchorPosition}
|
||||
open={openEventDisplay}
|
||||
onClose={handleCloseEventDisplay}
|
||||
onClose={eventHandlers.handleCloseEventDisplay}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function eventToFullCalendarFormat(
|
||||
filteredEvents: CalendarEvent[],
|
||||
filteredTempEvents: CalendarEvent[],
|
||||
userId: string | undefined
|
||||
) {
|
||||
return filteredEvents
|
||||
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
|
||||
.map((e) => {
|
||||
if (e.calId.split("/")[0] === userId) {
|
||||
return { ...e, editable: true };
|
||||
}
|
||||
return { ...e, editable: false };
|
||||
});
|
||||
}
|
||||
|
||||
function extractEvents(
|
||||
selectedCalendars: string[],
|
||||
calendars: Record<string, Calendars>
|
||||
) {
|
||||
let filteredEvents: CalendarEvent[] = [];
|
||||
selectedCalendars.forEach((id) => {
|
||||
if (calendars[id].events) {
|
||||
filteredEvents = filteredEvents
|
||||
.concat(
|
||||
Object.keys(calendars[id].events).map(
|
||||
(eventid) => calendars[id].events[eventid]
|
||||
)
|
||||
)
|
||||
.filter((event) => !(event.status === "CANCELLED"));
|
||||
}
|
||||
});
|
||||
return filteredEvents;
|
||||
}
|
||||
|
||||
function updateCalsDetails(
|
||||
selectedCalendars: string[],
|
||||
pending: boolean,
|
||||
calendars: Record<string, Calendars>,
|
||||
rangeKey: string,
|
||||
dispatch: Function,
|
||||
calendarRange: { start: Date; end: Date },
|
||||
calType?: "temp"
|
||||
) {
|
||||
selectedCalendars.forEach((id) => {
|
||||
if (Object.keys(calendars[id].events).length > 0) {
|
||||
return;
|
||||
}
|
||||
if (!pending && rangeKey) {
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId: id,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
},
|
||||
calType,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { DateSelectArg } from "@fullcalendar/core";
|
||||
import { CalendarApi } from "@fullcalendar/core";
|
||||
import { CalendarEvent } from "../../../features/Events/EventsTypes";
|
||||
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||
import { getDeltaInMilliseconds } from "../../../utils/dateUtils";
|
||||
import {
|
||||
getCalendarDetailAsync,
|
||||
getEventAsync,
|
||||
putEventAsync,
|
||||
updateEventLocal,
|
||||
} from "../../../features/Calendars/CalendarSlice";
|
||||
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
|
||||
|
||||
export interface EventHandlersProps {
|
||||
setSelectedRange: (range: DateSelectArg | null) => void;
|
||||
setAnchorEl: (el: HTMLElement | null) => void;
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
selectedCalendars: string[];
|
||||
tempcalendars: Record<string, Calendars>;
|
||||
calendarRange: { start: Date; end: Date };
|
||||
dispatch: any;
|
||||
setOpenEventDisplay: (open: boolean) => void;
|
||||
setAnchorPosition: (position: { top: number; left: number } | null) => void;
|
||||
setEventDisplayedId: (id: string) => void;
|
||||
setEventDisplayedCalId: (id: string) => void;
|
||||
setEventDisplayedTemp: (temp: boolean) => void;
|
||||
calendars: Record<string, Calendars>;
|
||||
}
|
||||
|
||||
export const createEventHandlers = (props: EventHandlersProps) => {
|
||||
const {
|
||||
setSelectedRange,
|
||||
setAnchorEl,
|
||||
calendarRef,
|
||||
selectedCalendars,
|
||||
tempcalendars,
|
||||
calendarRange,
|
||||
dispatch,
|
||||
setOpenEventDisplay,
|
||||
setAnchorPosition,
|
||||
setEventDisplayedId,
|
||||
setEventDisplayedCalId,
|
||||
setEventDisplayedTemp,
|
||||
calendars,
|
||||
} = props;
|
||||
|
||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
||||
setSelectedRange(selectInfo);
|
||||
setAnchorEl(document.body);
|
||||
};
|
||||
|
||||
const handleClosePopover = () => {
|
||||
calendarRef.current?.unselect();
|
||||
setAnchorEl(null);
|
||||
setSelectedRange(null);
|
||||
selectedCalendars.forEach((calId) =>
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
Object.keys(tempcalendars).forEach((calId) =>
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
},
|
||||
calType: "temp",
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloseEventDisplay = () => {
|
||||
setAnchorPosition(null);
|
||||
setOpenEventDisplay(false);
|
||||
};
|
||||
|
||||
const handleMonthUp = (
|
||||
selectedMiniDate: Date,
|
||||
setSelectedMiniDate: (date: Date) => void
|
||||
) => {
|
||||
setSelectedMiniDate(
|
||||
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() - 1)
|
||||
);
|
||||
};
|
||||
|
||||
const handleMonthDown = (
|
||||
selectedMiniDate: Date,
|
||||
setSelectedMiniDate: (date: Date) => void
|
||||
) => {
|
||||
setSelectedMiniDate(
|
||||
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1)
|
||||
);
|
||||
};
|
||||
|
||||
const handleEventClick = (info: any) => {
|
||||
info.jsEvent.preventDefault();
|
||||
|
||||
if (info.event.url) {
|
||||
window.open(info.event.url);
|
||||
} else {
|
||||
console.log(info.event);
|
||||
setOpenEventDisplay(true);
|
||||
setAnchorPosition({
|
||||
top: info.jsEvent.clientY,
|
||||
left: info.jsEvent.clientX,
|
||||
});
|
||||
setEventDisplayedId(info.event.extendedProps.uid);
|
||||
setEventDisplayedCalId(info.event.extendedProps.calId);
|
||||
setEventDisplayedTemp(info.event._def.extendedProps.temp);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEventAllow = (dropInfo: any, draggedEvent: any) => {
|
||||
if (
|
||||
draggedEvent?.extendedProps.uid &&
|
||||
draggedEvent.extendedProps.uid.split("/")[1]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleEventDrop = (arg: any) => {
|
||||
if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event =
|
||||
calendars[arg.event._def.extendedProps.calId].events[
|
||||
arg.event._def.extendedProps.uid
|
||||
];
|
||||
|
||||
const totalDeltaMs = getDeltaInMilliseconds(arg.delta);
|
||||
|
||||
const originalStart = new Date(event.start);
|
||||
const computedNewStart = new Date(originalStart.getTime() + totalDeltaMs);
|
||||
const originalEnd = new Date(event.end ?? "");
|
||||
const computedNewEnd = new Date(originalEnd.getTime() + totalDeltaMs);
|
||||
const newEvent = {
|
||||
...event,
|
||||
start: computedNewStart.toISOString(),
|
||||
end: computedNewEnd.toISOString(),
|
||||
} as CalendarEvent;
|
||||
dispatch(updateEventLocal({ calId: newEvent.calId, event: newEvent }));
|
||||
dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
|
||||
};
|
||||
|
||||
const handleEventResize = (arg: any) => {
|
||||
if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event =
|
||||
calendars[arg.event._def.extendedProps.calId].events[
|
||||
arg.event._def.extendedProps.uid
|
||||
];
|
||||
if (event.uid.split("/")[1]) {
|
||||
dispatch(getEventAsync(event));
|
||||
}
|
||||
const originalStart = new Date(event.start);
|
||||
const computedNewStart = new Date(
|
||||
originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta)
|
||||
);
|
||||
const originalEnd = new Date(event.end ?? "");
|
||||
const computedNewEnd = new Date(
|
||||
originalEnd.getTime() + getDeltaInMilliseconds(arg.endDelta)
|
||||
);
|
||||
const newEvent = {
|
||||
...event,
|
||||
start: computedNewStart.toISOString(),
|
||||
end: computedNewEnd.toISOString(),
|
||||
} as CalendarEvent;
|
||||
|
||||
dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
|
||||
};
|
||||
|
||||
return {
|
||||
handleDateSelect,
|
||||
handleClosePopover,
|
||||
handleCloseEventDisplay,
|
||||
handleMonthUp,
|
||||
handleMonthDown,
|
||||
handleEventClick,
|
||||
handleEventAllow,
|
||||
handleEventDrop,
|
||||
handleEventResize,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
export interface MouseHandlersProps {
|
||||
calendarEl: HTMLElement;
|
||||
}
|
||||
|
||||
export const createMouseHandlers = (props: MouseHandlersProps) => {
|
||||
const { calendarEl } = props;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const timegridEl = calendarEl.querySelector(".fc-timegrid-body");
|
||||
if (!timegridEl) return;
|
||||
|
||||
const allDayTable = calendarEl.querySelector(".fc-scrollgrid-sync-table");
|
||||
if (allDayTable) {
|
||||
const allDayRect = allDayTable.getBoundingClientRect();
|
||||
if (e.clientY >= allDayRect.top && e.clientY <= allDayRect.bottom) {
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
if (target && target.closest(".fc-timegrid-slot-label")) {
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
return;
|
||||
}
|
||||
|
||||
const dayColumns = timegridEl.querySelectorAll(".fc-timegrid-col");
|
||||
if (dayColumns.length === 0) return;
|
||||
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
|
||||
let targetColumn: Element | null = null;
|
||||
for (const column of dayColumns) {
|
||||
const rect = column.getBoundingClientRect();
|
||||
if (e.clientX >= rect.left && e.clientX <= rect.right) {
|
||||
targetColumn = column;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetColumn) {
|
||||
const rect = targetColumn.getBoundingClientRect();
|
||||
const relativeY = e.clientY - rect.top;
|
||||
const hourHeight = rect.height / 24;
|
||||
const hourIndex = Math.floor(relativeY / hourHeight);
|
||||
|
||||
if (relativeY >= 0 && relativeY <= rect.height) {
|
||||
const highlight = document.createElement("div");
|
||||
highlight.className = "hour-highlight";
|
||||
highlight.style.top = `${hourIndex * hourHeight}px`;
|
||||
highlight.style.height = `${hourHeight}px`;
|
||||
|
||||
(targetColumn as HTMLElement).style.position = "relative";
|
||||
targetColumn.appendChild(highlight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
const timegridEl = calendarEl.querySelector(".fc-timegrid-body");
|
||||
if (timegridEl) {
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
}
|
||||
};
|
||||
|
||||
const addMouseEventListeners = () => {
|
||||
calendarEl.addEventListener("mousemove", handleMouseMove);
|
||||
calendarEl.addEventListener("mouseleave", handleMouseLeave);
|
||||
|
||||
(calendarEl as any).__calendarMouseMoveHandler = handleMouseMove;
|
||||
(calendarEl as any).__calendarMouseLeaveHandler = handleMouseLeave;
|
||||
};
|
||||
|
||||
const removeMouseEventListeners = () => {
|
||||
if ((calendarEl as any).__calendarMouseMoveHandler) {
|
||||
calendarEl.removeEventListener(
|
||||
"mousemove",
|
||||
(calendarEl as any).__calendarMouseMoveHandler
|
||||
);
|
||||
delete (calendarEl as any).__calendarMouseMoveHandler;
|
||||
}
|
||||
if ((calendarEl as any).__calendarMouseLeaveHandler) {
|
||||
calendarEl.removeEventListener(
|
||||
"mouseleave",
|
||||
(calendarEl as any).__calendarMouseLeaveHandler
|
||||
);
|
||||
delete (calendarEl as any).__calendarMouseLeaveHandler;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleMouseMove,
|
||||
handleMouseLeave,
|
||||
addMouseEventListeners,
|
||||
removeMouseEventListeners,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,245 @@
|
||||
import React from "react";
|
||||
import { CalendarApi } from "@fullcalendar/core";
|
||||
import { updateSlotLabelVisibility } from "../utils/calendarUtils";
|
||||
import { createMouseHandlers } from "./mouseHandlers";
|
||||
import { userAttendee } from "../../../features/User/userDataTypes";
|
||||
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||||
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
|
||||
export interface ViewHandlersProps {
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
setSelectedDate: (date: Date) => void;
|
||||
setSelectedMiniDate: (date: Date) => void;
|
||||
onViewChange?: (view: string) => void;
|
||||
calendars: Record<string, Calendars>;
|
||||
tempcalendars: Record<string, Calendars>;
|
||||
}
|
||||
|
||||
export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
const {
|
||||
calendarRef,
|
||||
setSelectedDate,
|
||||
setSelectedMiniDate,
|
||||
onViewChange,
|
||||
calendars,
|
||||
tempcalendars,
|
||||
} = props;
|
||||
|
||||
const handleDayHeaderDidMount = (arg: any) => {
|
||||
if (arg.view.type === "timeGridWeek") {
|
||||
const headerEl = arg.el;
|
||||
|
||||
const handleDayHeaderClick = () => {
|
||||
calendarRef.current?.changeView("timeGridDay", arg.date);
|
||||
setSelectedDate(new Date(arg.date));
|
||||
setSelectedMiniDate(new Date(arg.date));
|
||||
|
||||
if (onViewChange) {
|
||||
onViewChange("timeGridDay");
|
||||
}
|
||||
};
|
||||
|
||||
headerEl.addEventListener("click", handleDayHeaderClick);
|
||||
(headerEl as any).__dayHeaderClickHandler = handleDayHeaderClick;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDayHeaderWillUnmount = (arg: any) => {
|
||||
const headerEl = arg.el;
|
||||
if ((headerEl as any).__dayHeaderClickHandler) {
|
||||
headerEl.removeEventListener(
|
||||
"click",
|
||||
(headerEl as any).__dayHeaderClickHandler
|
||||
);
|
||||
delete (headerEl as any).__dayHeaderClickHandler;
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDidMount = (arg: any) => {
|
||||
const updateNowIndicator = () => {
|
||||
const nowIndicatorArrow = document.querySelector(
|
||||
".fc-timegrid-now-indicator-arrow"
|
||||
) as HTMLElement;
|
||||
if (nowIndicatorArrow) {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
nowIndicatorArrow.setAttribute("data-time", timeString);
|
||||
updateSlotLabelVisibility(now);
|
||||
}
|
||||
};
|
||||
|
||||
updateNowIndicator();
|
||||
const timeInterval = setInterval(updateNowIndicator, 60000);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
if (
|
||||
element.classList?.contains("fc-timegrid-now-indicator-arrow") ||
|
||||
element.querySelector?.(".fc-timegrid-now-indicator-arrow") ||
|
||||
element.classList?.contains("fc-timegrid-slot-label") ||
|
||||
element.querySelector?.(".fc-timegrid-slot-label")
|
||||
) {
|
||||
setTimeout(() => {
|
||||
updateNowIndicator();
|
||||
updateSlotLabelVisibility(new Date());
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
(arg.el as any).__timeInterval = timeInterval;
|
||||
(arg.el as any).__timeObserver = observer;
|
||||
|
||||
if (arg.view.type === "timeGridWeek" || arg.view.type === "timeGridDay") {
|
||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||
if (calendarEl) {
|
||||
const mouseHandlers = createMouseHandlers({ calendarEl });
|
||||
mouseHandlers.addMouseEventListeners();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewWillUnmount = (arg: any) => {
|
||||
if ((arg.el as any).__timeInterval) {
|
||||
clearInterval((arg.el as any).__timeInterval);
|
||||
delete (arg.el as any).__timeInterval;
|
||||
}
|
||||
|
||||
if ((arg.el as any).__timeObserver) {
|
||||
(arg.el as any).__timeObserver.disconnect();
|
||||
delete (arg.el as any).__timeObserver;
|
||||
}
|
||||
|
||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||
if (calendarEl) {
|
||||
const mouseHandlers = createMouseHandlers({ calendarEl });
|
||||
mouseHandlers.removeMouseEventListeners();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEventContent = (arg: any) => {
|
||||
const event = arg.event;
|
||||
if (
|
||||
(!event._def.extendedProps.temp &&
|
||||
!calendars[arg.event._def.extendedProps.calId]) ||
|
||||
(event._def.extendedProps.temp &&
|
||||
!tempcalendars[arg.event._def.extendedProps.calId])
|
||||
)
|
||||
return;
|
||||
|
||||
const attendees = event._def.extendedProps.attendee || [];
|
||||
const isPrivate =
|
||||
event._def.extendedProps.class === "PRIVATE" ||
|
||||
event._def.extendedProps.class === "CONFIDENTIAL";
|
||||
let Icon = null;
|
||||
let titleStyle: React.CSSProperties = {};
|
||||
const ownerEmails = new Set(
|
||||
(event._def.extendedProps.temp ? tempcalendars : calendars)[
|
||||
arg.event._def.extendedProps.calId
|
||||
].ownerEmails?.map((email) => email.toLowerCase())
|
||||
);
|
||||
|
||||
const delegated = (
|
||||
event._def.extendedProps.temp ? tempcalendars : calendars
|
||||
)[arg.event._def.extendedProps.calId].delegated;
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
);
|
||||
if (!delegated && showSpecialDisplay.length === 0) return null;
|
||||
|
||||
switch (showSpecialDisplay?.[0]?.partstat) {
|
||||
case "DECLINED":
|
||||
Icon = null;
|
||||
titleStyle.textDecoration = "line-through";
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
Icon = HelpOutlineIcon;
|
||||
break;
|
||||
case "NEEDS-ACTION":
|
||||
Icon = AccessTimeIcon;
|
||||
break;
|
||||
case "ACCEPTED":
|
||||
Icon = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ style: { display: "flex", alignItems: "center" } },
|
||||
isPrivate &&
|
||||
React.createElement(LockIcon, {
|
||||
"data-testid": "lock-icon",
|
||||
fontSize: "small",
|
||||
style: { marginRight: "4px" },
|
||||
}),
|
||||
Icon &&
|
||||
React.createElement(Icon, {
|
||||
fontSize: "small",
|
||||
style: { marginRight: "4px" },
|
||||
}),
|
||||
React.createElement("span", { style: titleStyle }, event.title)
|
||||
);
|
||||
};
|
||||
|
||||
const handleEventDidMount = (arg: any) => {
|
||||
const attendees = arg.event._def.extendedProps.attendee || [];
|
||||
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
||||
const ownerEmails = new Set(
|
||||
calendars[arg.event._def.extendedProps.calId].ownerEmails?.map((email) =>
|
||||
email.toLowerCase()
|
||||
)
|
||||
);
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
);
|
||||
|
||||
if (!showSpecialDisplay[0]) return;
|
||||
|
||||
arg.el.classList.remove(
|
||||
"declined-event",
|
||||
"tentative-event",
|
||||
"needs-action-event"
|
||||
);
|
||||
|
||||
switch (showSpecialDisplay[0].partstat) {
|
||||
case "DECLINED":
|
||||
arg.el.classList.add("declined-event");
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
arg.el.classList.add("tentative-event");
|
||||
break;
|
||||
case "NEEDS-ACTION":
|
||||
arg.el.classList.add("needs-action-event");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleDayHeaderDidMount,
|
||||
handleDayHeaderWillUnmount,
|
||||
handleViewDidMount,
|
||||
handleViewWillUnmount,
|
||||
handleEventContent,
|
||||
handleEventDidMount,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { CalendarEvent } from "../../../features/Events/EventsTypes";
|
||||
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
|
||||
import { getCalendarDetailAsync } from "../../../features/Calendars/CalendarSlice";
|
||||
|
||||
export const updateSlotLabelVisibility = (currentTime: Date) => {
|
||||
const slotLabels = document.querySelectorAll(".fc-timegrid-slot-label");
|
||||
const currentMinutes = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
|
||||
slotLabels.forEach((label) => {
|
||||
const labelElement = label as HTMLElement;
|
||||
const timeText = labelElement.textContent?.trim();
|
||||
|
||||
if (timeText && timeText.match(/^\d{1,2}:\d{2}$/)) {
|
||||
const [hours, minutes] = timeText.split(":").map(Number);
|
||||
const labelMinutes = hours * 60 + minutes;
|
||||
|
||||
let timeDiff = Math.abs(currentMinutes - labelMinutes);
|
||||
|
||||
if (timeDiff > 12 * 60) {
|
||||
timeDiff = 24 * 60 - timeDiff;
|
||||
}
|
||||
|
||||
if (timeDiff <= 15) {
|
||||
labelElement.style.opacity = "0.2";
|
||||
} else {
|
||||
labelElement.style.opacity = "1";
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const eventToFullCalendarFormat = (
|
||||
filteredEvents: CalendarEvent[],
|
||||
filteredTempEvents: CalendarEvent[],
|
||||
userId: string | undefined
|
||||
) => {
|
||||
return filteredEvents
|
||||
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
|
||||
.map((e) => {
|
||||
if (e.calId.split("/")[0] === userId) {
|
||||
return { ...e, editable: true };
|
||||
}
|
||||
return { ...e, editable: false };
|
||||
});
|
||||
};
|
||||
|
||||
export const extractEvents = (
|
||||
selectedCalendars: string[],
|
||||
calendars: Record<string, Calendars>
|
||||
) => {
|
||||
let filteredEvents: CalendarEvent[] = [];
|
||||
selectedCalendars.forEach((id) => {
|
||||
if (calendars[id].events) {
|
||||
filteredEvents = filteredEvents
|
||||
.concat(
|
||||
Object.keys(calendars[id].events).map(
|
||||
(eventid) => calendars[id].events[eventid]
|
||||
)
|
||||
)
|
||||
.filter((event) => !(event.status === "CANCELLED"));
|
||||
}
|
||||
});
|
||||
return filteredEvents;
|
||||
};
|
||||
|
||||
export const updateCalsDetails = (
|
||||
selectedCalendars: string[],
|
||||
pending: boolean,
|
||||
calendars: Record<string, Calendars>,
|
||||
rangeKey: string,
|
||||
dispatch: Function,
|
||||
calendarRange: { start: Date; end: Date },
|
||||
calType?: "temp"
|
||||
) => {
|
||||
selectedCalendars.forEach((id) => {
|
||||
if (Object.keys(calendars[id].events).length > 0) {
|
||||
return;
|
||||
}
|
||||
if (!pending && rangeKey) {
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId: id,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
},
|
||||
calType,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user