import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import {
Checkbox,
FormControl,
FormControlLabel,
IconButton,
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
TextField,
Typography,
ToggleButtonGroup,
ToggleButton,
} from "@mui/material";
import {
Description as DescriptionIcon,
Public as PublicIcon,
Lock as LockIcon,
CameraAlt as VideocamIcon,
ContentCopy as CopyIcon,
Close as DeleteIcon,
} from "@mui/icons-material";
import { Box, Button } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import React, {
useEffect,
useState,
useMemo,
useCallback,
useRef,
} from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
import { ResponsiveDialog } from "../../components/Dialog";
import { putEventAsync } from "../Calendars/CalendarSlice";
import { Calendars } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { createSelector } from "@reduxjs/toolkit";
import RepeatEvent from "../../components/Event/EventRepeat";
import { TIMEZONES } from "../../utils/timezone-data";
import {
generateMeetingLink,
addVideoConferenceToDescription,
} from "../../utils/videoConferenceUtils";
import {
getTimezoneOffset,
resolveTimezone,
} from "../../components/Calendar/TimezoneSelector";
import { getCalendarRange } from "../../utils/dateUtils";
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
import { TimezoneAutocomplete } from "../../components/Timezone/TimezoneAutocomplete";
// Helper component for field with label
const FieldWithLabel = React.memo(
({
label,
isExpanded,
children,
}: {
label: string;
isExpanded: boolean;
children: React.ReactNode;
}) => {
if (!isExpanded) {
// Normal mode: label on top
return (
{label}
{children}
);
}
// Extended mode: label on left
return (
{label}
{children}
);
}
);
FieldWithLabel.displayName = "FieldWithLabel";
function EventPopover({
anchorEl,
open,
onClose,
selectedRange,
setSelectedRange,
calendarRef,
event,
}: {
anchorEl: HTMLElement | null;
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
selectedRange: DateSelectArg | null;
setSelectedRange: Function;
calendarRef: React.RefObject;
event?: CalendarEvent;
}) {
const dispatch = useAppDispatch();
const organizer = useAppSelector((state) => state.user.organiserData);
const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const tempList = useAppSelector((state) => state.calendars.templist);
const selectPersonnalCalendars = createSelector(
(state) => state.calendars,
(calendars) =>
Object.keys(calendars.list)
.map((id) => {
if (id.split("/")[0] === userId) {
return calendars.list[id];
}
return {} as Calendars;
})
.filter((calendar) => calendar.id)
);
const userPersonnalCalendars: Calendars[] = useAppSelector(
selectPersonnalCalendars
);
const timezoneList = useMemo(() => {
const zones = Object.keys(TIMEZONES.zones).sort();
const browserTz = resolveTimezone(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
return { zones, browserTz };
}, []);
const calendarTimezone = useAppSelector((state) => state.calendars.timeZone);
const [showMore, setShowMore] = useState(false);
const [showDescription, setShowDescription] = useState(
event?.description ? true : false
);
const [showRepeat, setShowRepeat] = useState(
event?.repetition?.freq ? true : false
);
const [title, setTitle] = useState(event?.title ?? "");
const [description, setDescription] = useState(event?.description ?? "");
const [location, setLocation] = useState(event?.location ?? "");
const [start, setStart] = useState(event?.start ? event.start : "");
const [end, setEnd] = useState(event?.end ? event.end : "");
const [calendarid, setCalendarid] = useState(
event?.calId
? userPersonnalCalendars.findIndex((e) => e.id === event?.calId)
: 0
);
const [allday, setAllDay] = useState(event?.allday ?? false);
const [repetition, setRepetition] = useState(
event?.repetition ?? ({} as RepetitionObject)
);
const [attendees, setAttendees] = useState(
event?.attendee
? event.attendee.filter((a) => a.cal_address !== organizer.cal_address)
: []
);
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
const [timezone, setTimezone] = useState(
event?.timezone
? resolveTimezone(event.timezone)
: resolveTimezone(calendarTimezone)
);
const [hasVideoConference, setHasVideoConference] = useState(
event?.x_openpass_videoconference ? true : false
);
const [meetingLink, setMeetingLink] = useState(
event?.x_openpass_videoconference || null
);
// Use ref to track if we've already initialized to avoid infinite loop
const isInitializedRef = useRef(false);
const userPersonnalCalendarsRef = useRef(userPersonnalCalendars);
// Update ref when userPersonnalCalendars changes
useEffect(() => {
userPersonnalCalendarsRef.current = userPersonnalCalendars;
}, [userPersonnalCalendars]);
// Sync timezone with Redux when modal opens or timezone changes
useEffect(() => {
if (open && !event?.timezone && calendarTimezone) {
setTimezone(resolveTimezone(calendarTimezone));
}
}, [open, calendarTimezone, event?.timezone]);
const resetAllStateToDefault = useCallback(() => {
setShowMore(false);
setShowDescription(false);
setShowRepeat(false);
setTitle("");
setDescription("");
setAttendees([]);
setLocation("");
setStart("");
setEnd("");
setCalendarid(0);
setAllDay(false);
setRepetition({} as RepetitionObject);
setAlarm("");
setEventClass("PUBLIC");
setBusy("OPAQUE");
setTimezone(resolveTimezone(calendarTimezone));
setHasVideoConference(false);
setMeetingLink(null);
}, [calendarTimezone]);
useEffect(() => {
if (selectedRange) {
setStart(
selectedRange ? formatLocalDateTime(selectedRange.start, timezone) : ""
);
setEnd(
selectedRange ? formatLocalDateTime(selectedRange.end, timezone) : ""
);
}
}, [selectedRange]);
// Initialize state when event prop changes
useEffect(() => {
if (event) {
// Editing existing event - populate fields with event data
setTitle(event.title ?? "");
setDescription(event.description ?? "");
setLocation(event.location ?? "");
setStart(event.start ? event.start : "");
setEnd(event.end ? event.end : "");
setCalendarid(
event.calId
? userPersonnalCalendarsRef.current.findIndex(
(e) => e.id === event.calId
)
: 0
);
setAllDay(event.allday ?? false);
setRepetition(event.repetition ?? ({} as RepetitionObject));
setShowRepeat(event.repetition?.freq ? true : false);
setAttendees(
event.attendee
? event.attendee.filter(
(a) => a.cal_address !== organizer?.cal_address
)
: []
);
setAlarm(event.alarm?.trigger ?? "");
setEventClass(event.class ?? "PUBLIC");
setBusy(event.transp ?? "OPAQUE");
setTimezone(
event.timezone ? resolveTimezone(event.timezone) : calendarTimezone
);
setHasVideoConference(event.x_openpass_videoconference ? true : false);
setMeetingLink(event.x_openpass_videoconference || null);
// Update description to include video conference footer if exists
if (event.x_openpass_videoconference && event.description) {
const hasVideoFooter = event.description.includes("Visio:");
if (!hasVideoFooter) {
setDescription(
addVideoConferenceToDescription(
event.description,
event.x_openpass_videoconference
)
);
} else {
setDescription(event.description);
}
}
}
}, [event, organizer?.cal_address, calendarTimezone]);
// Reset state when creating new event (event is undefined)
useEffect(() => {
if (!event && isInitializedRef.current) {
// Creating new event - reset all fields to default
setShowMore(false);
setShowDescription(false);
setShowRepeat(false);
setTitle("");
setDescription("");
setAttendees([]);
setLocation("");
setStart("");
setEnd("");
setCalendarid(0);
setAllDay(false);
setRepetition({} as RepetitionObject);
setAlarm("");
setEventClass("PUBLIC");
setBusy("OPAQUE");
setTimezone(timezoneList.browserTz);
setHasVideoConference(false);
setMeetingLink(null);
}
isInitializedRef.current = true;
}, [event, calendarTimezone]);
const handleAddVideoConference = () => {
const newMeetingLink = generateMeetingLink();
const updatedDescription = addVideoConferenceToDescription(
description,
newMeetingLink
);
setDescription(updatedDescription);
setHasVideoConference(true);
setMeetingLink(newMeetingLink);
};
const handleCopyMeetingLink = async () => {
if (meetingLink) {
try {
await navigator.clipboard.writeText(meetingLink);
// You could add a toast notification here
console.log("Meeting link copied to clipboard");
} catch (err) {
console.error("Failed to copy link:", err);
}
}
};
const handleDeleteVideoConference = () => {
// Remove video conference footer from description
const updatedDescription = description.replace(
/\nVisio: https?:\/\/[^\s]+/,
""
);
setDescription(updatedDescription);
setHasVideoConference(false);
setMeetingLink(null);
};
const handleClose = () => {
onClose({}, "backdropClick");
resetAllStateToDefault();
};
const handleSave = async () => {
const newEventUID = crypto.randomUUID();
const newEvent: CalendarEvent = {
calId: userPersonnalCalendars[calendarid].id,
title,
URL: `/calendars/${userPersonnalCalendars[calendarid].id}/${newEventUID}.ics`,
start: new Date(start).toISOString(),
allday,
uid: newEventUID,
description,
location,
class: eventClass,
repetition,
organizer,
timezone,
attendee: [
{
cn: organizer.cn,
cal_address: organizer.cal_address,
partstat: "ACCEPTED",
rsvp: "FALSE",
role: "CHAIR",
cutype: "INDIVIDUAL",
},
],
transp: busy,
color: userPersonnalCalendars[calendarid]?.color,
alarm: { trigger: alarm, action: "EMAIL" },
x_openpass_videoconference: meetingLink || undefined,
};
if (end) {
newEvent.end = new Date(end).toISOString();
}
if (attendees.length > 0) {
newEvent.attendee = newEvent.attendee.concat(attendees);
}
// Close popup immediately
onClose({}, "backdropClick");
// Reset all state to default values
resetAllStateToDefault();
// Save to API in background
await dispatch(
putEventAsync({
cal: userPersonnalCalendars[calendarid],
newEvent,
})
);
if (tempList) {
const calendarRange = getCalendarRange(new Date(start));
await updateTempCalendar(tempList, newEvent, dispatch, calendarRange);
}
};
const dialogActions = (
{!showMore && (
} onClick={() => setShowMore(!showMore)}>
More options
)}
{showMore && (
)}
);
return (
setShowMore(!showMore)}
actions={dialogActions}
>
setTitle(e.target.value)}
size="small"
margin="dense"
/>
{!showDescription && (
}
onClick={() => setShowDescription(true)}
size="small"
sx={{
textTransform: "none",
color: "text.secondary",
}}
>
Add description
)}
{showDescription && (
setDescription(e.target.value)}
size="small"
margin="dense"
multiline
minRows={2}
maxRows={10}
sx={{
"& .MuiInputBase-root": {
maxHeight: "33%",
overflowY: "auto",
},
"& textarea": {
resize: "vertical",
},
}}
/>
)}
{showMore && (
Start
)}
{
const newStart = e.target.value;
setStart(newStart);
// Update selectedRange for visual feedback
const startISO = formatLocalDateTime(
new Date(newStart),
timezone
);
const newRange = {
...selectedRange,
start: new Date(startISO),
startStr: startISO,
allDay: allday,
};
setSelectedRange(newRange);
calendarRef.current?.select(newRange);
}}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
{showMore && (
End
)}
{
const newEnd = e.target.value;
setEnd(newEnd);
// Update selectedRange for visual feedback
const endISO = formatLocalDateTime(new Date(newEnd), timezone);
const newRange = {
...selectedRange,
end: new Date(endISO),
endStr: endISO,
allDay: allday,
};
setSelectedRange(newRange);
calendarRef.current?.select(newRange);
}}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
{
const endDate = new Date(end);
const startDate = new Date(start);
setAllDay(!allday);
if (endDate.getDate() === startDate.getDate()) {
endDate.setDate(startDate.getDate() + 1);
setEnd(formatLocalDateTime(endDate, timezone));
}
const newRange = {
...selectedRange,
startStr: allday ? start.split("T")[0] : start,
endStr: allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString(),
start: new Date(allday ? start.split("T")[0] : start),
end: new Date(
allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString()
),
allDay: allday,
};
setSelectedRange(newRange);
}}
/>
}
label="All day"
/>
{
const newShowRepeat = !showRepeat;
setShowRepeat(newShowRepeat);
if (newShowRepeat) {
setRepetition({
freq: "daily",
interval: 1,
occurrences: 0,
endDate: "",
selectedDays: [],
} as RepetitionObject);
} else {
setRepetition({
freq: "",
interval: 1,
occurrences: 0,
endDate: "",
selectedDays: [],
} as RepetitionObject);
}
}}
/>
}
label="Repeat"
/>
{showRepeat && (
)}
}
onClick={handleAddVideoConference}
size="medium"
sx={{
textTransform: "none",
color: "text.secondary",
display: hasVideoConference ? "none" : "flex",
}}
>
Add Visio conference
{hasVideoConference && meetingLink && (
<>
}
onClick={() => window.open(meetingLink, "_blank")}
size="medium"
variant="contained"
sx={{
textTransform: "none",
mr: 1,
}}
>
Join Visio conference
>
)}
setLocation(e.target.value)}
size="small"
margin="dense"
/>
{!showMore && (
Calendar
)}
{/* Extended options */}
{showMore && (
<>
{
if (newValue !== null) {
setEventClass(newValue);
}
}}
size="small"
>
All
Participants
>
)}
);
}
export default EventPopover;
export function formatLocalDateTime(date: Date, timeZone?: string) {
const formatter = new Intl.DateTimeFormat("en-CA", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone,
});
const formatted = formatter.format(date);
return formatted.replace(", ", "T");
}