import { useEffect, useState } from "react"; import { deleteEventAsync, getEventAsync, moveEventAsync, putEventAsync, removeEvent, } from "../Calendars/CalendarSlice"; import { useAppDispatch, useAppSelector } from "../../app/hooks"; import AttendeeSelector from "../../components/Attendees/AttendeeSearch"; import { Popover, Button, Box, Typography, ButtonGroup, Card, CardContent, Divider, IconButton, Avatar, Badge, Modal, TextField, CardHeader, FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, Checkbox, FormControlLabel, CardActions, Link, } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import CloseIcon from "@mui/icons-material/Close"; import VideocamIcon from "@mui/icons-material/Videocam"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import CircleIcon from "@mui/icons-material/Circle"; import CancelIcon from "@mui/icons-material/Cancel"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import { userAttendee } from "../User/userDataTypes"; import { TIMEZONES } from "../../utils/timezone-data"; import { Calendars } from "../Calendars/CalendarTypes"; import { CalendarEvent, RepetitionObject } from "./EventsTypes"; import { isValidUrl } from "../../utils/apiUtils"; import { formatLocalDateTime } from "./EventModal"; import RepeatEvent from "../../components/Event/EventRepeat"; export default function EventDisplayModal({ eventId, calId, open, onClose, }: { eventId: string; calId: string; open: boolean; onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void; }) { const dispatch = useAppDispatch(); const calendar = useAppSelector((state) => state.calendars.list[calId]); const event = useAppSelector( (state) => state.calendars.list[calId]?.events[eventId] ); const user = useAppSelector((state) => state.user); const [showAllAttendees, setShowAllAttendees] = useState(false); const [showMore, setShowMore] = useState(false); const calendars = Object.values( useAppSelector((state) => state.calendars.list) ); const userPersonnalCalendars: Calendars[] = calendars.filter( (c) => c.id?.split("/")[0] === user.userData?.openpaasId ); // Form state const [title, setTitle] = useState(event?.title ?? ""); const [description, setDescription] = useState(event?.description ?? ""); const [location, setLocation] = useState(event?.location ?? ""); const [start, setStart] = useState( formatLocalDateTime(new Date(event?.start ?? Date.now())) ); const [end, setEnd] = useState( formatLocalDateTime(new Date(event?.end ?? Date.now())) ); const [allday, setAllDay] = useState(event?.allday); const [repetition, setRepetition] = useState( event?.repetition ?? ({} as RepetitionObject) ); const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? ""); const [busy, setBusy] = useState(event?.transp ?? "OPAQUE"); const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC"); const [timezone, setTimezone] = useState(event?.timezone ?? "UTC"); const [newCalId, setNewCalId] = useState(event?.calId); const [calendarid, setCalendarid] = useState( calId.split("/")[0] === user.userData?.openpaasId ? userPersonnalCalendars.findIndex((cal) => cal.id === calId) : calendars.findIndex((cal) => cal.id === calId) ); const [attendees, setAttendees] = useState( (event?.attendee || []).filter( (a) => a.cal_address !== event?.organizer?.cal_address ) ); const currentUserAttendee = event?.attendee?.find( (person) => person.cal_address === user.userData.email ); const organizer = event?.attendee?.find( (a) => a.cal_address === event?.organizer?.cal_address ) ?? ({} as userAttendee); const isOwn = organizer?.cal_address === user.userData.email; const isOwnCal = userPersonnalCalendars.find((cal) => cal.id === calId); const attendeeDisplayLimit = 3; useEffect(() => { if (!event || !calendar) { onClose({}, "backdropClick"); } setRepetition(event?.repetition ?? ({} as RepetitionObject)); }, [open, eventId, dispatch, onClose, event]); if (!event || !calendar) return null; function handleRSVP(rsvp: string) { const newEvent = { ...event, attendee: event.attendee?.map((a) => a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a ), }; dispatch(putEventAsync({ cal: calendar, newEvent })); } const handleSave = async () => { const newEventUID = crypto.randomUUID(); const newEvent: CalendarEvent = { calId, title, URL: event.URL ?? `/calendars/${calId}/${newEventUID}.ics`, start: new Date(start), end: new Date(end), allday, uid: event.uid ?? newEventUID, description, location, repetition, class: eventClass, organizer: event.organizer, timezone, attendee: [organizer, ...attendees], transp: busy, color: userPersonnalCalendars[calendarid]?.color, alarm: { trigger: alarm, action: "EMAIL" }, }; const [baseId, recurrenceId] = event.uid.split("/"); if (recurrenceId) { Object.keys(userPersonnalCalendars[calendarid].events).forEach( (element) => { if (element.split("/")[0] === baseId) { dispatch(removeEvent({ calendarUid: calId, eventUid: element })); } } ); } await dispatch( putEventAsync({ cal: userPersonnalCalendars[calendarid], newEvent, }) ); if (newCalId !== calId) { dispatch( moveEventAsync({ cal: userPersonnalCalendars[calendarid], newEvent, newURL: `/calendars/${newCalId}/${event.uid}.ics`, }) ); dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid })); } onClose({}, "backdropClick"); }; const handleToggleShowMore = async () => { setShowMore(!showMore); }; const calList = calId.split("/")[0] === user.userData?.openpaasId ? Object.keys(userPersonnalCalendars).map((calendar, index) => ( {userPersonnalCalendars[index].name} )) : Object.keys(calendars).map((calendar, index) => ( {calendars[index].name} - {calendars[index].owner} )); return ( {/* Close button */} onClose({}, "backdropClick")} > {/* Title */} setTitle(e.target.value)} size="small" margin="dense" /> {/* RSVP */} {currentUserAttendee && isOwnCal && ( )} {/* Calendar selector */} Calendar {/* Dates */} setStart(formatLocalDateTime(new Date(e.target.value))) } size="small" margin="dense" InputLabelProps={{ shrink: true }} /> setEnd(formatLocalDateTime(new Date(e.target.value))) } 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)); } }} /> } label="All day" /> {/* Description & Location */} setDescription(e.target.value)} size="small" margin="dense" multiline rows={2} /> {isOwn && ( { const newAttendeeList = attendees.concat(value); setAttendees(newAttendeeList); }} /> )} setLocation(e.target.value)} size="small" margin="dense" /> {/* Video */} {event.x_openpass_videoconference && ( } text="Video conference available" data={event.x_openpass_videoconference} /> )} {/* Attendees */} {event.attendee?.length > 0 && ( Attendees: {organizer.cal_address && renderAttendeeBadge(organizer, "org", true)} {(showAllAttendees ? attendees : attendees.slice(0, attendeeDisplayLimit) ).map((a, idx) => ( {renderAttendeeBadge(a, idx.toString())} {isOwn && ( { const newAttendeesList = [...attendees]; newAttendeesList.splice(idx, 1); setAttendees(newAttendeesList); }} > )} ))} {attendees.length > attendeeDisplayLimit && ( setShowAllAttendees(!showAllAttendees)} > {showAllAttendees ? "Show less" : `Show more (${ attendees.length - attendeeDisplayLimit } more)`} )} )} {/* Extended options */} {showMore && ( <> Alarm Visibility is Busy {/* Error */} {event.error && ( } text={event.error} error /> )} )} {isOwn && ( { onClose({}, "backdropClick"); dispatch( deleteEventAsync({ calId, eventId, eventURL: event.URL }) ); }} > )} {isOwn && ( )} ); } export function InfoRow({ icon, text, error = false, data, }: { icon: React.ReactNode; text: string; error?: boolean; data?: string; }) { return ( {icon} {isValidUrl(data) ? {text} : text} ); } export function renderAttendeeBadge( a: userAttendee, key: string, isOrganizer?: boolean ) { const classIcon = a.partstat === "ACCEPTED" ? ( ) : a.partstat === "DECLINED" ? ( ) : null; return ( {classIcon} ) } > {a.cn || a.cal_address} {isOrganizer && ( Organizer )} ); } export function stringToColor(string: string) { let hash = 0; for (let i = 0; i < string.length; i++) { hash = string.charCodeAt(i) + ((hash << 5) - hash); } let color = "#"; for (let i = 0; i < 3; i++) { const value = (hash >> (i * 8)) & 0xff; color += `00${value.toString(16)}`.slice(-2); } return color; } export function stringAvatar(name: string) { return { sx: { width: 24, height: 24, fontSize: 18, bgcolor: stringToColor(name) }, children: name[0], }; }